WCF WebSockets を使用したブラウザーへの Push 処理

2012/04 追記 : 最新 (.NET 4.5) の WCF を使用した WebSocket 開発については、「WebSocket サーバー開発 : WCF 4.5 編」に記載しました。(本記事は、古い内容です。)

2011/12 追記 : Internet Explorer 10 Platform Preview 3 以降では、WebSocket が実装されています。 (このため、Internet Explorer 10 Platform Preview 3 以降では、下記のクライアント側の prototype は不要です。)

環境 : Visual Studio 2010, Windows Azure SDK 1.3, WCF WebSockets 11.05.10 (WebSockets Prototype spec07), Exchange Web Services Managed API 1.1

こんにちは。

昨日は、ISV 定期セミナーにご参加いただいた皆様、ありがとうございました。(時間オーバーをしてしまい、すみませんでした。)

昨日のセミナーでは内部の実装方法の詳細は説明しませんでしたが、セミナーの中でご紹介した SharePoint Online、Exchange Online、Lync Online を使った開発手法 (基礎) については、私のブログでも (過去に) ポストしていますのでご参照ください。ここでは、デモの中で使用していた実装技術に関するその他の補足事項を記載しておきます。

まず今回は、デモの中で、Exchange Online の予定表とリアルタイムに連携する Azure の予定表アプリケーション (Web アプリケーション) をご紹介しましたが、ここで使用していた WCF WebSocket について補足しておきたいと思います。
昨日のデモでは、予定表の連携をおこなうアプリケーションでしたが、ここ (この投稿) では、サンプル コードを簡単にする目的から、Exchange Online のメールの受信を通知する簡単な JavaScript のアプリケーションを構築し、Azure にホストしてみましょう。

 

WebSocket とは

ご存じの通り、WCF では、既に、WS に準拠した Duplex 通信によるPush  (サーバーからクライアントへの Push) や、Peer Channel を使用したピア・ツー・ピア通信が可能です。また、チャット アプリケーションなどでありがちな探索 (Discovery) も、.NET Framework 4 から使用可能な WCF の WS-Discovery 標準の実装を使うことができます。

しかし、クライアント側 (ブラウザ側) に目を向けると、こうした高度な通信技術を受け取れるクライアントは、.NET アプリケーション (サーバー側の Web アプリケーションを含む) や、Silverlight アプリケーションなど、こうした仕様を実装した限られたクライアントのみという点にも注意してください。

補足 : このため、この投稿では、主に JavaScript を使用した Push 技術を記述しますが、Silverlight だけで構築するなら、Duplex バインディングを使用して同様の仕組みを構築できます。

http://msdn.microsoft.com/en-us/library/cc645027(v=vs.95).aspx

補足 : また、スマートフォンのアプリケーションでは、Windows Phone 7 の Push Notification など、専用の Push 技術が提供されています。(Push Notification を使うと、Windows Phone 7 上のタイルのアイコンなどに通知結果を反映させるなど、スマートフォンらしい Push の実装をおこなうことができます。)

WebSocket は、W3C と IETF で標準化が進められている仕様の 1 つで (W3C では WebSocket API の標準化、IETF では WebSocket プロトコルの標準化を進めています)、ブラウザに対する Push 型の通信を実現できます。(なお、WebSocket では、ブラウザ側からサーバー側に対するメッセージ送信も可能であり、双方向の通信が可能です。) 定期的にポーリング (Pooling) をおこなってイベントを検知するのではなく、単一の long-term の TCP コネクションを使用して、ここで Push イベントを処理します。Push 型なので、厳格なリアルタイム性が実現でき、効率的な通信を実現できます。

つまり、この WebSocket が使えるようになれば、スマートフォンを含む多くのブラウザーで、標準的な手法を使って、双方向のリアルタイム通信を実現することができます。

補足 : TCP を使用するので、企業内で使用する際には、企業で使用しているファイアウォールやプロキシー サーバーの設定に注意してください。

しかし、この仕様は、現在 (2011年06月現在) 進行中であり、まだセキュリティ面などいくつかの課題を抱えているため、ブラウザーによっては実装されていなかったり、WebSocket そのものは実装されていても既定で無効 (オフ) になっているなど、まだブラウザ依存性の高い技術となっているのが現状です。そして、残念ながら IE (最新バージョンの Internet Explorer 9) でも、まだ WebSocket は実装されていません。
このため、twitter の Widget など、広く一般に使用されている通知系のクライアントでは、ブラウザへの互換性に配慮し、タイマーなど (setTimeout 関数など) を使用した実装がおこなわれているのが現状です。

 

WCF WebSocket (Lab) について

Microsoft では、こうしたいくつかの理由で実装されていない HTML 5 の機能について、HTML 5 Lab (ドラフト版) として、実験的に機能を提供しています。(まだ仕様が 2 分されてますが、個人的には、Media Capture も、早く統一的に使えるようになると良いですね …)

< HTML 5 LABS >
http://html5labs.interoperabilitybridges.com

ここで紹介する WCF WebSocket も、こうした Lab の中の 1 つで、現時点の HTML 5 の WebSockets の仕様に準じた WCF サービス のためのライブラリを提供しています。

補足 : なお、この WCF WebSocket は、WebSockets 07 Protocol Specification に準じているようです。(こちら に記載されています。)

また、上記で、「Internet Explorer では、WebSocket は実装されていない」と記載しましたが、この Lab では、Silverlight アプリケーション (Microsoft.ServiceModel.WebSockets.xap) と、それをラッピングした JavaScript ライブラリ (jquery.slws.js の WebSocketDraft オブジェクト) を使うことで、JavaScript から WebSocket と同様のクライアント処理がおこなえるようになっています。つまり、ある意味、クライアント側 (ブラウザ側) は、”WebSocket もどき” な実装になっているわけです。
なお、ブラウザ側は、このように “WebSocket もどきな実装” (WebSocket に似た実装) ですが、Silverlight を使っているため、Firefox など、Silverlight が動くブラウザを使って この WCF WebSocket を使った JavaScript による Push 技術を利用できます。

ちなみに、残念ながら、Firefox 4、Chrome 11 に搭載されている WebSocket から、現在の WCF WebSockets には接続できませんでした。(つまり、iPhone、Android などのスマートフォンからは、残念ながら使用できません。)

将来は、セキュリティ面なども含む仕様詳細も Fix し、これらがすべてつながるようになることを願いたいですね。

 

環境構築

では、早速、環境構築における注意点を記載していきましょう。

まず、開発環境に、以下の WCF WebSockets Prototype をダウンロードしてインストールします。

http://html5labs.interoperabilitybridges.com/prototypes/websockets/websockets/download

開発においては、Interoperability チームのブログ に記載されているように、現在、4502-4534 の範囲のポートのみサポートされているので注意してください。(今回のサンプルでは、下記の通り、ポート番号 4502 を使用します。)

また、上述の通り、企業内のプロキシーやファイアウォールの設定によって、接続がブロックされる場合があるので注意してください。(HTML 5 Lab の チャット アプリケーション がちゃんと動くなら問題ありませんので、試してみてください。)

 

構築手順 1 – WebSocket サービス (Worker Role) の作成

ここでは、Exchange Online のメールの受信を通知する簡単な JavaScript のアプリケーションを構築し、Azure にホストします。画面 (UI) を持ったアプリケーションを Azure の ASP.NET MVC Web Role で実装し、WCF WebSocket のサービスを Azure の Worker Role として実装します。

なお、ここでは、WebSocket の説明を中心に記載するので、以下で使用している Exchange Web Services Managed API の概要については述べません。(Managed API を使用した通知アプリケーションについては、こちら を参照してください。) 今回は、Exchange Server に long-term な接続 (Connection) をおこなって更新イベントをリアルタイムに取得する Streaming Notification と呼ばれる 通知 (Notification) を使用しますが、例えば、データ更新を反映し、また翌日に続きから更新を反映するような、いわゆる 同期 (Synchronization) の仕組みも Managed API で簡単に構築できます。

まず、Windows Azure SDK のインストールされた環境で Visual Studio を開き、[Windows Azure Project] を新規作成します。(この際、ターゲット フレームワークとして、[.NET Framework 4] を選択します。)
表示されるウィザードで、[ASP.NET MVC 2 Web Role] と [Worker Role] を設定します。(それぞれ、適当に名前を変更しておきます。)

まず、WebSocket を処理するサービスを Worker Role を使って実装します。

今回のサンプルでは、WebSocket のサービスをポート番号 4502 番でホストします。(上述したポート番号に関する制約に注意してください。) このため、Azure プロジェクトの Roles フォルダにある Worker Role を右クリックして [プロパティ] を選択し、下図の通り、Tcp の 4502 番の Endpoint を追加します。

Worker Role のプロジェクトに、以下の参照を追加します。

System.ServiceModel.dll
%programfiles%Microsoft SDKsWCF WebSockets11.05.10binMicrosoft.ServiceModel.WebSockets.dll
%programfiles%MicrosoftExchangeWeb Services1.1Microsoft.Exchange.WebServices.dll

Worker Role のプロジェクトをマウスで右クリックして [クラス] を追加します。(今回、ファイル名を MyNotifyService.cs とします。) そして、下記の通り WebSocketsService クラスから継承したサービスのクラスを実装します。
ここでは、WebSocket の開始 (Open) と同時に Exchange Online に接続し、Exchange の Streaming Notification を使って Inbox の新規メールの受信通知を設定します。メールが受信されたら、クライアント (ブラウザー) に Push による通知 (SendMessage) をおこないます。

. . .

using System.ServiceModel;
using Microsoft.ServiceModel.WebSockets;
using Microsoft.Exchange.WebServices.Data;
. . .

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, ConcurrencyMode = ConcurrencyMode.Multiple)]
class MyNotifyService : WebSocketsService
{
    private ExchangeService sv;
    private StreamingSubscriptionConnection subcon;

    public override void OnOpen()
    {
        Trace.WriteLine("MyNotifyService Open called !", "Information");

        // Exchange Online に接続 (今回はデモなので、Address は決めうち !)
        string emailAddress = @"demouser1@o365demos.onmicrosoft.com";
        string password = @"XXXXXXX";
        ExchangeVersion ver = new ExchangeVersion();
        ver = ExchangeVersion.Exchange2010_SP1;
        sv = new ExchangeService(ver, TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time"));
        //sv.TraceEnabled = true; // デバッグ用
        sv.Credentials = new System.Net.NetworkCredential(emailAddress, password);
        // イントラの確認は不要なので、下記のようにしておくと良い (省略)
        //sv.EnableScpLookup = false;
        sv.AutodiscoverUrl(emailAddress, AutodiscoverCallback); // 一度取得した URL は、次回からキャッシュしておくと速い ! (省略)

        // Exchange Streaming Notification の開始 (今回はデモなので、5 分で終わり !)
        StreamingSubscription sub = sv.SubscribeToStreamingNotifications(
            new FolderId[] { new FolderId(WellKnownFolderName.Inbox) }, EventType.NewMail);
        subcon = new StreamingSubscriptionConnection(sv, 5); // only 5 minutes
        subcon.AddSubscription(sub);
        subcon.OnNotificationEvent +=
            new StreamingSubscriptionConnection.NotificationEventDelegate(subcon_OnNotificationEvent);
        subcon.Open();

        base.OnOpen();
    }

    // Inbox のメール アイテムを受信
    private void subcon_OnNotificationEvent(object sender, NotificationEventArgs args)
    {
        foreach (NotificationEvent notifyEvt in args.Events)
        {
            if (notifyEvt.EventType == EventType.NewMail)
            {
                ItemEvent itemEvt = (ItemEvent)notifyEvt;
                EmailMessage msg = EmailMessage.Bind(sv, itemEvt.ItemId);
                // クライアントに Push !!
                // 今回はタイトルだけ送るが、本来は、JsonValue を Parse して送ると良い !
                this.SendMessage(msg.Subject);
            }
        }
    }

    private static bool AutodiscoverCallback(string url)
    {
        // リダイレクトの検証をおこない、OK なら true ! (今回は何もしない)
        return true;
    }

    protected override void OnClose(object sender, EventArgs e)
    {
        // Exchange Streaming Notification の終了
        subcon.Close();

        base.OnClose(sender, e);
    }

    public override void OnMessage(string value)
    {
        // クライアントからメッセージが来た場合 (今回は何もしない)
        base.OnMessage(value);
    }
}

補足 : 今回は、Mail の Subject のみを渡していますが、現実の開発では、Json を使用して値のセットを返すと良いでしょう。(.NET の JsonValue の処理については、こちら に記載しました。なお、ASP.NET MVC アプリケーションの場合は、JsonResult を使うことができます。)

補足 : Azure アプリケーションでは、下記の通り、Time Zone に注意してコードを記述しておいてください。(昨日のデモでお見せした予定表アプリケーションのように、日付と時刻を扱うものについては特に注意してください。)
ExchangeService sv = new ExchangeService(ver, TimeZoneInfo.FindSystemTimeZoneById(“Tokyo Standard Time”));

なお、このサンプル アプリケーションでは、1 つのサービス インスタンスから 1 つのクライアント (ブラウザー) に対して通知をおこなっていますが、チャットのアプリケーションのように複数のセッション間で対話をしたいときは、static な変数などを用意し、サービス インスタンスが作成された際のコンストラクターなどで、毎回、この変数にインスタンス (セッション) を格納しておくと良いでしょう。(そして、この格納されているインスタンスすべてから、クライアントにメッセージを送信します。)

つぎに、Windows Azure の Worker Role で、このサービスを起動するようにします。
WorkerRole.cs を開き、下記の通り、Run メソッドを実装します。(下記の「NotifyService」は、上図の Endpoint の名前にあわせて変更してください。)

. . .

using Microsoft.ServiceModel.WebSockets;

public override void Run()
{
    RoleInstanceEndpoint exEndPoint =
        RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["NotifyService"];
    // 注 : なお、development fabric では、IPEndpoint.Port が fix しないため、4503 に固定 !
    string address = String.Format("ws://{0}/notify", exEndPoint.IPEndpoint);
    //string address = String.Format("ws://{0}:4503/notify", exEndPoint.IPEndpoint.Address);
    WebSocketsHost<MyNotifyService> sh =
        new WebSocketsHost<MyNotifyService>(new Uri(address));
    sh.AddWebSocketsEndpoint();
    sh.Open();
    . . .

さいごに、通常の WebSocket では不要ですが、今回は、上述の通り、内部で Silverlight の “WebSocket もどき” なクライアントを使用するため、Silverlight クライアントからクロス ドメインで接続ができるように、下記の clientaccesspolicy.xml を作成してサーバー側に配置します。

<?xml version="1.0" encoding="utf-8" ?>
<access-policy>
  <cross-domain-access>
    <policy>
      <allow-from http-request-headers="*">
        <domain uri="*" />
      </allow-from>
      <grant-to>
        <resource path="/" include-subpaths="true" />
        <socket-resource port="4502-4530" protocol="tcp" />
      </grant-to>
    </policy>
  </cross-domain-access>
</access-policy>

なお、配置場所に注意してください。
今回のようなネイティブの tcp でホストされているサービスの場合は、クライアントは、そのマシンの http (ポート 80) のルートのポリシー (clientaccesspolicy.xml) を参照します。(例えば、ローカルでプログラムを起動する場合は、http://localhost/clientaccesspolicy.xml として配置します。)
Windows Azure では、単一の Web Role を作成した場合は、既定で、その Web Role に対して HTTP のポート 80 のルートが割り当てられるので、今回の場合、同時に作成した Web Role のプロジェクト (上記の ASP.NET MVC 2 Web Role のプロジェクト) のルートに clientaccesspolicy.xml を配置します。(なお、Windows Azure のプロジェクトでは、.csdef ファイルで、サイトの protocol、port などを変更できます。)

補足 : つまり、今回はクライアント側 (Web Role 側) のプロジェクトにポリシーを配置しますが、これは、サーバー側 (Worker Role 側) のポリシーを制御しているという点に注意してください。(環境に応じて、配置場所に注意してください。)

 

構築手順 2 – クライアント (Web Role) の作成

つぎに、ASP.NET MVC の Web アプリケーションを作成します。今回はサンプルなので、簡単な JavaScript を埋め込む程度の実装で終了します。(実際の開発では、画面遷移や Json を返す ActionResult など、機能に応じた MVC の設計をおこなってください。)

まず、jQuery を使用するため、Site.Master を開き、<head></head> に下記 (太字) を追加します。

<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>

<!DOCTYPE html PUBLIC ...>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>
    <link href="../../Content/Site.css" rel="stylesheet" type="text/css" />
    http://ajax.microsoft.com/ajax/jQuery/jquery-1.4.2.min.js
</head>
...

今回は、上述した通り Silverlight を使った “WebSocket もどき” なクライアントなので、ちょっと面倒ですが、以下の準備をしておきましょう。(無論、一般の WebSocket の開発では、こうした設定はすべて不要です。)

まず、プロジェクトをマウスで右クリックして、[追加] – [新しいフォルダ] で「ClientBin」フォルダを作成します。この作成したフォルダ (ClientBin) をマウスで右クリックし、[追加] – [既存の項目] を選択して、以下を追加します。

%programfiles%Microsoft SDKsWCF WebSockets11.05.10websubscriptionClientBinMicrosoft.ServiceModel.WebSockets.xap

つぎに、Scripts フォルダに、%programfiles%Microsoft SDKsWCF WebSockets11.05.10websubscriptionjs にある 4 つの js ファイル (h5utils.js, jquery.slws.js, json2.js, Silverlight.js) を追加します。

Site.Master を開き、<head></head> に、これら 4 つの js ファイル (h5utils.js, jquery.slws.js, json2.js, Silverlight.js) の参照を追加します。

. . .

<head runat="server">
    . . .

    http://ajax.microsoft.com/ajax/jQuery/jquery-1.4.2.min.js
    http://../../Scripts/h5utils.js
    http://../../Scripts/jquery.slws.js
    http://../../Scripts/json2.js
    http://../../Scripts/Silverlight.js
</head>
. . .

以上で、”WebSocket もどき” を使う準備が完了しました。

では、この “WebSocket もどき” (WebSocket のドラフト版) を使って、処理を作成しましょう。

今回は、ASP.NET MVC で最初に表示される Index.aspx のビューの表示の際に、下記の通り、WebSocket (ドラフト版の WebSocketDraft オブジェクト) を開始します。そして、メッセージを 1 回受信したら、その内容をアラートで表示します。(MVC を使っている意味がまったくありませんが、サンプルなので我慢してください。)
なお、下記で、URL は、Azure の DNS にあわせて変更しておいてください。

...


    var socket;

    function openConnection() {
        // (なお、development fabric では、Debug 用に 4503 を使用)
        socket = new WebSocketDraft('ws://tsmatsuz1.cloudapp.net:4502/notify');
        //socket = new WebSocketDraft('ws://localhost:4503/notify');
        socket.onerror = function (evt) {
            alert('errored : ' + evt.data);
        };
        socket.onopen = function (evt) {
            alert('opened');
        };
        socket.onclose = function (evt) {
            alert('closed');
        };
        socket.onmessage = function (evt) {
            alert('received : ' + evt.data);
        };
    }

    $(document).ready(function () {
        $.slws.ready(function () {
            openConnection();
        });
    })

...

ここでは、サーバーからクライアントへの一方向の通知のサンプルですが、下記の通り、クライアントからサーバーにメッセージの send をおこなうこともできます。(例えば今回のサンプル アプリケーションの場合、Exchange Online のメール アドレス、パスワードなどの情報をサーバーに渡すことができます。)

conn.send(jsonMessage);

 

配置と動作確認

では、配置してみましょう。

Visual Studio でクラウド プロジェクトをマウスで右クリックして、[発行] を選択します。今回は、サービス パッケージを作成して、手動で Azure にアップロードします。このため、下記のダイアログ ボックスで、[Create Service Package Only] を選択して、[OK] ボタンを押します。

これにより、プロジェクトの binDebugPublish フォルダーにパッケージ ファイル (.cspkg) と構成ファイル (.cscfg) が保存されるので、これを以下の手順で Windows Azure にアップロードして開始します。

Windows Azure の管理ポータル にログインし、[New Production Deployment] をクリックして、上記で作成した .cspkg ファイルと .cscfg ファイルを配置します。(ここでは、いきなり、Production に入れてしまいましょう …)

補足 : あらかじめ、Windows Azure のサブスクリプション契約と、Hosted Services の新規作成などをおこなっておいてください。(ここでは、その説明は省略しています。)

補足 : パッケージの配置の際、インスタンス数が既定の 1 のままだと、高可用性が保障できないという旨の警告 (Warning) が表示されます。(今回は、これは無視して先に進みます。)

ステータスが Ready になったら、ブラウザーで、配置した Windows Azure のサイト (Web Role) に接続してみましょう。

下図の通り、Exchange Online のメールを受信する Azure アプリケーションが完成しました !

 

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