WebSocket サーバー開発 : ASP.NET SignalR とクロス ブラウザーへの対応

(2013/03 : ASP.NET SignalR 1.0 にあわせてコード等を変更)
(2012/06 : RC 版にあわせて、補足事項を追記)

WebSocket サーバー開発

こんにちは。

今回は、ASP.NET SignalR のサンプル コードと作成手順を記載しておきます。

ASP.NET SignalR は、このあと見て行くように、単なる WebSocket 開発のためだけのライブラリーではなく、さらに多くの付加価値を含んだ上位のフレームワークです。抽象度も高く、日本語のエンコードなど含めプリミティブな処理を開発者が記述する必要はありません。(それでいて、IoC 的なフレームワークを持っているため、細かな動作のカスタマイズが可能です。)

前回まで WebSocket を使ったプログラミングを見てきましたが、WebSocket における現実の課題の 1 つとして、クロス ブラウザー対応 (cross-browser) の問題があります。
HTML 5 に対応した最近のブラウザー環境であれば問題ありませんが、現実には、WebSocket を実装していない以前のブラウザー (old browser) でも主要機能だけは使わせたいといったニーズはあるでしょう。セッションで説明した通り、WebSocket については、最近まで RFC 段階だった仕様で、ブラウザーによっては、セキュリティ上の理由などから、つい最近まで使えなくなっていた (既定がオフになっていた) ものもあります。また、セッションで説明したように、ブラウザーが対応していても、環境によって使用できないケースも考えられます。
こうした場合、WebSocket が使えないブラウザーのために、定期的な polling (setTimeout など) や long polling などの代替策を実装したコードを用意しておく必要がありますが、そんな面倒なコードを構築したくはないでしょう。(WebSocket オブジェクトだけでなく、MozWebSocket オブジェクトへの対応など、考え出すときりがありません。。。)

こうしたクロス ブラウザー (cross-browser) に対応する方法の 1 つとして、今回紹介するオープン ソースの ASP.NET SignalR が使えます。(NuGet、GitHub で取得できます。)

セッションで説明したように、ASP.NET SignalR では WebSocket が利用できる場合には、双方向通信の手段として WebSocket を使用し、それ以外の場合には、long polling など使用可能な他の方式で双方向通信と同等の処理をおこないます。(このため、基本的に、クロス ドメインでの接続はできません。) 第 1 回 と同様、WebSocket transport を使用する場合は、サーバー側は、Windows Server 2012 以上、IIS 8 以上 (IIS のコンポーネントとして [WebSocket Protocol] が必要)、.NET Framework 4.5 以上 (ApplicationPool の .NET バージョン 4.0 以上) が必要です。

補足 : SignalR 0.4 以降 (SignalR.JS 0.4 含む) では、long polling、WebSocket 以外に、HTML 5 の Server-Sent Events (SSE, および JavaScript 側の EventSource オブジェクト) の transport にも対応しています。(ここでは、long polling、WebSocket 以外の transport の説明は省略します。)

 

ASP.NET SignalR のプログラミング

まず、ASP.NET SignalR を使った一般的なプログラミングと、その動きを見てみましょう。
今回も、これまでと同様、カードの種類 (Spades, Hearts, Clubs, Diamonds) と金額を送信すると、参加しているすべてのプレイヤーに通知される簡単なゲームをブラウザー アプリケーションとして構築します。(だから、ゲームになってませんけど。。。)

まず、Visual Studio を開いて ASP.NET Web Application の [Single Page Application] プロジェクトを新規作成し、NuGet を使って、このプロジェクトに、SignalR をインストールしておきます。(依存するパッケージとして、Microsoft.AspNet.SignalR.Core、Microsoft.AspNet.SignalR.JS、Microsoft.AspNet.SignalR.Owin、Microsoft.AspNet.SignalR.SystemWeb と、さらに、これらが使用している jQuery、Microsoft.Owin.Host.SystemWeb のパッケージもインストールされます。)

まずは、サーバー側の処理を実装します。

ソリューション エクスプローラーで、プロジェクトを右クリックして、[追加] – [新しい項目] メニューを選択して [SignalR Hub クラス] を追加します。(今回、このファイルを BetsHub.cs とします。)
ここに、以下の通り、コードを実装します。

. . .
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
. . .

[HubName("betsgame")]
public class BetsHub : Hub
{
  public void CommitBet(string nickname, string target, int money)
  {
    this.Clients.All.notifybet(nickname, target, money);
  }
}
. . .

ASP.NET SignalR のサーバー側は、上記の通り、Microsoft.AspNet.SignalR.Hub を継承して構築します。
上記の this.Clients.All は、このサーバーに接続しているすべてのクライアントを取得しています。notifybet メソッドは、以降で構築するクライアント側 (JavaScript) の function です。(this.Clients.All は dynamic クラスです。)
つまり、上記の CommitBet メソッドでは、接続中のすべてのクライアント (JavaScript) の notifybet 関数をサーバー側から呼び出しています。(前述の通り、条件に応じて、long pooling や WebSocket が使用されます。)

もし、呼び出し元のクライアントだけに通知する場合は、下記の通り Caller を使用します。

public class BetsHub : Hub
{
    . . .
    public void Testcall(string nickname, string target, int money)
    {
        this.Clients.Caller.notifybet(nickname, target, money);
    }
}

また、特定のクライアントに通知する場合は、下記の通り、Connection Id が使用できます。(下記のサンプル・コードでは、呼び出し元に通知しているため、Caller を使用できますが。。。)

// client side
betsgame.server.testcall($.connection.hub.id, 'matsu', 'spades', 100);
// server side
[HubName("betsgame")]
public class BetsHub : Hub
{
    . . .
    public void Testcall(string conId, string nickname, string target, int money)
    {
        // use client-side property, or use Context.ConnectionId
        this.Clients.Client(conId).notifybet(nickname, target, money);
    }
}

また、グループ化をおこない、特定のグループのみに通知することもできます。詳細は、Hubs (github) を参考してください。

なお、これまでの WebSocket 開発と同様、下図の通り、IIS 8 にホストしてデバッグするように、プロジェクトを設定しておきましょう。(上述の通り、ASP.NET SignalR を使うだけなら IIS 7.5 以下でも充分ですが、WebSocket を使用するので . . .)

つぎに、JavaScript 側 (クライアント側) を実装します。
今回は、MVC のコントローラーとビューを追加し、ビュー (Index.cshtml など) に、下記の通りコードを記述します。

@{
  Layout = null;
}

<!DOCTYPE html>

<html>
<head>
  <meta name="viewport" content="width=device-width" />
  <title>Index</title>
  http://~/Scripts/jquery-1.6.4.min.js
  http://~/Scripts/jquery.signalR-1.0.1.min.js
  http://~/signalr/hubs
  
  $(document).ready(function () {
    var nickname = prompt('What is your nickname ?');

    // setup hub client (signalR)
    var betsgame = $.connection.betsgame;
    betsgame.client.notifybet = function (nickname, target, money) {
      alert(nickname + ' bets '
        + money + '$ to ' + target + '.');
    };

    // connect to hub (signalR)
    $.connection.hub.start()
      .done(function () {
        $('#sendbutton').click(function () {
          betsgame.server.commitBet(nickname,
            $('#target').val(),
            $('#bets').val());
        });
      });
  });
  
</head>
<body>
  <h3>SignalR 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>

</body>
</html>

上記で、~signalr/hubs のライブラリー (.js ファイル) は、ASP.NET によって動的に生成されます。そして、この動的に生成されるライブラリー (.js ファイル) には、さきほどサーバー側 (C#) で定義した betsgame、commitBet などが JavaScript で定義されています。(プログラミングの際は、.js ファイルの実体は存在しません。)
さいごに、このライブラリー (~signalr/hubs) の動的作成をおこなうために、Global.asax.cs の Application_Start の先頭に下記を記述してください。

public partial class Startup
{
  public void Configuration(IAppBuilder app)
  {
    ...
    app.MapSignalR();
  }
}

上記の JavaScript のコードでは、サーバー側から callback される notifybet 関数の作成もおこなっています。(C# 側から、この関数が call されます。)

$.connection.hub.start() によって hub (すなわち、SignalR のサーバー) に接続し、接続が完了すると、この start の第 2 引数に指定している function が呼ばれます。(そして、このタイミングで、ボタンの Click イベントを登録しています。)

このアプリケーションの動作は、これまでのサンプル アプリケーションと同様、[Bets !] ボタンを押すと、ゲームに参加している参加者全員にその内容が通知されます。(下図)

 

.NET Client からの接続 (外部プログラムからの呼び出し)

ほとんどの場合、クライアントは JavaScript ですが、.NET Client (C# など) から接続する場合は、Microsoft.AspNet.SignalR.Client の NuGet Package をインストールして、下記の通り実装します。(下記のサンプルは Server 側のメソッド呼び出しですが、Client 側の受信も可能です。)
なお、SignalR を外部から呼び出す際の既定の URL は下記の通り /signalr ですが、これは上述の MapSignalR で変更できます。

using Microsoft.AspNet.SignalR.Client;

var hubConnection = new HubConnection("http://app.example.com/signalr");
IHubProxy hubProxy = hubConnection.CreateHubProxy("betsgame");
await hubConnection.Start();
await hubProxy.Invoke("CommitBet", "John", "Hearts", 200);

この方式を使って、例えば、ASP.NET Web Api などの外部プログラムから呼び出すことで、SingalR の Server / Client 以外のプログラムから通知をおこなうこと (Server initiated な呼び出し) が可能です。

 

繰り返しになりますが、このアプリケーションでは、WebSocket が使用できる条件のときには WebSocket が使用され、それ以外には、long polling などの代替の方法が使用されます。(Fiddler などで確認してみてください。)
例えば、long pooling では、通知を待機している関数ごと (上記の場合、notifybet の 1 箇所のみです) に、長時間の ajax 呼び出し (XMLHttpRequest による呼び出し) がおこなわれ、サーバー側から関数が呼ばれると、その HTTP 要求が応答を返します。(ajax 呼び出しが終了します。) そして、再度、次の Ajax 呼び出しと待機がおこなわれます。
また、環境やネットワーク トラブルなどの問題で WebSocket による接続できない場合などでも、必要に応じ、long polling などが使用されます。さらに、WebSocket をサポートしていないブラウザー (old browser など) と、WebSocket をサポートするブラウザーの双方が混在している場合も、相互でメッセージのやりとりが可能です。(例えば、IE 9 と IE 10 でつないだ場合、前者は long polling が使われ、後者は WebSocket が使われますが、双方でメッセージの交換が可能です。)

補足 : ASP.NET SignalR では、内部で JavaScript の JSON Parser (JSON.Parse) を使用しているため、IE の互換表示モード (以前の IE のモード) では動作しないので注意してください。(IE 7 以下の場合は、http://cdnjs.cloudflare.com/ajax/libs/json2/20110223/json2.js のスクリプト参照を追加してください。Native JSON については、こちら を参照してください。)

補足 : SignalR では、timeout における KeepAlive が構成できます。Windows Azure における idle connection への対策などが可能です。

補足 : なお、上記のコードの通り、ASP.NET SignalR では、クライアント間の情報共有の仕組みが ASP.NET SignalR 自体に組み込まれているため (上記の this.Clients.All)、Scale Out への対処方法について気になる方も多いでしょう。ASP.NET SignalR では、SignalR.Redis パッケージ、SIgnalR.WindowsAzureServiceBus パッケージを使用することで、Publish / Subscribe パターンによる Scaling が可能です。これについては、「SignalR の Scale Out とロード バランサー対応」 に掲載しました。(2012/06/11 記述変更)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s