[C#]System.Net.WebSocketsを試す。その2。サーバー編。

System.Net.WebSocketsを使用してサーバーを作成。
System.Net.WebSocketsには”WebSocketServer”とか分かりやすい名前はなく、クライアントからの待受はSystem.Net.HttpListenerクラスが行います。これはWebSocketのはじめのハンドシェイクはhttpにより行われることからですね。

詳しいことはMSDNの記事に載っています。

Windows 8 のネットワーク接続 Windows 8 と WebSocket プロトコル
http://msdn.microsoft.com/ja-jp/magazine/jj863133.aspx

上記の記事内のサンプルソースもとても参考になります。

エコーサーバーを作成してみる。
以前作成したSuperWebSocketを使用したサーバーと同じ動作をするものを作成してみます。

ソース

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using System.Net;
using System.Net.WebSockets;

using System.Diagnostics;

namespace WsEchoServerForWin8
{
    class Program
    {
        static void Main(string[] args)
        {
            StartServer();
            Console.WriteLine("{0}:Server start.\nPress any key to exit.", DateTime.Now.ToString());
            Console.ReadKey();
            Parallel.ForEach(_client,p=>
            {
                if (p.State == WebSocketState.Open) p.CloseAsync(WebSocketCloseStatus.NormalClosure, "", System.Threading.CancellationToken.None);
            });
        }

        /// <summary>
        /// クライアントのWebSocketインスタンスを格納
        /// </summary>
        static List<WebSocket> _client = new List<WebSocket>();

        /// <summary>
        /// WebSocketサーバースタート
        /// </summary>
        static async void StartServer()
        {
            /// httpListenerで待ち受け
            var httpListener = new HttpListener();
            httpListener.Prefixes.Add("http://192.168.1.10:12345/");
            httpListener.Start();

            while (true)
            {
                /// 接続待機
                var listenerContext = await httpListener.GetContextAsync();
                if (listenerContext.Request.IsWebSocketRequest)
                {
                    /// httpのハンドシェイクがWebSocketならWebSocket接続開始
                    ProcessRequest(listenerContext);
                }
                else
                {
                    /// httpレスポンスを返す
                    listenerContext.Response.StatusCode = 400;
                    listenerContext.Response.Close();
                }
            }
        }
        
        /// <summary>
        /// WebSocket接続毎の処理
        /// </summary>
        /// <param name="listenerContext"></param>
        static async void ProcessRequest(HttpListenerContext listenerContext)
        {
            Console.WriteLine("{0}:New Session:{1}", DateTime.Now.ToString(), listenerContext.Request.RemoteEndPoint.Address.ToString());

            /// WebSocketの接続完了を待機してWebSocketオブジェクトを取得する
            var ws = (await listenerContext.AcceptWebSocketAsync(subProtocol:null)).WebSocket;

            /// 新規クライアントを追加
            _client.Add(ws);

            /// WebSocketの送受信ループ
            while (ws.State == WebSocketState.Open)
            {
                try
                {
                    var buff = new ArraySegment<byte>(new byte[1024]);

                    /// 受信待機
                    var ret = await ws.ReceiveAsync(buff, System.Threading.CancellationToken.None);

                    /// テキスト
                    if (ret.MessageType == WebSocketMessageType.Text)
                    {
                        Console.WriteLine("{0}:String Received:{1}", DateTime.Now.ToString(), listenerContext.Request.RemoteEndPoint.Address.ToString());
                        Console.WriteLine("Message={0}", Encoding.UTF8.GetString(buff.Take(ret.Count).ToArray()));

                        /// 各クライアントへ配信
                        Parallel.ForEach(_client,
                            p => p.SendAsync(new ArraySegment<byte>(buff.Take(ret.Count).ToArray()),
                            WebSocketMessageType.Text,
                            true,
                            System.Threading.CancellationToken.None));
                    }
                    else if(ret.MessageType == WebSocketMessageType.Close) /// クローズ
                    {
                        Console.WriteLine("{0}:Session Close:{1}", DateTime.Now.ToString(), listenerContext.Request.RemoteEndPoint.Address.ToString());
                        break;
                    }
                }
                catch
                {
                    /// 例外 クライアントが異常終了しやがった
                    Console.WriteLine("{0}:Session Abort:{1}", DateTime.Now.ToString(), listenerContext.Request.RemoteEndPoint.Address.ToString());
                    break;
                }
            }

            /// クライアントを除外する
            _client.Remove(ws);
            ws.Dispose();
            
        }
    }
}

実行画面

※実行環境はWindows8で、実行するには管理者権限が必要でした。デバッグ時にはVS自体に管理者権限を付加。ホストPC外からの接続待ちには該当Portに対してファイアーウォールの設定を行う必要がありました。

なぜArraySegmentか、という考察。
送受信時に使用しているArraySegmentについて。ArraySegmentなんて初めて使いましたが。ArraySegmentを使う理由は上部の記事(MSDN)と記事内のサンプルソースを読むと分かってきます。サンプルソースではデータ受信時の処理がループを使用し複数回に分けて送られてくることを想定して作られています。SendAsyncに指定しているendOfMessage(3つめパラメータ)が重要で、要は、大きなデータを分割して送信し、最後の送信だけendOfMessage=trueを指定することでデータ送信の終わりを受信側に知らせることができます。受信側は最後のメッセージを受信した時点で1つのデータを完成させるわけですね。ArraySegmentを使用する理由は1次配列の操作をするためだからだでしょう、楽かどうか別として。

WebSocketライブラリによって動作が違う。
文字列”Hello WebSockets World!!”を3回に分けてSendAsyncを使って送信してみます。

string s = "Hello WebSockets World!!";
var sendData = Encoding.UTF8.GetBytes(s);
await ws.SendAsync(new ArraySegment<byte>(sendData.Take(5).ToArray()), WebSocketMessageType.Text, false, System.Threading.CancellationToken.None); // endOfMessage = false
await ws.SendAsync(new ArraySegment<byte>(sendData.Skip(5).Take(5).ToArray()), WebSocketMessageType.Text, false, System.Threading.CancellationToken.None); // endOfMessage = false
await ws.SendAsync(new ArraySegment<byte>(sendData.Skip(5 + 5).ToArray()), WebSocketMessageType.Text, true, System.Threading.CancellationToken.None); // endOfMessage = true

System.Net.WebSockets.ClientWebSocketの動作。
以前作成したClientWebSocketを使用したクライアントでデータを受信した時のトレース。

Receive
Count = 5
Type  = Text
EndOfMessage= False
Msg   = Hello

Receive
Count = 5
Type  = Text
EndOfMessage= False
Msg   =  WebS

Receive
Count = 14
Type  = Text
EndOfMessage= True
Msg   = ockets World!!

ReceiveAsyncが3回呼ばれます。これらはReceiveAsyncが返す戻り値の値ですが、EndOfMessageのフラグが送信通りになっています。

しかし、WebSocket4Netを使用したクライアントでは受信処理は1度だけで送信文字列は完成された状態で受信できます。WebSocket4NetのMessageReceivedイベントで渡ってくる受信データには文字列が格納されている変数しかなく、最後のデータであるかどうかは知るすべはありません。これはWebSocketの仕様に合わせた各ライブラリの仕様上の動作だと思われますが、そのへんは使用するライブラリによりある程度はコーディングなどでフォローするしかないようです。
ちなみに、WebSocket4Netを使用したクライアントはSendAsync間にSleep(or Task.Delay)を入れてあげないと、中途半端な文字列になったりしてうまく受信できませんでした。

System.Net.WebSockets.ClientWebSocketを使用したクライアントが受信した時。
Img20130312013413
WebSocket4Netを使用したクライアントが受信した時。
Img20130312013648

今回作成したソースはこちら

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA


This blog is kept spam free by WP-SpamFree.