WebSocket サーバー開発 : WCF 4.5 編


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

WebSocket サーバー開発

こんにちは。

今日は、WCF を使った WebSocket サーバー開発のコードを記載します。

前回 は、サンプルとして Web ブラウザーを使用しましたが、WebSocket の利用シーンは Web ブラウザーのみとは限りません。例えば、WebSocket の利用環境が整備された企業では、インターネット上の資源 (リソース) を使った双方向連携を企業内のクライアント アプリケーション (企業内システム) で使用することも考えられます。
これから見て行くように、WCF 4.5 (.NET Framework 4.5 の WCF) を使うと、WebSocket をベースの通信基盤として、さらに抽象度の高いサービス / クライアントのアプリケーションが構築できます。

すみません、セッションでは時間がなかったため、クライアント作成のデモを省略しましたので、以下ではこの手順も細かく (ちゃんと) 記載しておきます。

 

Server Programming

では、さっそく、サーバー側を構築してみましょう。これから構築するサンプル アプリケーションは、前回同様、カードの種類 (Spades, Hearts, Clubs, Diamonds) と金額 (Bets) を送信すると、参加しているすべてのプレイヤーにその情報 (誰が、どこに、いくら賭けたか ?) が通知されるという簡単なゲームのアプリケーションを構築します。(ゲームになってませんけど。。。)

まず、ASP.NET Web アプリケーション (もちろん、MVC でも OK) のプロジェクトを新規作成し、[追加] – [新しい項目] で [WCF サービス] を追加します。(作成した WCF サービスを、WebsocketService1.svc とします。)

前回同様、Web.config に下記の 1 行 (太字) を追加します。(2012/06/04 追記)

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

</configuration>

サービスを以下の通り実装します。

WebsocketService1.svc.cs

. . .

[ServiceContract(CallbackContract =
  typeof(IWebsocketServiceCallback1))]
public interface IWebsocketService1
{
    [OperationContract(IsOneWay = true)]
    void StartBet(string nickname);
    [OperationContract(IsOneWay = true)]
    void CommitBet(string target, int money);
}

[ServiceContract]
public interface IWebsocketServiceCallback1
{
    [OperationContract(IsOneWay = true)]
    void SendBetInfo(string nickname, string target, int money);
}

// Default InstanceContextMode is PerSession.
// (uses WebSocket session. . .)
public class WebsocketService1 : IWebsocketService1, IDisposable
{
    private static List<IWebsocketServiceCallback1> callbackServices =
        new List<IWebsocketServiceCallback1>();

    private IWebsocketServiceCallback1 callback;
    private string NickName;

    public void StartBet(string nickname)
    {
        this.NickName = nickname;

        callback =
          OperationContext.Current.GetCallbackChannel<IWebsocketServiceCallback1>();
        callbackServices.Add(callback);
    }

    public void CommitBet(string target, int money)
    {
        Broadcast(this.NickName, target, money);
    }

    public void Dispose()
    {
        callbackServices.Remove(callback);
    }

    public static void Broadcast(string nickname, string target, int money)
    {
        foreach (var svc in callbackServices)
            svc.SendBetInfo(nickname, target, money);
    }
}
. . .

Web.config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  . . .

  <system.serviceModel>
    <services>
      <service name="ws_server1.WebsocketService1"
               behaviorConfiguration="WebsocketService1Behavior">
        <endpoint address=""
                  binding="netHttpBinding"
                  bindingConfiguration="WebsocketService1Binding"
                  contract="ws_server1.IWebsocketService1" />
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="WebsocketService1Behavior">
          <serviceMetadata httpGetEnabled="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <bindings>
      <netHttpBinding>
       <binding name="WebsocketService1Binding">
         <webSocketSettings transportUsage="Always" />
       </binding>
      </netHttpBinding>
    </bindings>
  </system.serviceModel>
</configuration>

上記の Web.config に注目してください。WebSocket のプロトコルを使用して通信する場合には、netHttpBinding バインディングを使用します。(セッションで説明したように、一定の条件が揃うと、フレームワークの内部で WebSocket が使用されます。ここでは、上記の webSocketSettings に関する説明は省略します。)

また、上記のインターフェイス (IWebsocketService1、IWebsocketServiceCallback1) の定義のように、WebSocket 通信では、Oneway の Duplex サービスとして定義しておきます。(Request-Reply 型ではありません。)

なお、WCF を使用した場合、開始時の WebSocket のネゴシエーション (Negotiation) は内部でおこなわれるため、開始時のパラメーター設定などは、上記のようにメソッド (StartBet メソッド) として別途 初期化用のメソッドを用意しておきます。(こうした点は、前回 の ASP.NET 4.5 の場合と異なる点です。)

補足 : なお、WCF サービスでも、サービスのコード (ロジック) の中から HTTP (URI 文字列) の Query String を取得することは可能です。この場合は、下記のとおり、createNotificationOnConnection プロパティを true に設定しておきます。(ここでは、詳細の説明は省略します。)

. . .
<netHttpBinding>
  <binding name="WebsocketService1Binding">
    <webSocketSettings transportUsage="Always"
      subProtocol=". . ."
      createNotificationOnConnection="true" />
  </binding>
</netHttpBinding>
. . .

補足 : セッションで述べたように、WCF では自己ホスト (self-host) も可能です。自己ホストの場合、IIS 8 は使用されず、内部で HttpListener の WebSocket の処理が使用されます。

補足 : セッションで説明した Windows Azure 対応 (idle connection 切断への対応) をおこなう場合は、WCF では、keepAliveInterval 属性が使用できます。(内部で使用している HttpListenerContext.AcceptWebSocketAsync メソッドでも keepAliveInterval が使用できます。)

なお、前回 同様、このサービスは、IIS 8 にホストして実行してください。(その他の留意事項も、前回同様です。)

 

Client Programming (.NET Client)

つぎに、上記のサービスと連携して動作するクライアント アプリケーションを作成してみましょう。今回は、前回 と異なり、コンソール アプリケーションを構築します。

Visual Studio で [コンソール アプリケーション] のプロジェクトを新規作成し、作成されたプロジェクトで、[サービス参照の追加] (Add Service Reference) をおこなって上記のサービスのアドレスを入力します。(これにより、上記のサービスと接続するためのプロキシのコードが自動生成されます。)
すると、クライアント側の構成ファイル (App.config) では、下記の通り、ws://… のアドレスの WebSocket を使った通信が構成されているのがわかります。

<configuration>
  . . .

  <system.serviceModel>
    <bindings>
      <netHttpBinding>
        <binding name="NetHttpBinding_IWebsocketService1">
          <webSocketSettings transportUsage="Always" />
        </binding>
      </netHttpBinding>
    </bindings>
    <client>
      <endpoint
        address="ws://kkdeveva22/ws_server1/WebsocketService1.svc"
        binding="netHttpBinding"
        bindingConfiguration="NetHttpBinding_IWebsocketService1"
        contract="ServiceReference1.IWebsocketService1"
        name="NetHttpBinding_IWebsocketService1" />
    </client>
  </system.serviceModel>
</configuration>

以降のクライアント プログラムの構築 (開発) 方法は、いつもの WCF 開発と同じです。ただし、Duplex 通信のため、下記の通りコールバック関数 (サーバー側から呼び出される関数) を設定しておいてください。
下記のアプリケーションでは、「q」が入力されるまで、spades に 200 $ 賭け続けます。また、他の Player (自分も含む) が賭けると、その内容が通知されてコンソールに表示されます。

. . .

static void Main(string[] args)
{
    Console.WriteLine("Please input your nickname ! (and Enter)");
    string nickname = Console.ReadLine();

    var context = new System.ServiceModel.InstanceContext(
      new WebsocketService1Callback());
    ServiceReference1.WebsocketService1Client cl =
      new ServiceReference1.WebsocketService1Client(context);
    cl.StartBet(nickname);

    while (true)
    {
        if (Console.ReadLine() == "q")
            break;
        cl.CommitBet("spades", 200);
        System.Threading.Thread.Sleep(3000);
    }
}

private class WebsocketService1Callback
  : ServiceReference1.IWebsocketService1Callback
{
    public void SendBetInfo(string nickname, string target, int money)
    {
        Console.WriteLine("{0} bets {2} $ to {1}.",
          nickname, target, money);
    }
}
. . .

実行結果は、下図のようになります。(下図では、2 つのコンソール アプリケーションを起動しています。)

WCF の場合、既定で、サービスの InstanceContextMode は PerSession (セッションごと) ですが、上記の場合、内部で HTTP WebSocket のセッションが使用されます。つまり、上記の ServiceReference1.WebsocketService1Client インスタンスを使用している間、このセッションが維持されます。

また、内部では、下記のような SOAP メッセージ (Binary) が WebSocket を使って受け渡されています。(WCF に詳しい方はおわかりと思いますが、「SOAP」と表現するよりは、WCF の Message をシリアライズした内容と言ったほうが良いかもしれません。。。)

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
  xmlns:a="http://www.w3.org/2005/08/addressing">
  <s:Header>
    <a:Action s:mustUnderstand="1">http://tempuri.org/IWebsocketService1/CommitBet</a:Action>
    <a:To s:mustUnderstand="1">http://kkdeveva22/ws_server1/WebsocketService1.svc</a:To>
  </s:Header>
  <s:Body>
    <CommitBet xmlns="http://tempuri.org/">
      <target>spades</target>
      <money>200</money>
    </CommitBet>
  </s:Body>
</s:Envelope>

補足 : なお、ここでは詳細の説明を省略しますが、通常、サービス / クライアント間で受け渡しをおこなう独自のプロトコルを処理する場合は、下記ように、サービス / クライアントの双方の .config に subProtocol 属性を設定します。(上記の WCF の場合、既定では、内部で「soap」のプロトコル名が使用されています。) これにより、WebSocket の Negotiation の際にお互いでサポートするプロトコルの確認をおこないます。

<netHttpBinding>
  <binding name="WebsocketService1Binding">
    <webSocketSettings transportUsage="Always"
      subProtocol="soapoverwebsocket" />
  </binding>
</netHttpBinding>

また、Web ブラウザー (JavaScript) からプロトコルを指定する際は、下記の通り記述します。

websocket = new WebSocket('ws://...', "soapoverwebsocket");

セッションでも説明したように、WCF を使った WebSocket プログラミングとは、上記のような性質のものです。(ここでは、コンセプトの説明は省略します。。。)

 

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

2012/06/04 追記 : 下記ですが、Visual Studio 2012 RC (.NET Framework 4.5 RC) では、現在の Microsoft.WebSockets (<= 0.2.2) パッケージで動作しませんので、もう少々お待ちください . . . (新しいバージョンのパッケージが出てきたら更新します。)

当然ですが、クライアントが Web ブラウザーの場合、上記のような面倒なサービスと連携はしたくないでしょう。(私なら嫌です。)

そこで、WCF で作成した WebSocket サービスと Web ブラウザーを連携させたい場合 (WCF の自己ホストなど、WCF の性質を活用したい場合など) は、前回 も紹介した NuGet の Microsoft.WebScokets パッケージを使用します。

補足 : 以降で説明しますが、この方法で実装すると、上述とはまったく異なるバインディングが使用されます。(つまり、「WCF である」という点を除き、それぞれ別の仕組みが使われています。)

まずは、前回 記載した方法で、Microsoft.WebScokets パッケージをプロジェクト (サーバー側のプロジェクト) にインストールしておきます。

プロジェクトに [WCF サービス] を追加します。(今回は、WebsocketService2.svc とします。) そして、下記の通り、Microsoft.WebScokets パッケージ (Microsoft.WebSockets.dll) の Microsoft.ServiceModel.WebSockets.WebSocketService を継承した WCF サービスを実装します。

. . .
using Microsoft.ServiceModel.WebSockets;

public class WebsocketService2 : WebSocketService
{
  we'll implement here, later ...
}

この Microsoft.ServiceModel.WebSockets.WebSocketService で提供している override メソッドは、前回の ASP.NET の場合と同様です。(OnOpen, OnClose, OnError, OnMessage を実装します。) 違いは、今回の場合、WCF のフレームワークが使用されるという点のみです。
前回 同様、下記の通り、実装します。

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

public class WebsocketService2 : WebSocketService
{
    private static List<WebSocketService> callbackServices =
        new List<WebSocketService>();

    private string NickName;

    public override void OnOpen()
    {
        this.NickName = this.QueryParameters["nickname"];

        callbackServices.Add(this);
    }

    protected override void OnClose()
    {
        callbackServices.Remove(this);
    }

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

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

    public static void Broadcast(string jsonString)
    {
        foreach (var svc in callbackServices)
            svc.Send(jsonString);
    }
}

また、上記 (Microsoft.ServiceModel.WebSockets.WebSocketService) を使用した場合、Microsoft.ServiceModel.WebSockets.WebSocketHost を使ってホストする必要があるため、WebsocketService2.svc のマークアップ コードに、下記の通り記述します。(この WebSocketHost が必要な構成をすべておこなってくれるため、Web.config 上に構成情報を記述する必要はありません。)

<%@ ServiceHost Language="C#"
     Debug="true"
     Service="ws_server1.WebsocketService2"
     CodeBehind="WebsocketService2.svc.cs"
     Factory="ws_server1.WebSocketHostFactory"%>

using System;
using System.ServiceModel;
using System.ServiceModel.Activation;
using Microsoft.ServiceModel.WebSockets;

namespace ws_server1
{
    public class WebSocketHostFactory : ServiceHostFactory
    {
        protected override ServiceHost CreateServiceHost(
          Type serviceType, Uri[] baseAddresses)
        {
            var host = new WebSocketHost(serviceType, baseAddresses);
            host.AddWebSocketEndpoint();
            return host;
        }
    }
}

上記のサービスへは、前回とまったく同じ方法で、ブラウザーから簡単に接続できます。下記は、前回 構築したクライアント (HTML / JavaScript) とまったく同じコードです。(ASP.NET MVC と Razor を使った Web アプリケーションです。)

_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/WebsocketService2.svc?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>
}

実行結果は、前回 同様です。(ブラウザーを複数立ち上げて接続し、[Bets] ボタンを押すと、すべてのブラウザーに alert が表示されます。)

 

このように、WCF を使用した場合には抽象度が高くなりますが、プリミティブな WebSocket 通信には少し手間が必要です。一方、Microsoft.WebScokets パッケージを使用すると、よりプリミティブな WebSocket を WCF のメカニズムと組み合わせて実装できます。

次回は、セッションの最後に紹介した SignalR のコードを記載します。

 

 

Comments (0)

Skip to main content