かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

SignalRの.NET Client

ASP.NET SignalRが利用者は特に何も意識することなくリアルタイムにサーバーサイドからの通知を受け取ったりできる素敵な子だということは周知の事実です。

ASP.NET MVCと組み合わせて使ったり…

もちろん、JavaScriptから呼び出してリアルタイムに画面が更新されるWebアプリも…

こんな素敵な技術を、Webアプリだけで使うなんてもったいない!!ということで、クライアントライブラリはJavaScript以外にも、.NET用にも提供されています。そう、コンソールアプリケーションやWPFアプリケーションやWinFormアプリケーションやWindows ストア アプリやSilverlightや、日本で未発売のWindows Phone 8とかでも使えます。
MSのクライアントプラットフォーム大集合といった感じですね。

今回は、.NET用のクライアントの簡単な使い方を紹介します。

サーバーサイドの開発

とりあえず、ASP.NETアプリケーションを作りましょう。そして、NuGetからMicrosoft.AspNET.SignalRを追加します。そして、Global.asaxにSignalR用のルートの設定を追加します。

protected void Application_Start(object sender, EventArgs e)
{
    RouteTable.Routes.MapHubs();
}

あとは、Hubクラスを作って接続してる人みんなにメッセージをばらまく処理を書きます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.AspNet.SignalR;

namespace HelloSignalR
{
    public class HelloHub : Hub
    {
        public void Hello(string name)
        {
            Clients.All.HelloWorld(name + " > Hello world!!");
        }
    }
}

Clients.Allはdyanamic型なので好きなメソッドを生やすことが出来ます。ここのメソッド名がクライアントサイドで受け取るときのイベント名になります。サーバーサイドはおしまい!ここらへんの詳細については、巷のWebの記事を参考にしたほうがいいと思います。

クライアントサイドの開発

ということで、クライアントサイドです。適当にコンソールアプリケーションを作ってNuGetからMicrosoft.AspNet.SignalR.Clientを参照に追加します。使い方は簡単。HubConnectionを作成したあとHub名を指定してIHubProxyを作る。Onメソッドでサーバーからのイベント受信をする。Invokeメソッドでサーバーに処理を投げつける。

取りあえず、忠実にHelloHubにメッセージなげつけて受信するだけのプログラムを書きました。

namespace HelloSignalR.Client
{
    using Microsoft.AspNet.SignalR.Client.Hubs;
    using System;

    class Program
    {
        static void Main(string[] args)
        {
            // Hubのあるサイトへの接続
            var conn = new HubConnection("http://localhost:10551/");
            // HubConnectionをStartする前にHelloHubクラスに対するproxyを作っておく
            var proxy = conn.CreateHubProxy("helloHub");
            // サーバーからHelloWorldというイベントが来たら実行する処理を登録
            proxy.On<string>("HelloWorld", Console.WriteLine);

            // 接続開始
            conn.Start().Wait();

            // 10回ほどメッセージを送ってみる
            for (int i = 0; i < 10; i++)
            {
                proxy.Invoke("Hello", "tanaka-" + i).Wait();
            }

            // 待ち
            Console.ReadKey();
            // 終了
            conn.Stop();
        }
    }
}

結構簡単ですね。ちなみに、InvokeやOnとか文字列でイベント名を指定してるのがけしからんですよね。ということで、INotifyPropertyChangedの実装でも同じものCallerMemberNameAttributeを使って楽できるようにしてみます。

public static class HubProxyExtensions
{
    public static IDisposable On<T>(this IHubProxy self, Action<T> onData, [CallerMemberName] string eventName = null)
    {
        return self.On<T>(eventName.Substring("On".Length), onData);
    }

    public static Task Invoke(this IHubProxy self, [CallerMemberName] string method = null, params object[] args)
    {
        return self.Invoke(method, args);
    }
}

OnとかInvokeは、他にジェネリック引数の数が違うオーバーライドとかがあるので、そういうのも用意してあげるとよさそう。こういうのを用意しておくとHelloHubを使いやすくするためのクラスを書きやすくなります。

例えば以下のような感じに。

// IHubProxyをタイプセーフに使うために用意するであろうクラス
class HelloHub
{
    private IHubProxy proxy;
    public HelloHub(IHubProxy proxy)
    {
        this.proxy = proxy;
    }

    public IDisposable OnHelloWorld(Action<string> onData)
    {
        return this.proxy.On(onData);
    }

    public Task Hello(string name)
    {
        // ここが気に入らない…複数引数があるときはargs: new object[] { a, b, c }になってしまう...
        return this.proxy.Invoke(args: name);
    }
}

サーバーサイドのメソッド名やイベントの名前はCallerMemberNameから取得してるのでちゃんとクラスさえ定義してあげたら、それを使う人は間違いがなくてよさそう。このクラスを使うとMainメソッドの中は以下のようになります。

// Hubのあるサイトへの接続
var conn = new HubConnection("http://localhost:10551/");
// HubConnectionをStartする前にHelloHubクラスに対するproxyを作っておく
var proxy = conn.CreateHubProxy("helloHub");
var helloHub = new HelloHub(proxy);
helloHub.OnHelloWorld(Console.WriteLine);

// 接続開始
conn.Start().Wait();

// 10回ほどメッセージを送ってみる
for (int i = 0; i < 10; i++)
{
    helloHub.Hello("tanaka-" + i).Wait();
}

// 待ち
Console.ReadKey();
// 終了
conn.Stop();

当然なのですが、クラス定義してるので文字列での指定が消えました!ばっちりですね。
実行結果は、つまらないので今まで載せてませんでしたが以下のようになります。

tanaka-0 > Hello world!!
tanaka-1 > Hello world!!
tanaka-2 > Hello world!!
tanaka-3 > Hello world!!
tanaka-4 > Hello world!!
tanaka-5 > Hello world!!
tanaka-6 > Hello world!!
tanaka-7 > Hello world!!
tanaka-8 > Hello world!!
tanaka-9 > Hello world!!

続きは…

こういう連続してサーバーサイドからデータが流れてくるものをさばくのに最高のライブラリ…それはReactive Extensionsですよね!!
awaitが単一のものを待って処理するのが得意ならReactive Extensionsは複数のものを待ちつつ来たものから処理していくというのが大変得意です。この組み合わせはきっとアツイ!