読者です 読者をやめる 読者になる 読者になる

アーキテクチャをスマートに。

株式会社ネオジニア代表。ITアーキテクトとしてのお仕事や考えていることなどをたまに綴っています。(記事の内容は個人の見解に基づくものであり、所属組織を代表するものではありません)

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 でやる方法を調べて試してみました。

環境

IIS Express の制限により、Windows 7 ではダメです。Windows 8 または Windows Server 2012IIS 動かさないと 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 を外部ホストからアクセスできるようにする必要があります)


簡単!

コード見てもらえればわかると思いますが、メッチャ簡単です!
簡易なチャットアプリぐらいならすぐに作れてしまいそうです。昔はTCPで 1対N 通信でチャットアプリみたいなの作ろうと思うと、サーバソケット作って listen して accept が来たら別スレッド立てて対話させて、って結構めんどくさいことやってました。それと比べるととんでもなく高レベルなAPIなわけで、しかもクライアントはHTML5のWebSocket対応ならOKなのでスマホでも動きます。
WebSocketが役に立つのは特定用途向けだと思いますが、応用の幅が広がると思います。