見出し画像

自動売買システム日経225ミニ専用をC#で作る。Webhookサーバーを作成する


「自動売買システム日経225ミニ専用をC#で作る」の概要 自動取引売買システム構成図 ③のLocal Webhook Serverを作成しngrokでlocal severを外部公開します。
 ネットを検索するといくつかのWebhook Serverの紹介記事が見つかります。python Flask等 今回は「C#で作る」がテーマなのでC#用のwebSeverを探しました。ありました!
SimpleWebSever名前のとうり非常にシンプルで使いやすそうです。これを拝借しこれにTCP Sockeとを組み込み作成します。

開発環境

・ Windows 10
・ Visual studio 2022

SimpleWebServerをGithubよりダウンロードする

github soleil-taruto/Hatena 上でcodeをクリックしURLをコピーする。

ダウンロード元

自分のPCにクローンを作成する

コマンドプロンプトをオープンする。
git clone をタイプしコピーしたアドレスを貼り付け⏎
pc上の c\xxx\Hatenaを確認しあればOKです

Visual Studio 2022を開きプロジェクトを作成する

1.新しいプロジェクトの作成(N)選択⏎
2.コンソールアプリを選択し次へ(N) ⏎
3.プロジェクト名に「WebhookServer」場所(L)にプロジェクト作成ディレクトリを指定し次へ(N) ⏎
4.フレームワークを選択し作成(C) ⏎
以上でプロジェクトは作成されました。

クローンで作成したファイルからSimpleWebServerをプロジェクトに追加する

ソリューションエクスプローラーウインドウでWebhookServerプロジェクトをセレクトし右クリックしサブウインドウを開き「追加」ー>「既存の項目」
C:\Users\xxxxxx\Hatena\a20211009\SimpleWebServer\SimpleWebServer.cs と Program.cs  2ファイルを追加する

TCP Soket Server用のプロジェクト作成

  1. ソリューション エクスプローラーでソリューションを右クリックし、 [追加]>[新しいプロジェクト] の順に選択します。

    1. [新しいプロジェクトの追加] ページで、検索ボックスに「ライブラリ」と入力します。

    2. 言語の一覧 から [C#] を選択し、次に、プラットフォームの一覧から [すべてのプラットフォーム] を選択します。 [クラス ライブラリ] テンプレートを選んでから、 [次へ] を選択します。

    3. [新しいプロジェクトの構成] ページで、 [プロジェクト名] ボックスに「SocketServer」と入力してから、 [次へ] を選択します。

    4. [追加情報] ページで、[.NET6 (長期的なサポート)] を選択し、[作成] を選択します。

Socketを使ったサーバーサーバー作成

1.IPアドレスとポート番号を指定して、ローカルエンドポ 
  イントを設定する
2.TCP/IPのソケットを作成
3.TCP/IPのソケットをローカルエンドポイントにバインド
4.データを待つ
6.非同期ソケットを開始して、接続をリッスンする
7.端末からデータ受信を待ち受ける
8.受信したメッセージをクライアントに送信する
以上を非同期のsocketメソッドを使って実装します。

次のコードをコピーしSocketServerクラスを作成する

public class TCPServer
    {
        //マニュアルリセットイベントのインスタンスを生成
        public ManualResetEvent allDone = new ManualResetEvent(false);

        StateObject? state = null;

        public async void Start(int port)
        {
            await StartListening(port);
        }

        //TCP/IPの接続開始処理
        public async Task<bool> StartListening(int port)
        {
            // IPアドレスとポート番号を指定して、ローカルエンドポイントを設定
            IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, port);

            // TCP/IPのソケットを作成
            Socket TcpServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            try
            {
                TcpServer.Bind(localEndPoint);  // TCP/IPのソケットをローカルエンドポイントにバインド
                TcpServer.Listen(10);           // 待ち受け開始

                await Task.Run(()=>
                {
                    while (true)
                    {
                        // シグナルの状態をリセット
                        allDone.Reset();

                        // 非同期ソケットを開始して、接続をリッスンする
                        Debug.WriteLine("接続待機中...");
                        TcpServer.BeginAccept(new AsyncCallback(AcceptCallback), TcpServer);

                        // シグナル状態になるまで待機
                        allDone.WaitOne();
                    }
                });

            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.ToString());
            }

            return false;
        }
        public Action<object> WriteLog { get; set; }

        public void AcceptCallback(IAsyncResult ar)
        {
            // シグナル状態にし、メインスレッドの処理を続行する
            allDone.Set();
            WriteLog("TCP Cilent 接続");
            // クライアント要求を処理するソケットを取得
            Socket TcpServer = (Socket)ar.AsyncState;
            Socket TcpClient = TcpServer.EndAccept(ar);

            // 端末からデータ受信を待ち受ける
            state = new StateObject();
            state.workSocket = TcpClient;
            TcpClient.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state);
        
        }

        public void SendMessage(byte[] message)
        {
            if (state != null && state.workSocket != null)
            {
                Socket TcpClient = state.workSocket;

                // クライアントへデータの送信を開始
                TcpClient.BeginSend(message, 0, message.Length, 0, new AsyncCallback(SendCallback), TcpClient);

            }
        }

        public static void ReceiveCallback(IAsyncResult ar)
        {
            var content = string.Empty;

            try
            {
                // 非同期オブジェクトからソケット情報を取得
                StateObject state = (StateObject)ar.AsyncState;
                Socket TcpClient = state.workSocket;

                // クライアントソケットからデータを読み取り
                int bytesRead = TcpClient.EndReceive(ar);

                if (bytesRead > 0)
                {
                    // 受信したデータを蓄積
                    state.sb.Append(Encoding.ASCII.GetString(state.buffer, 0, bytesRead));

                    // 蓄積データの終端タグを確認
                    content = state.sb.ToString();
                    if (content.IndexOf("<EOF>") > -1)
                    {
                        // 終了タグ<EOF>があれば、読み取り完了
                        Debug.WriteLine(string.Format("クライアントから「{0}」を受信", content));

                        // ASCIIコードをバイトデータに変換
                        byte[] byteData = Encoding.ASCII.GetBytes("OK");

                        // クライアントへデータの送信を開始
                        TcpClient.BeginSend(byteData, 0, byteData.Length, 0, new AsyncCallback(SendCallback), TcpClient);
                    }
                    else
                    {
                        // 取得していないデータがあるので、受信再開
                        TcpClient.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state);
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("[" + DateTime.Now + "] " + ex.Message);

            }
        }
        private static void SendCallback(IAsyncResult ar)
        {
            try
            {
                // 非同期オブジェクトからソケット情報を取得
#pragma warning disable CS8600 // Null リテラルまたは Null の可能性がある値を Null 非許容型に変換しています。
                Socket TcpClient = (Socket)ar.AsyncState;
#pragma warning restore CS8600 // Null リテラルまたは Null の可能性がある値を Null 非許容型に変換しています。

                // クライアントへデータ送信完了
#pragma warning disable CS8602 // null 参照の可能性があるものの逆参照です。
                int bytesSent = TcpClient.EndSend(ar);
#pragma warning restore CS8602 // null 参照の可能性があるものの逆参照です。
                
                Console.WriteLine("[" + DateTime.Now + "] " + "Messageをクライアントへ送信");
                
                //ソケット通信を終了
                Debug.WriteLine("接続終了");
                //TcpClient.Shutdown(SocketShutdown.Both);
                //TcpClient.Close();

            }
            catch (Exception e)
            {
                Debug.WriteLine(e.ToString());
            }
        }
        
    }
    // 非同期処理でソケット情報を保持する為のオブジェクト
    public class StateObject
    {
        // 受信バッファサイズ
        public const int BufferSize = 1024;

        // 受信バッファ
        public byte[] buffer = new byte[BufferSize];

        // 受信データ
        public StringBuilder sb = new StringBuilder();

        // ソケット
        public Socket? workSocket = null;
    }

WebhookServerプロジェクト変更

Program.csを以下コードに変更してください

static void Main(string[] args)
        {
            Console.SetWindowSize(80, 20);

            SimpleWebServer.Run(
                @"C:\www\DocRoot",   // ドキュメントルート (index.html 等を配置したフォルダ) を指定する。
                            80);        // ポート番号 (特に理由が無ければ 80 を指定する)
        }


SimpleWebServer.csの変更

TCPServerのインスタンスをSimpleWebServer.csに作成します
SimpleWebServerのメソッドRun()に下記コードを追加する
(public static void Run(string docRoot, int portNo))

87行 public static TCPServer? tcpserver = null;
100行目から下記5行を追加する
1.//socket server 起動
2.int port = 8000;
3.tcpserver = new TCPServer();
4.tcpserver.WriteLog = WriteLog;
5.tcpserver.Start(port);

// tcpserver プロパティ設定
        public static TCPServer? tcpserver = null;
		public static void Run(string docRoot, int portNo)
		{
			try
			{
				if (string.IsNullOrEmpty(docRoot))
					throw new Exception("ドキュメントルートを指定して下さい。");

				if (!Directory.Exists(docRoot))
					throw new Exception("ドキュメントルートは存在しません:" + docRoot);

				if (portNo < 1 || 65535 < portNo)
					throw new Exception("不正なポート番号です:" + portNo);
				
				//socket server 起動
				int port = 8000;
				tcpserver = new TCPServer();
				tcpserver.WriteLog = WriteLog;
				tcpserver.Start(port);

次にメソッド”POST”を書き換える
148行目 case "POST"の下の行
 
throw new Exception("Unsupported method"); を下記2行に書き換え変更する
this.Webhook(channel, false);
break;

Webhook()メソッドの作成

Webhook()メソッドを173行目に作成する

private void P_Connected(HTTPServerChannel channel)
		{
			WriteLog("Client: " + channel.Channel.Handler.RemoteEndPoint);

			if (7 < channel.Method.Length) // ? 最も長いメソッドより長い。
				throw new Exception("Received method is too long");

			WriteLog("Method: " + channel.Method);

			switch (channel.Method)
			{
				case "GET":
					this.GetOrHead(channel, false);
					break;

				case "HEAD":
					this.GetOrHead(channel, true);
					break;

				case "POST":
					this.Webhook(channel, false);
					break;
				case "PUT":
					throw new Exception("Unsupported method");

				case "DELETE":
					throw new Exception("Unsupported method");

				case "CONNECT":
					throw new Exception("Unsupported method");

				case "OPTIONS":
					throw new Exception("Unsupported method");

				case "TRACE":
					throw new Exception("Unsupported method");

				case "PATCH":
					throw new Exception("Unsupported method");

				default:
					throw new Exception("Unknown method");
			}
		}

        private void Webhook(HTTPServerChannel channel, bool v)
        {
			int readLen = channel.Body.Length;
			string content = System.Text.Encoding.ASCII.GetString(channel.Body, 0, readLen);

			tcpserver.SendMessage(channel.Body);
			System.Diagnostics.Debug.WriteLine($"contentbody: {content}");
		}

「private class HTTPBodyOutputStream : IDisposable」を変更する。このクラスの上の行に「/// POSTを使う場合は#if falseとする」と記述がある これに従って true を falseに変更する。
以上でWebhookServerが出来ました。次回はSocket受信クライエントを作成しサーバーのテストとサーバーを外部に公開する記事を予定しています。








この記事が気に入ったらサポートをしてみませんか?