ASP.NET MVC で WebSocket サーバを作る
ASP.NET MVC で WebSocket サーバを立てたくて色々調べた結果、あまり情報がなくて苦労したたのでここに記録を残します。
キーワード: ASP.NET MVC WebSocket Microsoft.WebSockets SignalR
背景
ASP.NET MVC で WebSocket やろうと思うと、SignalR という新しい仕組みがあるんですが、これを使うとクライアント側にも jquery.signalR.js を読み込まなくてはいけなくなるので、JS以外のクライアントで使えないって事と、もっと素に近い WebSocket を扱う方法を知りたかった、というわけです。
要するに WebSocket っぽい OnOpen(), OnClose(), OnMessage() などのメソッドを書いて作っていくやり方です。
NuGetにある Microsoft.WebSockets を利用すれば、それができそうだったのですが、ASP.NET でのやり方は書いてあるのに ASP.NET MVC でのやり方は見つけられませんでした。ま、SignalR 使えってことなんでしょうかね。。。(あれはあれで素晴らしいアーキテクチャだと思います。WebSocketでWCFのようにリモートメソッドコールが出来るとか、良くも悪くもMS的な発想だなと思います)
このへん に載ってる内容によると、ASP.NET で WebSocket を使うには、IHttpHandler を実装して ProcessRequest() メソッドのなかでゴニョゴニョするわけですが、それを ASP.NET MVC でやる方法を調べて試してみました。
環境
- Windows 8.1
- VisualStudio 2012
- ASP.NET MVC 4
- IE 10, Chrome などのWebSocket対応ブラウザ
IIS Express の制限により、Windows 7 ではダメです。Windows 8 または Windows Server 2012 で IIS 動かさないと WebSocket 使えません。*1
どうやってやるのか?
ASP.NET MVC ではコントローラを作ってアクションに対する処理を書いていきますが、IRouteHandler を実装した独自クラスを作れば、GetHttpHandler() メソッドで好きに IHttpHandler を返すことができます。そうすれば、ProcessRequest() でMVCのお作法を無視してリクエストを自由に処理することができます。
あとは、Microsoft.Web.WebSockets.WebSocketHandler を継承したクラスを作って、OnOpen() やら OnMessage() やらを実装すればよいわけです。
こちらのブログ も参考にさせていただきました。
わかりやすいサンプル実装
以下は WebSocket の動作検証までのサンプル実装です。チュートリアル的に書いてます。
まずプロジェクト新規作成します。「ASP.NET MVC 4 」「基本」で作ります。プロジェクト名は WebSocketTest1 としました。
NuGet にて Microsoft.WebSocket パッケージを導入します。「WebSocket」で検索したら出てきます。SignalR を入れちゃダメですよ。
サーバ側
ではコーディングです。まず WebSocketHandler を作りましょう。
プロジェクト右クリック→[追加]→[クラス] としてコントローラを新規作成します。
クラス名は WsHandler1 としました。
using Microsoft.Web.WebSockets; using Newtonsoft.Json.Linq; using System; using System.Diagnostics; namespace WebSocketTest1 { public class WsHandler1 : WebSocketHandler { private static WebSocketCollection AllClients = new WebSocketCollection(); public override void OnOpen() { Debug.WriteLine("OnOpen " + this.WebSocketContext.UserHostAddress); AllClients.Add(this); } public override void OnClose() { Debug.WriteLine("OnClose " + this.WebSocketContext.UserHostAddress); AllClients.Remove(this); } public override void OnError() { Debug.WriteLine("OnError "+ this.WebSocketContext.UserHostAddress); } public override void OnMessage(string jsonMsg) { Debug.WriteLine("OnMessage "+ this.WebSocketContext.UserHostAddress+ ": " + jsonMsg); JObject jsonObj = JObject.Parse(jsonMsg); jsonObj["time"] = DateTime.Now.ToString(); AllClients.Broadcast(jsonObj.ToString()); } } }
次に、IRouteHandler を実装して、GetHttpHandler() で独自の IHttpHandler を返すクラスを作ります。別々のクラスとして作っても良いですが、面倒なので一つのクラスにまとめました。
WsRouteHandler.cs
using Microsoft.Web.WebSockets; using System.Web; using System.Web.Routing; namespace WebSocketTest1 { public class WsRouteHandler<TWS> : IRouteHandler, IHttpHandler where TWS : WebSocketHandler, new() { public IHttpHandler GetHttpHandler(RequestContext requestContext) { return this; } public void ProcessRequest(HttpContext context) { if (context.IsWebSocketRequest) { var handler = new TWS(); context.AcceptWebSocketRequest(handler); } else { context.Response.StatusCode = 400; // bad request } } public bool IsReusable { get { return false; } } } }
ジェネリックを使って、外部から WebSocketHandler を指定できるようにしてます。DI(依存性の注入)っちゅーやつです。
それから、RouteConfig にルートを追加して、その独自クラスに割り当てる必要があります。
/App_Start/RouteConfig.cs を開き、MapRoute より前に以下の一文を追加します。
routes.Add(new Route("wsserver", new WsRouteHandler<WsHandler1>()));
これで、http://localhost/wsserver とアクセスされたときのルート定義を設定しているわけです。(ただしブラウザで直接アクセスしてもダメです。context.IsWebSocketRequest が false になるので、HTTP ERROR 400 を返します)
クライアント側
引き続き、WebSocket のクライアント側を作ります。
とりあえずコントローラを追加します。名前は HomeController 「空のMVCコントローラー」で生成し、Index() メソッド内で右クリックして [ビューの追加] を行います。
ビューの生成オプションはデフォルトで。生成された Index.cshtml に対して編集していきます。
@{ ViewBag.Title = "WebSocket Test"; } <h2>@ViewBag.Title</h2> <button id="connectBtn">Connect</button><br /> Message: <input type="text" id="message" /><button id="sendBtn">Send</button><br /> Log: <pre id="applog" style="width:100%; height:260px; overflow:auto; border:1px solid gray;"></pre> @section scripts { <script type="text/javascript"> var socket; $(function () { $('#connectBtn').click(function () { try{ socket = new WebSocket('ws://' + location.hostname + ':' + location.port + '/wsserver'); socket.onopen = function(evt) { log('connected.'); }; socket.onclose = function onClose(evt) { log('closed.'); }; socket.onerror = function onError(evt) { log('websocket error! '); }; socket.onmessage = function onMessage(evt) { log('reseive: ' + evt.data); }; } catch (e) { alert(e.message); } }); $('#sendBtn').click(function () { var obj = new Object(); obj.message = $('#message').val(); var jsonString = JSON.stringify(obj); socket.send(jsonString); log('send: ' + jsonString); $('#message').val(''); return false; }); }); function log(msg) { var txt = $('#applog').text(); txt = msg + '\n' + txt; $('#applog').text(txt); } </script> }
では実行してみましょう。
Connect ボタンを押して Message を入力して Send すると、応答が返ってきます。
サーバ側では、送られてきたメッセージに対して DateTime.Now を追加してブロードキャストするようにしています。2つのブラウザからアクセスして Send すると、双方のブラウザにメッセージが着信します。
iPhoneでも動きました。(IIS Express を外部ホストからアクセスできるようにする必要があります)