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
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を使用したクライアントが受信した時。
WebSocket4Netを使用したクライアントが受信した時。
今回作成したソースはこちら。