.NET Micro FrameworkからSignalRでデータをWebサイトに送る

…ようやくつながった。

※と思いきや、この投稿の内容では、SignalR2.0では動かないようなので、ご注意ください。SignalR2.0版は、後日投稿の予定です。

って訳で、.NET Micro Frameworkから、SignalRでデータをWebサイトに送信する方法を紹介します。想定シナリオとしては、.NET Micro Framework上でセンサー計測情報を送り続けるというものです。ネット越しにSignalRで.NET Micro Frameworkがデータを受け取りモータなどを制御するというシナリオも考えられますが、ネット越しにリアルタイム的な制御をするのは結構難しいので、ここでは、.NET Micro FrameworkからWebへのデータ送信のみを取り扱います。

先ず、SignalRって何って方、

https://msdn.microsoft.com/ja-jp/windowsazure/dn146079.aspx

こちらご覧ください。簡単にいうと、ネットワークを介したリアルタイム通信機構です。

Webサイト側は、

https://www.asp.net/signalr/overview/signalr-20/getting-started-with-signalr-20/tutorial-high-frequency-realtime-with-signalr-20

を参考に、Webアプリを作ってください。このWebアプリを動かすためのサイトは、https://www.windowsazure.com/ja-jp/ でサブスクリプション契約し、Webサイトを使ってくださいね。一日当たりの使用に制限つきですが無料枠あります。Webサイトの作成は、https://msdn.microsoft.com/ja-jp/windowsazure/jj983748.aspx  を参考にやってください。

Internet ExplorerのF12や、Fiddlerなどを使って解析すると、SignalRを使った通信は、最初にWebサイトに対してネゴシエーションを行い、ConnectionTokenを取得し、データを送り続けるという形式をとります。これを、.NET Micro Frameworkで実装すると、

ネゴシエーションフェーズ:

        string entryPoint = serverUri + "negotiate?_=1369908593886";
        var request = HttpWebRequest.Create(entryPoint) as HttpWebRequest;
        request.Method = "GET";
        var response = request.GetResponse() as HttpWebResponse;
        if (response.StatusCode == HttpStatusCode.OK)
        {
            using (var resStream = response.GetResponseStream())
            {
                var reader = new StreamReader(resStream);
                var jsonContent = reader.ReadToEnd();
                int ctStartIndex = jsonContent.IndexOf(keyConnectionType);
                jsonContent = jsonContent.Substring(ctStartIndex + keyConnectionType.Length + 3);
                connectionToken = jsonContent.Substring(0, jsonContent.IndexOf("\""));
                int ciStartIndex = jsonContent.IndexOf(keyConnectionId);
                jsonContent = jsonContent.Substring(ciStartIndex + keyConnectionId.Length + 3);
                connectionId = jsonContent.Substring(0, jsonContent.IndexOf("\""));
                isConnected = true;
                order = 0;
                UpdatePosition(serverUri, topPos, leftPos, order);
                order++;
            }
        }

※このコードでは、ConnectionTokenに'+'が含まれると正しくサーバーに送信できません。上のコードに、'+'が入っている場合には、"%2b"に置き換える処理を加えてください。

serverUriは、SignalRをホストするサイトのURLの下のsignalrです。例えば、ローカルのエミュレータを使っている場合は、"https://localhost:53925/signalr/" といった様に、WebサイトのURLに/signalr/を加えた文字列です。.NET MFの通常のHttp WebアクセスのコードでWebサイトから応答を取得します。JSON形式で送られてくるので、そこから、ConnectionTokenを取り出します。コードの中のkeyConnectionTypeは、”ConnectionToken"という値をセットしたstringです。orderは、int型の変数。データ送信フェーズで使います。
※keyConnectionIdは、”ConnectionId”

データ送信フェーズ:

 実装は、以下の様になります。

        string sendUri = serverUri + "send?transport=foreverFrame&connectionToken=" + connectionToken;
        using (var sendRequest = HttpWebRequest.Create(sendUri) as HttpWebRequest)
        {
            sendRequest.Method = "POST";
            sendRequest.ContentType = "application/x-www-form-urlencoded; charset=UTF-8";
            sendRequest.KeepAlive = true;
            sendRequest.Timeout = 3000000;

            string updateContent = "{\"H\":\"moveshapehub\",\"M\":\"UpdateModel\",\"A\":[{\"top\":" + topPos + ",\"left\":" + leftPos + "}],\"I\":" + order + "}";
            string encoded = "data=" + HttpUtility.UrlEncode(updateContent);
            byte[] content = System.Text.UTF8Encoding.UTF8.GetBytes(encoded);
            sendRequest.ContentLength = content.Length;
            using (var reqStream = sendRequest.GetRequestStream())
            {
                reqStream.Write(content, 0, content.Length);
                var updateResponse = sendRequest.GetResponse() as HttpWebResponse;
                if (updateResponse.StatusCode == HttpStatusCode.OK)
                {
                    Debug.Print("Succeeded");
                }
                using (var sendResStream = updateResponse.GetResponseStream())
                {
                    byte[] resContentBytes = new byte[updateResponse.ContentLength];
                    sendResStream.Read(resContentBytes, 0, (int)updateResponse.ContentLength);
                    char[] resContentChars = System.Text.UTF8Encoding.UTF8.GetChars(resContentBytes);
                    string resContent = new string(resContentChars);
                    Debug.Print(resContent);
                    order++;
                }
                updateResponse.Dispose();
            }
        }

 serviceUriは、ネゴシエーションの時と同じです。POSTメソッドで、通知するデータをJSON形式で送付します。MoveShapeのデモでは、

data={"H":"moveshapehub","M":"UpdateModel","A":[{"top":27,""left":105}],"I":3}

というデータを送付しています。大した量ではないので上のコードではべた書きです。上のコードでは、最後のIの値にorderというint変数を指定しています。送信順を指定しているようなので、クラスのメンバー変数などで用意しておいて、順次増していきます。
topPos、leftPosは、int型の変数で、ブラウザの左上からの座標値を指定します。.NET MFのエミュレータの場合、真ん中のボタンでネゴシエーション、UP、DOWN、LEFT、RIGHTボタンで上下左右に座標を動かす、といった感じでコーディングしてください。

        int topPos = 200;
        int leftPos = 200;
        int order = 0;
        private void OnButtonUp(object sender, RoutedEventArgs evt)
        {
            string serverUri = "https://localhost:53925/signalr/";
            ButtonEventArgs e = (ButtonEventArgs)evt;
            if (e.Button==Button.VK_SELECT)
            {
                try
                {
                    // ネゴシエーションコード
                    isConnected = true;
                }
                catch (Exception ex)
                {
                    Debug.Print("Negotiation - " + ex.Message);
                }
            }
            else
            {
                if (isConnected)
                {
                    int delta = 10;
                    switch (e.Button)
                    {
                        case Button.VK_LEFT:
                            leftPos -= delta;
                            if (leftPos < 0)
                            {
                                leftPos = 0;
                            }
                            break;
                        case Button.VK_UP:
                            topPos -= delta;
                            if (topPos < 0)
                            {
                                topPos = 0;
                            }
                            break;
                        case Button.VK_RIGHT:
                            leftPos += delta;
                            break;
                        case Button.VK_DOWN:
                            topPos += delta;
                            break;
                    }
                    // 更新通知コード
                    order++;
                }
            }
        }

こんな感じ。

HttpUtilityというクラスがコードの中に出てきますが、UrlEncode()メソッドで、URLパラメータ用の変換を施しています。.NET Micro Frameworkでは残念ながらこの機能がないので、

        public static string UrlEncode(string s)
        {
            char[] encodedChar = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
                int num0='0';
                int num9='9';
                int chara='a';
                int charz='z';
                int charA='A';
                int charZ='Z';
            string encoded = "";
            foreach (var c in s.ToCharArray())
            {
                int cv = c;
                if ((num0 <= cv && cv <= num9) || (chara <= cv && cv <= charz) || (charA <= cv && cv <= charZ))
                {
                    encoded += c;
                }
                else
                {
                    encoded += "%";
                    int ch = c >> 4;
                    int cl = c & 0x0f;
                    if (ch > 0x8)
                    {
                        throw new ArgumentOutOfRangeException("should be ascii code");
                    }
                    encoded += encodedChar[ch];
                    encoded += encodedChar[cl];
                }
            }
            return encoded;
        }

 こんなコードで代用しています。(バグありかも)

以上、.NET Micro Frameworkの方は、Emulatorでも試せるので、是非やってみてくださいね。