WebSocket サーバー開発 : ASP.NET 4.5 編


(2012/06/04 : RC 版にあわせて、補足事項を追記)

WebSocket サーバー開発

こんにちは。

ここでは、セッションで紹介した ASP.NET の WebSocket を紹介します。(ちょっと量があるので、.NET 4.5 / ASP.NET 4.5、WCF、SignalR と、3 回にわけて紹介します。) また、セッションで説明できなかったプログラミングの細かなポイント (落とし穴など) も注記します。

なお、セッションで解説した背景やメリット、代替案、Windows 環境におけるテクノロジー スタック等々については、ここでは説明を省略します。(これらについては、セッションの Recording が公開されると思うので、そちらをご覧ください。)

まず今回は、冒頭で紹介した ASP.NET 4.5 (.NET Framework 4.5) を使ったプログラミングを紹介します。

 

環境設定 (セットアップ)

環境面での注意事項については、セッションでお話しした通りです。(Windows Server 8 の http.sys が必要です。)

ここで紹介するサンプルを動かすには、セッションで説明したように、IIS 8 のインストール時に、役割サービスとして、WebSocket Protocol (下図) を入れておきましょう。

補足 : あと、Visual Studio 2012 を入れる前に、Windows Server 2012 上の .NET Framework 4.5 や IIS の機能をインストールしておいてください。

また、現実に使用する際には、セッションでも説明したように、皆さんの環境のプロキシーやファイアウォールなどの設定にも注意してください。(簡単なサンプルなどで試してみてください。)

 

Server Programming

まず、ASP.NET アプリケーション (ASP.NET MVC など) のプロジェクトを新規作成し、Web.config を開いて、下記 (太字) の設定を追加します。(2012/06/04 追記)

<configuration>
  <appSettings>
    . . .
    <add
      key="aspnet:UseTaskFriendlySynchronizationContext"
      value="true" />
  </appSettings>
  . . .

紹介したように、.NET Framework 4.5 の ASP.NET では、WebSocket 関連のクラスやメンバーが追加されており、これらを使用して WebSocket のサーバー処理を迅速に開発できます。

さっそく、サンプルを作成してみましょう。
ここでは、セッションでも紹介した、複数参加型のゲームのサンプルを構築します。プレイヤーは WebSocket を Open して参加し、カードの種類 (Spades, Hearts, Clubs, Diamonds) と金額 (Bets) を送信すると、参加しているすべてのプレイヤーにその情報 (誰が、どこに、いくら賭けたか ?) が通知されるという簡単なサンプルです。

まず、以下の手順で、ASP.NET の ハンドラーを追加し、このハンドラーのコードで WebSocket の処理を記述します。

プロジェクトをマウスで右クリックして、[追加] - [新しい項目] で、[Web] - [Generic Handler] を追加します。(プロジェクトに、ハンドラーのマークアップ (.ashx) とコード ビハインド (.ashx.cs) が追加されます。)

ここ (.ashx.cs) に、下記のコードを記述します。
AcceptWebSocketRequest の引数には、受信メッセージを処理する Func<System.Web.WebSockets.AspNetWebSocketContext,System.Threading.Tasks.Task> 型の Func を指定します。(下記の BetsHandler1 クラスは、このあと実装します。)

. . .
using Newtonsoft.Json.Linq;
using System.Threading.Tasks;
using System.Web.WebSockets;
. . .

public class Handler1 : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        if (context.IsWebSocketRequest)
        {
            // we'll implement Betshandler, later . . .
            BetsHandler1 handler = new BetsHandler1();
            context.AcceptWebSocketRequest(handler.Receive);
        }
        else
        {
            context.Response.StatusCode = 400; // bad request
        }
    }

    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
}

補足 : なお、セッションで、HttpListener の WebSocket サポートについて触れましたが、HttpListener を使った自己ホストの場合には、下記の通り実装します。(次回説明する WCF では、自己ホストの場合、内部でこの HttpListener の WebSocket が使用されます。)

. . .
using System.Net;
. . .

static void Main(string[] args)
{
    HttpListener listener = new HttpListener();
    listener.Prefixes.Add("http://*:81/");
    listener.Start();

    while (true)
    {
        HttpListenerContext context = listener.GetContext();
        context.AcceptWebSocketAsync( . . . )
        . . .
    }
}
. . .

IsWebSocketRequest は、HTTP 要求が WebSocket を要求しているか確認し、AcceptWebSocketRequest は、WebSocket の Negotiation を処理します。

では、実際に、WebSocket への送受信 (Send / Receive) をさばく処理 (上記の BetsHandler1) を実装します。下記の通り実装します。

今回は、クライアント側から Json フォーマットの文字列 (Spades、Hearts などの対象と、賭け金のデータ) を受け取り、この Json オブジェクトに、誰 (who) の賭け金であるか情報を付加し、このデータを参加している BetsHandler1 インスタンスすべて (クライアント側) に Send (通知) します。
また、セッションで説明したように、WebSocket の最初のネゴシエーションでは、標準的な MIME 形式の HTTP のエンベロープを使用してやり取りされます。このため、開始時のパラメーター情報 (今回の場合、nickname) は、Query 文字列や Cookie などを使用して渡すことができます。

. . .

public class BetsHandler1
{
  private static List<BetsHandler1> connectedHandlers =
    new List<BetsHandler1>();
  private string NickName;
  public System.Net.WebSockets.WebSocket webSocket;

  public async Task Receive(AspNetWebSocketContext context)
  {
    this.NickName = context.QueryString["nickname"];
    connectedHandlers.Add(this);

    webSocket = context.WebSocket;
    ArraySegment<byte> buf = new ArraySegment<byte>(new byte[2048]);
    while (true)
    {
      System.Net.WebSockets.WebSocketReceiveResult res =
        await webSocket.ReceiveAsync(
          buf,
          System.Threading.CancellationToken.None);
      if (res.MessageType ==
        System.Net.WebSockets.WebSocketMessageType.Close)
      {
        // Close Message
        connectedHandlers.Remove(this);
        await webSocket.CloseOutputAsync(
          System.Net.WebSockets.WebSocketCloseStatus.NormalClosure,
          "closing ... (not wait)",
          System.Threading.CancellationToken.None);
        break;
      }
      else if (res.MessageType ==
        System.Net.WebSockets.WebSocketMessageType.Text)
      {
        // Text Message
        string jsonMsg =
          System.Text.Encoding.ASCII.GetString(buf.Array, 0, res.Count);
        JObject jsonObj = JObject.Parse(jsonMsg);
        jsonObj["nickname"] = this.NickName;
        BetsHandler1.Broadcast(jsonObj.ToString());
      }
    }
  }

  public static void Broadcast(string jsonMsg)
  {
    foreach (var handler in connectedHandlers)
    {
      ArraySegment<byte> buf = new ArraySegment<byte>(
        System.Text.Encoding.ASCII.GetBytes(jsonMsg));
      handler.webSocket.SendAsync(
        buf,
        System.Net.WebSockets.WebSocketMessageType.Text,
        true,
        System.Threading.CancellationToken.None);
    }
  }
}
. . .

補足 : なお、受信するメッセージには、WebSocketMessageType.Close、WebSocketMessageType.Text (UTF-8 文字列)、WebSocketMessageType.Binary (バイナリー) の 3 種類があります。例えば、JavaScript 側で下記のようにデータを渡すと、バイナリーとして認識されます。(バイナリーには、BLOB または ArrayBuffer が使用可能で、下記は、後者の場合です。)

var byteArray = new Uint8Array(3);
byteArray[0] = 0x01;
byteArray[1] = 0x01;
byteArray[2] = 0x01;
websocket.send(byteArray);

上記のコードを見ておわかりの通り、.NET Framework 4.5 を使用した WebSocket サーバー開発では、ASP.NET レベル (System.Web) ではなく、最終的に、TCP レベルの System.Net.WebSockets.WebSocket オブジェクトを使用します。(つまり、Negotiation 以降は、より Tcp レベルに近いやり取りを開発者自らがプログラミングします。)
上記の Receive メソッドでは、WebSocket によるブラウザーからのデータ送信 (JavaScript の WebSocket.send) だけではなく、WebSocket の Close など、すべてのメッセージが通知されるため、プログラミングによって、これらの振り分けをおこなう必要があります。

 

配置

セッションで紹介したように、上記の WebSocket の処理を実行するには IIS 8 が必要です。(Visual Studio 11 が既定で使用する IIS Express 7.5 には、WebSocket は入っていません。)
このため、プロジェクトのプロパティを表示して、[Web] タブを開き、下図の通り、[IIS Express を使用する] のチェックボックスを外して、IIS を使用します。(さらに、[仮想ディレクトリの作成] をクリックして、IIS アプリケーションも作成しておきましょう。)

補足 (2012/05 追記) : 今月更新された IIS 8 Express Beta には WebSocket が含まれています。(こちら を参照してください。) このため、この最新版の IIS 8 Express を使ってデバッグが可能です。

補足 (2012/06 追記) : 先日リリースされた Visual Studio 2012 RC 版に、IIS 8 Express 最新版が入りました ! 今後は、IIS Express を使って、WebSocket サーバーのデバッグが可能です。

 

Client Programming (HTML / JavaScript)

では、上記の WebScoket サーバーを使用するクライアント側の処理を作成してみましょう。
今回は、ASP.NET MVC (View Engine として Razor) を使って、下記の通り作成してみます。(HTML 5 を使用するため、DOCTYPE にも注意してください。)

_Layout.cshtml

<!DOCTYPE html>
<html lang="en">
    <head>
        . . .

        @RenderSection("script", false)
    </head>
    <body>
        . . .

Index.cshtml

<h3>WebScoket Demos !</h3>

Target:
<select id="target" size="1">
    <option value="spades">Spades</option>
    <option value="hearts">Hearts</option>
    <option value="clubs">Clubs</option>
    <option value="diamonds">Diamonds</option>
</select>

Tip:<input id="bets" type="number" />$

<br />

<button id="sendbutton">Bets !</button>

@section script {
    <script type="text/javascript">
    $(document).ready(function () {
        var nickname = prompt('What is your nickname ?');
        websocket =
          new WebSocket('ws://localhost/ws_server1/Handler1.ashx?nickname='
           + nickname);
        websocket.onerror = function (evt) {
            alert('errored : ' + evt.data);
        };
        websocket.onopen = function (evt) {
            alert('opened');
        };
        websocket.onclose = function (evt) {
            alert('closed');
        };
        websocket.onmessage = function (evt) {
            var res = JSON.parse(evt.data);
            alert(res.nickname + ' bets '
                + res.bets + '$ to ' + res.target + '.');
        };

        $('#sendbutton').click(function () {
            var obj = new Object();
            obj.target = $('#target').val();
            obj.bets = $('#bets').val();
            websocket.send(JSON.stringify(obj));
        });
    });

    </script>
}

 

動作確認

実行結果は、セッションで見ていただいた通りです。ブラウザー (WebSocket に対応したブラウザー) を複数立ち上げて接続し、[Bets] ボタンを押すと、すべてのブラウザーに alert が表示されます。(下図)

 

Server Programming (NuGet - Microsoft.WebScokets package の場合)

つぎに、NuGet の Microsoft.WebScokets パッケージと組み合わせた場合のソースコードを紹介します。

セッションで説明したように、ASP.NET 4.5 と Microsoft.WebScokets パッケージのヘルパーを使うと、さらに短いコードで、より直観的に処理が作成できます。上記と同じ処理を、今度は、Microsoft.WebScokets パッケージを使って記述してみましょう。

まず、Visual Studio 11 を使って、NuGet Package の Microsoft.WebSockets をインストールします。(プロジェクトに、必要なアセンブリのダウンロードや、参照追加などがおこなわれます。)

上記同様、[追加] - [新しい項目] で、[Web] - [Generic Handler] を追加します。(今回は、Handler2.ashx とします。)
このハンドラーに、今度は、下記の通りコードを記述します。さきほどのコードと比較してみてください。

. . .

using Microsoft.Web.WebSockets;
using Newtonsoft.Json.Linq;
. . .

public class Handler2 : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        // Using Microsoft.Web.WebSockets package,
        // you can take an argument as Microsoft.Web.WebSockets.WebSocketHandler !
        if (context.IsWebSocketRequest)
        {
            context.AcceptWebSocketRequest(new BetsHandler2());
        }
        else
        {
            context.Response.StatusCode = 400; // bad request
        }
    }

    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
}

public class BetsHandler2 : WebSocketHandler
{
    private static WebSocketCollection connectedHandlers =
      new WebSocketCollection(); // shared for all !

    private string NickName;

    public override void OnOpen()
    {
        this.NickName = this.WebSocketContext.QueryString["nickname"];
        connectedHandlers.Add(this);
    }

    public override void OnClose()
    {
        connectedHandlers.Remove(this);
    }

    public override void OnError()
    {
        base.OnError();
    }

    public override void OnMessage(string jsonMsg)
    {
        JObject jsonObj = JObject.Parse(jsonMsg);
        jsonObj["nickname"] = this.NickName;
        connectedHandlers.Broadcast(jsonObj.ToString());
    }
}

Microsoft.WebSockets パッケージを使うと、上記の通り、AcceptWebSocketRequest メソッドの引数に Microsoft.Web.WebSockets.WebSocketHandler オブジェクトを渡すことができます。
そして、この Microsoft.Web.WebSockets.WebSocketHandler では、上記の ASP.NET 4.5 だけの場合よりも、さらに直観的にプログラミングできていることがわかります。(上記の通り、受信するメッセージは、OnMessage、OnClose、OnError の各 override メソッドを使って記述できます。)

補足 : なお、同様に、バイト列などのバイナリーを扱えます。バイナリーを受信する際は、下記の override メソッドを実装します。

public override void OnMessage(byte[] message)
{
    . . .
}

クライアントのコードや、動作結果は、さきほどと同じです。

 

プログラミングの留意点

上記はシンプルにコードを紹介しましたが、実際のプログラミングでは、いろいろと注意しなければならないことがありますので、代表的な内容をメモしておきます。

  • データは UTF-8 か Binary
    セッションでも説明しましたが、WebSocket のメッセージ交換では、UTF-8 テキストか、バイナリーを送信することが可能です。このため、日本語 Shift-JIS などを扱う場合には注意してください。(文字化けします。) こうした場合は、エンコード / デコードをおこなうか、バイナリーとして扱ってサーバー側でコード化するなど、配慮してください。
  • Windows Azure への配置や ロード バランサー (Load Balance) 構成への対応
    セッションでも説明しましたが、上記のコードのまま Windows Azure に配置した場合や、ロード バランサー構成に配置した場合、正しく動作しません。(Windows Azure では、既定でラウンド ロビンが使用されます。)
    このため、実際の開発では、Windows Azure の Queue や Intenal Endpoint によるインスタンス間の通信や、専用のライブラリー (サード パーティー製の専用の仕組み、など) と連携するなど、負荷分散された状態で動作するよう細やかな設計・実装をおこなってください。(上記では、こうした処理を省略しています。)
    なお、Windows Azure を使用した場合、もう一点注意すべき重要な点 (Keep Alive の問題) がありますが、ここでは説明を省略します。(セッションで説明した通りです。)
  • ブラウザーによる細かな相違にも注意
    すみません、ここ、説明する時間が無かったのですが、実際の開発では、ブラウザーによる細かな相違にも注意しましょう。
    例えば、WebSocket が突然廃棄された場合、ブラウザーの種類によって Close が呼ばれる場合と、呼ばれない場合があります。(この結果、上記のコードの場合、不正な接続がサーバー側に残ってしまい、次回以降、エラーとなります。) また、通信エラーなどの際も、ブラウザーによっては onerror が呼ばれません。(onclose のみが呼ばれます。) さらに、上記の connectedHandlers の要素の 1 つが、ネットワーク トラブルなど何らかの原因によって急に使用できなくなることも考えられます。(こうした場合も Close が呼ばれるとは限りません。)
    このため、実際の開発では、こうした検査のためのコードも丁寧に入れておきましょう。
  • 認証 (Authentication) について
    認証について気になる方も多いかと思いますが、WebSocket の認証 (Authentication) については、仕様上は細かな記述はないようです。
    WebSocket の確立後に独自に認証のためのやりとりをおこなっても良いですが、WebSocket の Negotiation では通常の HTTP ヘッダーを扱えるため (これは、仕様にも、そう明言されています)、例えば、ドメイン A で認証 (フォーム認証、Basic 認証など) をおこない、このクライアント (ブラウザー) を使って、A で稼働している WebSocket サーバーを呼び出すと、Negotiation の際、サーバー側には必要な認証 Cookie などが渡されます。(念のため、Internet Explorer 10, Google Chrome 18, Firefox 11 で確認してみました。)
    このため、下記の通り、サーバー側で Web 認証の確認がおこなえます。
public async Task Receive(AspNetWebSocketContext context)
{
  . . .

  if(context.IsAuthenticated)
    . . .

 

次回は、セッションで紹介した WCF のコード (および、構築手順) を記載します。

 

Comments (2)

  1. tyobi says:

    ブログ大変参考にさせていただいております。

    ご紹介のWebsocketのハンドラーで、ping/pongは対応しているのでしょうか?

    イベントハンドラー的には未定義ぽく感じます。またメッセージタイプも特にPING/PONGなそうです。

  2. お世話になります。ASP.NET レベルでは自身でプログラミングする必要があると思いますので、さらに上位レイヤーである SignalR の keepalive などを試してみてください。(すみません、私も実際に使って検証してみたわけではなく。。。)

Skip to main content