チュートリアル: ASP.NET SignalR 1.x によるサーバー ブロードキャスト

作成者: Patrick FletcherTom Dykstra

警告

このドキュメントは、最新版の SignalR を対象としていません。 ASP.NET Core SignalR に関する記事を参照してください。

このチュートリアルでは、ASP.NET SignalR を使用してサーバー ブロードキャスト機能を提供する Web アプリケーションの作成方法について説明します。 サーバー ブロードキャストとは、クライアントに送信される通信がサーバーによって開始されることを意味します。 このシナリオでは、チャット アプリケーションなどのピアツーピアのシナリオとは異なるプログラミング アプローチが必要になります。ピアツーピアのシナリオでは、クライアントに送信される通信は 1 つまたは複数のクライアントによって開始されます。

このチュートリアルで作成するアプリケーションは、サーバー ブロードキャスト機能の一般的なシナリオである株価ティッカーをシミュレートしています。

このチュートリアルに関するコメントをぜひお寄せください。 チュートリアルに直接関連しない質問がある場合は、ASP.NET SignalR フォーラムまたは StackOverflow.com に投稿できます。

概要

Microsoft.AspNet.SignalR.Sample NuGet パッケージでは、株価ティッカーをシミュレートするサンプル アプリケーションを Visual Studio プロジェクトにインストールできます。 このチュートリアルの最初の部分では、そのアプリケーションの簡略化バージョンを一から作成します。 チュートリアルの残りの部分では、この NuGet パッケージをインストールし、作成された追加の機能とコードを確認します。

株価ティッカー アプリケーションは、接続されているすべてのクライアントにサーバーから定期的に通知を "プッシュ" またはブロードキャストする、一種の代表的なリアルタイム アプリケーションです。

このチュートリアルの最初の部分で作成するアプリケーションでは、株価データを含むグリッドを表示します。

StockTicker initial version

サーバーは定期的にランダムに株価を更新し、接続されているすべてのクライアントにその更新をプッシュします。 ブラウザーでは、Change 列と % 列の数値と記号が、サーバーからの通知に応じて動的に変化します。 他のブラウザーで同じ URL を開いている場合は、どのブラウザーにも、同じデータおよびそのデータが同じように変動するのが同時に表示されます。

このチュートリアルは、次のセクションで構成されています。

Note

アプリケーションを作成する手順を実行したくない場合は、新しい空の ASP.NET Web アプリケーション プロジェクトに SignalR.Sample パッケージをインストールし、そのコードの説明としてこれらの手順を確認することができます。 このチュートリアルの最初のパートでは SignalR.Sample のコードのサブセットについて説明し、2 番目のパートでは SignalR.Sample パッケージに含まれている主な追加機能について説明します。

前提条件

開始する前に、お使いのコンピューターに Visual Studio 2012 または 2010 SP1 がインストールされていることを確認してください。 Visual Studio をお持ちでない場合は、ASP.NET のダウンロードに関するページを参照して、無料の Visual Studio 2012 Express for Web を入手してください。

Visual Studio 2010 をお使いの場合は、NuGet がインストールされていることを確認してください。

プロジェクトを作成する

  1. [ファイル] メニューの [新しいプロジェクト] をクリックします。

  2. [新しいプロジェクト] ダイアログ ボックスで [テンプレート] の下の [C#] を展開し、[Web] を選びます。

  3. [ASP.NET 空の Web アプリケーション] テンプレートを選び、プロジェクトに「SignalR.StockTicker」という名前を付けて、[OK] をクリックします。

    New Project dialog box

SignalR NuGet パッケージを追加する

SignalR と JQuery の NuGet パッケージを追加する

NuGet パッケージをインストールすることで、SignalR 機能をプロジェクトに追加できます。

  1. [ツール] | [NuGet パッケージ マネージャー] | [パッケージ マネージャー コンソール] をクリックします。

  2. パッケージ マネージャーで次のコマンドを入力します。

    Install-Package Microsoft.AspNet.SignalR -Version 1.1.3
    

    SignalR パッケージを使うと、他の多数の NuGet パッケージを依存関係としてインストールすることができます。 インストールが完了すると、ASP.NET アプリケーションで SignalR を使うために必要なサーバーとクライアントのコンポーネントがすべて揃います。

サーバー コードを設定する

このセクションでは、サーバー上で実行されるコードを設定します。

Stock クラスを作成する

まず、株式に関する情報の格納と送信に使用する Stock モデル クラスを作成します。

  1. プロジェクト フォルダーに新しいクラス ファイルを作成し、「Stock.cs」という名前を付けてから、そのテンプレート コードを次のコードに置き換えます。

    using System;
    
    namespace SignalR.StockTicker
    {
        public class Stock
        {
            private decimal _price;
    
            public string Symbol { get; set; }
    
            public decimal Price
            {
                get
                {
                    return _price;
                }
                set
                {
                    if (_price == value)
                    {
                        return;
                    }
    
                    _price = value;
    
                    if (DayOpen == 0)
                    {
                        DayOpen = _price;
                    }
                }
            }
    
            public decimal DayOpen { get; private set; }
    
            public decimal Change
            {
                get
                {
                    return Price - DayOpen;
                }
            }
    
            public double PercentChange
            {
                get
                {
                    return (double)Math.Round(Change / Price, 4);
                }
            }
        }
    }
    

    株式を作成するときに設定する 2 つのプロパティは、Symbol (例: Microsoft の場合は MSFT) と Price です。 その他のプロパティは、Price を設定する方法とタイミングによって異なります。 Price を初めて設定したときは、その値が DayOpen に反映されます。 それ以降に Price を設定すると、Price と DayOpen の差に基づいて Change プロパティと PercentChange プロパティの値が計算されます。

StockTicker クラスと StockTickerHub クラスを作成する

SignalR Hub API を使用して、サーバーからクライアントへの対話を処理します。 SignalR Hub クラスから派生した StockTickerHub クラスを使用して、クライアントから受信した接続とメソッド呼び出しを処理します。 また、クライアントの接続とは関係なく、株式のデータを管理し、Timer オブジェクトを実行して株価の更新を定期的にトリガーする必要もあります。 Hub インスタンスは一時的なものであるため、これらの機能を Hub クラスに配置することはできません。 Hub クラスのインスタンスは、クライアントからサーバーへの接続や呼び出しなど、ハブに対する操作ごとに作成されます。 そのため、株式データの保持、株価の更新、株価の更新のブロードキャストを行うメカニズムは、別のクラスで実行する必要があります。このクラスには StockTicker という名前を付けます。

Broadcasting from StockTicker

サーバー上で実行する必要がある StockTicker クラスのインスタンスは 1 つのみです。そのため、各 StockTickerHub インスタンスからシングルトン StockTicker インスタンスへの参照を設定する必要があります。 StockTicker クラスは株式データを保持し、更新をトリガーするので、クライアントにブロードキャストできる必要がありますが、StockTicker は Hub クラスではありません。 そのため、StockTicker クラスでは、SignalR Hub 接続コンテキスト オブジェクトへの参照を取得する必要があります。 その後、SignalR 接続コンテキスト オブジェクトを使ってクライアントへのブロードキャストを行うことができます。

  1. ソリューション エクスプローラーでプロジェクトを右クリックし、[新しい項目の追加] をクリックします。

  2. ASP.NET and Web Tools 2012.2 Update を含む Visual Studio 2012 をお使いの場合は、[Visual C#] の下の [Web] をクリックし、[SignalR Hub クラス] 項目テンプレートを選びます。 それ以外の場合は、[クラス] テンプレートを選びます。

  3. 新しいクラスに「StockTickerHub.cs」という名前を付け、[追加] をクリックします。

    Add StockTickerHub.cs

  4. テンプレート コードを次のコードに置き換えます。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using Microsoft.AspNet.SignalR;
    using Microsoft.AspNet.SignalR.Hubs;
    
    namespace SignalR.StockTicker
    {
        [HubName("stockTickerMini")]
        public class StockTickerHub : Hub
        {
            private readonly StockTicker _stockTicker;
    
            public StockTickerHub() : this(StockTicker.Instance) { }
    
            public StockTickerHub(StockTicker stockTicker)
            {
                _stockTicker = stockTicker;
            }
    
            public IEnumerable<Stock> GetAllStocks()
            {
                return _stockTicker.GetAllStocks();
            }
        }
    }
    

    Hub クラスを使って、クライアントがサーバーに対して呼び出すことができるメソッドを定義します。 GetAllStocks() という 1 つのメソッドを定義しています。 クライアントは、サーバーへの最初の接続時にこのメソッドを呼び出して、すべての株式とそれらの現在の株価の一覧を取得します。 このメソッドは同期的に実行し、IEnumerable<Stock> を返すことができます。メモリからデータを返すためです。 このメソッドで、データベース検索や Web サービス呼び出しなどの待機を伴う処理を行ってデータを取得する必要がある場合は、戻り値として Task<IEnumerable<Stock>> を指定し、非同期処理を有効にする必要があります。 詳しくは、「ASP.NET SignalR Hubs API ガイド - サーバー」の「非同期的に実行するタイミング」を参照してください。

    HubName 属性により、クライアント上の JavaScript コードで Hub を参照する方法を指定します。 この属性を使わない場合、クライアント上での既定の名前はクラス名のキャメル ケース形式になります。この場合は stockTickerHub になります。

    後ほど StockTicker クラスを作成するときに確認するように、そのクラスのシングルトン インスタンスがその静的な Instance プロパティ内に作成されます。 その StockTicker のシングルトン インスタンスは、どれだけ多くのクライアントが接続または切断してもメモリ内に残り続けます。また、GetAllStocks メソッドが現在の株式情報を返すために使うのはこのインスタンスです。

  5. プロジェクト フォルダーに新しいクラス ファイルを作成し、「StockTicker.cs」という名前を付けてから、そのテンプレート コードを次のコードに置き換えます。

    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Threading;
    using Microsoft.AspNet.SignalR;
    using Microsoft.AspNet.SignalR.Hubs;
    
    namespace SignalR.StockTicker
    {
        public class StockTicker
        {
            // Singleton instance
            private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));
    
            private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();
    
            private readonly object _updateStockPricesLock = new object();
    
            //stock can go up or down by a percentage of this factor on each change
            private readonly double _rangePercent = .002;
    
            private readonly TimeSpan _updateInterval = TimeSpan.FromMilliseconds(250);
            private readonly Random _updateOrNotRandom = new Random();
    
            private readonly Timer _timer;
            private volatile bool _updatingStockPrices = false;
    
            private StockTicker(IHubConnectionContext clients)
            {
                Clients = clients;
    
                _stocks.Clear();
                var stocks = new List<Stock>
                {
                    new Stock { Symbol = "MSFT", Price = 30.31m },
                    new Stock { Symbol = "APPL", Price = 578.18m },
                    new Stock { Symbol = "GOOG", Price = 570.30m }
                };
                stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock));
    
                _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
    
            }
    
            public static StockTicker Instance
            {
                get
                {
                    return _instance.Value;
                }
            }
    
            private IHubConnectionContext Clients
            {
                get;
                set;
            }
    
            public IEnumerable<Stock> GetAllStocks()
            {
                return _stocks.Values;
            }
    
            private void UpdateStockPrices(object state)
            {
                lock (_updateStockPricesLock)
                {
                    if (!_updatingStockPrices)
                    {
                        _updatingStockPrices = true;
    
                        foreach (var stock in _stocks.Values)
                        {
                            if (TryUpdateStockPrice(stock))
                            {
                                BroadcastStockPrice(stock);
                            }
                        }
    
                        _updatingStockPrices = false;
                    }
                }
            }
    
            private bool TryUpdateStockPrice(Stock stock)
            {
                // Randomly choose whether to update this stock or not
                var r = _updateOrNotRandom.NextDouble();
                if (r > .1)
                {
                    return false;
                }
    
                // Update the stock price by a random factor of the range percent
                var random = new Random((int)Math.Floor(stock.Price));
                var percentChange = random.NextDouble() * _rangePercent;
                var pos = random.NextDouble() > .51;
                var change = Math.Round(stock.Price * (decimal)percentChange, 2);
                change = pos ? change : -change;
    
                stock.Price += change;
                return true;
            }
    
            private void BroadcastStockPrice(Stock stock)
            {
                Clients.All.updateStockPrice(stock);
            }
    
        }
    }
    

    複数のスレッドで StockTicker コードの同じインスタンスが実行されるため、StockTicker クラスはスレッド セーフである必要があります。

    静的フィールドにシングルトン インスタンスを格納する

    このコードは、クラスのインスタンスを含む Instance プロパティをサポートする静的な _instance フィールドを初期化します。コンストラクターはプライベートとしてマークされているため、これがこのクラスの作成できる唯一のインスタンスです。 _instance フィールドに遅延初期化が使われているのは、パフォーマンス上の理由ではなく、インスタンスの作成をスレッド セーフにするためです。

    private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));
    
    public static StockTicker Instance
    {
        get
        {
            return _instance.Value;
        }
    }
    

    StockTickerHub クラスで前述したように、クライアントがサーバーに接続するたびに、個別のスレッドで実行される StockTickerHub クラスの新しいインスタンスによって、StockTicker.Instance 静的プロパティから StockTicker シングルトン インスタンスが取得されます。

    ConcurrentDictionary に株式データを格納する

    コンストラクターではサンプル株式データを使って _stocks コレクションが初期化され、GetAllStocks によって株式が返されます。 前述のように、この株式のコレクションが StockTickerHub.GetAllStocks によって返されます。これは、クライアントが呼び出すことができる Hub クラスのサーバー メソッドです。

    private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();
    
    private StockTicker(IHubConnectionContext clients)
    {
        Clients = clients;
    
        _stocks.Clear();
        var stocks = new List<Stock>
        {
            new Stock { Symbol = "MSFT", Price = 30.31m },
            new Stock { Symbol = "APPL", Price = 578.18m },
            new Stock { Symbol = "GOOG", Price = 570.30m }
        };
        stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock));
    
        _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
    }
    
    public IEnumerable<Stock> GetAllStocks()
    {
        return _stocks.Values;
    }
    

    株式のコレクションは、スレッド セーフのために ConcurrentDictionary 型として定義されます。 別の方法としては、Dictionary オブジェクトを使用して、辞書に変更を加える際に辞書を明示的にロックすることもできます。

    このサンプル アプリケーションでは、アプリケーション データをメモリに格納し、StockTicker インスタンスが破棄されたときにそのデータが失われても問題ありません。 実際のアプリケーションでは、データベースなどのバックエンド データ ストアを操作します。

    株価を定期的に更新する

    このコンストラクターにより、株価をランダムに更新するメソッドを定期的に呼び出す Timer オブジェクトが起動します。

    _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
    
    private void UpdateStockPrices(object state)
    {
        lock (_updateStockPricesLock)
        {
            if (!_updatingStockPrices)
            {
                _updatingStockPrices = true;
    
                foreach (var stock in _stocks.Values)
                {
                    if (TryUpdateStockPrice(stock))
                    {
                        BroadcastStockPrice(stock);
                    }
                }
    
                _updatingStockPrices = false;
            }
        }
    }
    
    private bool TryUpdateStockPrice(Stock stock)
    {
        // Randomly choose whether to update this stock or not
        var r = _updateOrNotRandom.NextDouble();
        if (r > .1)
        {
            return false;
        }
    
        // Update the stock price by a random factor of the range percent
        var random = new Random((int)Math.Floor(stock.Price));
        var percentChange = random.NextDouble() * _rangePercent;
        var pos = random.NextDouble() > .51;
        var change = Math.Round(stock.Price * (decimal)percentChange, 2);
        change = pos ? change : -change;
    
        stock.Price += change;
        return true;
    }
    

    UpdateStockPrices は Timer によって呼び出され、state パラメーターで null が渡されます。 株価を更新する前に、_updateStockPricesLock オブジェクトに対するロックを取得します。 コードでは、別のスレッドで既に株価が更新されているかどうかをチェックしてから、リスト内の各株式に対して TryUpdateStockPrice を呼び出します。 TryUpdateStockPrice メソッドでは、株価を変更するかどうか、およびどのくらい変更するかが決定されます。 株価が変更されると、BroadcastStockPrice が呼び出され、接続されているすべてのクライアントに株価の変更がブロードキャストされます。

    _updatingStockPrices フラグは、スレッド セーフにアクセスできるよう volatile としてマークされています。

    private volatile bool _updatingStockPrices = false;
    

    実際のアプリケーションでは、TryUpdateStockPrice メソッドで Web サービスを呼び出し、価格を調べるでしょう。このコードでは、乱数ジェネレーターを使ってランダムな変更を行います。

    StockTicker クラスがクライアントにブロードキャストできるように SignalR コンテキストを取得する

    株価の変更は StockTicker オブジェクトから発生するため、接続されているすべてのクライアントに対してこのオブジェクトが updateStockPrice メソッドを呼び出す必要があります。 Hub クラスにはクライアント メソッドを呼び出すための API がありますが、StockTicker は Hub クラスから派生しておらず、Hub オブジェクトへの参照も持っていません。 そのため、接続されているクライアントにブロードキャストするには、StockTicker クラスで StockTickerHub クラスの SignalR コンテキスト インスタンスを取得し、それを使用してクライアントに対してメソッドを呼び出す必要があります。

    このコードでは、シングルトン クラス インスタンスを作成するときに SignalR コンテキストへの参照を取得し、その参照をコンストラクターに渡して、コンストラクターがそれを Clients プロパティに設定します。

    コンテキストの取得を 1 回だけに抑えたい理由は 2 つあります。コンテキストの取得はコストの高い操作であること、また 1 回だけ取得することで、クライアントに送信されるメッセージの意図した順序を保持できることです。

    private readonly static Lazy<StockTicker> _instance =
        new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));
    
    private StockTicker(IHubConnectionContext clients)
    {
        Clients = clients;
    
        // Remainder of constructor ...
    }
    
    private IHubConnectionContext Clients
    {
        get;
        set;
    }
    
    private void BroadcastStockPrice(Stock stock)
    {
        Clients.All.updateStockPrice(stock);
    }
    

    コンテキストの Clients プロパティを取得し、それを StockTickerClient プロパティに設定すれば、Hub クラスの場合と同じようなコードを記述してクライアント メソッドを呼び出すことができます。 たとえば、すべてのクライアントにブロードキャストするには、Clients.All.updateStockPrice(stock) を記述できます。

    BroadcastStockPrice で呼び出している updateStockPrice メソッドは、まだ存在していません。これは後ほど、クライアント上で実行するコードを記述するときに追加します。 ここで updateStockPrice を参照できるのは、Clients.All が動的であるためです。つまり、この式は実行時に評価されます。 このメソッド呼び出しが実行されると、SignalR によってメソッド名とパラメータ値がクライアントに送信され、クライアントに updateStockPrice という名前のメソッドがある場合はそのメソッドが呼び出され、パラメータ値が渡されます。

    Clients.All は、すべてのクライアントに送信するという意味です。 SignalR には、送信先のクライアントまたはクライアント グループを指定するためのオプションが他にも用意されています。 詳しくは、HubConnectionContext に関する記事を参照してください。

SignalR ルートを登録する

サーバーでは、どの URL をインターセプトして SignalR に転送するかを把握する必要があります。 これを行うために、Global.asax ファイルにいくつかのコードを追加します。

  1. ソリューション エクスプローラーでプロジェクトを右クリックし、[新しい項目の追加] をクリックします。

  2. [グローバル アプリケーション クラス] 項目テンプレートを選択し、[追加] をクリックします。

    Add global.asax

  3. SignalR ルート登録コードを Application_Start メソッドに追加します。

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

    既定では、すべての SignalR トラフィックのベース URL は "/signalr" であり、"/signalr/hubs" を使って、アプリケーション内にあるすべてのハブのプロキシを定義する、動的に生成された JavaScript ファイルを取得します。 MapHubs メソッドには、異なるベース URL と HubConfiguration クラスのインスタンスの特定の SignalR オプションを指定できるオーバーロードが含まれています。

  4. ファイルの先頭に using ステートメントを追加します。

    using System.Web.Routing;
    
  5. Global.asax ファイルを保存して閉じ、プロジェクトをビルドします。

これで、サーバー コードの設定が完了しました。 次のセクションでは、クライアントを設定します。

クライアント コードを設定する

  1. プロジェクト フォルダー内に新しい HTML ファイルを作成し、「StockTicker.html」という名前を付けます。

  2. テンプレート コードを次のコードに置き換えます。

    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>ASP.NET SignalR Stock Ticker</title>
        <style>
            body {
                font-family: 'Segoe UI', Arial, Helvetica, sans-serif;
                font-size: 16px;
            }
            #stockTable table {
                border-collapse: collapse;
            }
                #stockTable table th, #stockTable table td {
                    padding: 2px 6px;
                }
                #stockTable table td {
                    text-align: right;
                }
            #stockTable .loading td {
                text-align: left;
            }
        </style>
    </head>
    <body>
        <h1>ASP.NET SignalR Stock Ticker Sample</h1>
    
        <h2>Live Stock Table</h2>
        <div id="stockTable">
            <table border="1">
                <thead>
                    <tr><th>Symbol</th><th>Price</th><th>Open</th><th>Change</th><th>%</th></tr>
                </thead>
                <tbody>
                    <tr class="loading"><td colspan="5">loading...</td></tr>
                </tbody>
            </table>
        </div>
    
        <!--Script references. -->
        <!--Reference the jQuery library. -->
        <script src="/Scripts/jquery-1.8.2.min.js" ></script>
        <!--Reference the SignalR library. -->
        <script src="/Scripts/jquery.signalR-1.0.1.js"></script>
        <!--Reference the autogenerated SignalR hub script. -->
        <script src="/signalr/hubs"></script>
        <!--Reference the StockTicker script. -->
        <script src="StockTicker.js"></script>
    </body>
    </html>
    

    この HTML では、5 つの列、ヘッダー行、1 つのセルが 5 列すべてにまたがっているデータ行を含む表が作成されます。 このデータ行には "loading..." と表示されます。これは、アプリケーションの起動時にだけ一時的に表示されます。 JavaScript コードによってその行は削除され、その場所にはサーバーから取得した株式データが含まれている行が追加されます。

    script タグにより、jQuery スクリプト ファイル、SignalR コア スクリプト ファイル、SignalR プロキシ スクリプト ファイル、および後で作成する StockTicker スクリプト ファイルを指定します。 "/signalr/hubs" という URL を指定する SignalR プロキシ スクリプト ファイルは動的に生成され、Hub クラスのメソッドに対するプロキシ メソッドを定義します (この場合は StockTickerHub.GetAllStocks)。 必要に応じて、SignalR ユーティリティを使って手動でこの JavaScript ファイルを生成し、MapHubs メソッド呼び出しの動的ファイル作成を無効にすることもできます。

  3. 重要

    StockTicker.html の JavaScript ファイル参照が正しいことを確認してください。 つまり、スクリプト タグの jQuery バージョン (この例では 1.8.2) がプロジェクトの Scripts フォルダーの jQuery バージョンと同じであることを確認し、script タグの SignalR バージョンがプロジェクトの Scripts フォルダーの SignalR バージョンと同じであることを確認してください。 必要に応じて、script タグのファイル名を変更してください。

  4. ソリューション エクスプローラーで、StockTicker.html を右クリックして、[スタート ページとして設定] をクリックします。

  5. プロジェクト フォルダー内に新しい JavaScript ファイルを作成し、「StockTicker.js」という名前を付けます。

  6. テンプレート コードを次のコードに置き換えます。

    // A simple templating method for replacing placeholders enclosed in curly braces.
    if (!String.prototype.supplant) {
        String.prototype.supplant = function (o) {
            return this.replace(/{([^{}]*)}/g,
                function (a, b) {
                    var r = o[b];
                    return typeof r === 'string' || typeof r === 'number' ? r : a;
                }
            );
        };
    }
    
    $(function () {
    
        var ticker = $.connection.stockTickerMini, // the generated client-side hub proxy
            up = '▲',
            down = '▼',
            $stockTable = $('#stockTable'),
            $stockTableBody = $stockTable.find('tbody'),
            rowTemplate = '<tr data-symbol="{Symbol}"><td>{Symbol}</td><td>{Price}</td><td>{DayOpen}</td><td>{Direction} {Change}</td><td>{PercentChange}</td></tr>';
    
        function formatStock(stock) {
            return $.extend(stock, {
                Price: stock.Price.toFixed(2),
                PercentChange: (stock.PercentChange * 100).toFixed(2) + '%',
                Direction: stock.Change === 0 ? '' : stock.Change >= 0 ? up : down
            });
        }
    
        function init() {
            ticker.server.getAllStocks().done(function (stocks) {
                $stockTableBody.empty();
                $.each(stocks, function () {
                    var stock = formatStock(this);
                    $stockTableBody.append(rowTemplate.supplant(stock));
                });
            });
        }
    
        // Add a client-side hub method that the server will call
        ticker.client.updateStockPrice = function (stock) {
            var displayStock = formatStock(stock),
                $row = $(rowTemplate.supplant(displayStock));
    
            $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
                .replaceWith($row);
            }
    
        // Start the connection
        $.connection.hub.start().done(init);
    
    });
    

    $.connection は SignalR プロキシを参照します。 このコードでは、StockTickerHub クラスのプロキシへの参照を取得して、ticker 変数に格納しています。 プロキシ名は、[HubName] 属性によって設定された名前です。

    var ticker = $.connection.stockTickerMini
    
    [HubName("stockTickerMini")]
    public class StockTickerHub : Hub
    

    すべての変数と関数を定義した後、ファイル内のコードの最終行では、SignalR start 関数を呼び出して SignalR 接続を初期化しています。 start 関数は非同期的に実行され、jQuery Deferred オブジェクトを返します。つまり、done 関数を呼び出して、非同期操作が完了したときに呼び出す関数を指定できます。

    $.connection.hub.start().done(init);
    

    init 関数は、サーバー上の getAllStocks 関数を呼び出し、サーバーから返される情報を使って株価一覧を更新します。 メソッド名はサーバー上ではパスカル ケースですが、クライアント上では既定でキャメル ケースを使う必要があることに注意してください。 キャメル ケースの規則が適用されるのはメソッドのみであり、オブジェクトには適用されません。 たとえば、stock.symbolや stock.price ではなく、stock.Symbol や stock.Price を参照します。

    function init() {
        ticker.server.getAllStocks().done(function (stocks) {
            $stockTableBody.empty();
            $.each(stocks, function () {
                var stock = formatStock(this);
                $stockTableBody.append(rowTemplate.supplant(stock));
            });
        });
    }
    
    public IEnumerable<Stock> GetAllStocks()
    {
        return _stockTicker.GetAllStocks();
    }
    

    クライアント上でパスカル ケースを使いたい場合や、まったく異なるメソッド名を使いたい場合は、Hub クラス自体を HubName 属性で修飾するのと同じように、Hub メソッドを HubMethodName 属性で修飾できます。

    init メソッドでは、サーバーから受け取った stock オブジェクトごとに、表の行を表す HTML を作成します。そのために、formatStock を呼び出して stock オブジェクトのプロパティを書式設定してから、supplant (StockTicker.js の先頭で定義されています) を呼び出して rowTemplate 変数のプレースホルダーを stock オブジェクトのプロパティ値に置き換えています。 その後、結果として作成された HTML が、株価一覧に追加されます。

    init の呼び出しは、これを非同期の start 関数の完了後に実行されるコールバック関数として渡すことによって行われます。 start を呼び出した後に init を個別の JavaScript ステートメントとして呼び出した場合、この関数は start 関数で接続の確立が完了するのを待たずにすぐ実行されるため、失敗します。 その場合、init 関数は、サーバー接続が確立される前に getAllStocks 関数を呼び出そうとします。

    サーバーでは、株価を変更するときに、接続されているクライアントに対して updateStockPrice を呼び出します。 この関数は、サーバーからの呼び出しで使用できるようにするために、stockTicker プロキシの client プロパティに追加されます。

    ticker.client.updateStockPrice = function (stock) {
        var displayStock = formatStock(stock),
            $row = $(rowTemplate.supplant(displayStock));
    
        $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
            .replaceWith($row);
        }
    

    updateStockPrice 関数では、init 関数と同じ方法で、サーバーから受け取った stock オブジェクトが表の行として書式設定されます。 ただし、表に行を追加するのではなく、表内にある株式の現在の行を検索し、その行を新しい行に置き換えます。

アプリケーションをテストする

  1. F5 キーを押してデバッグ モードでアプリケーションを実行します。

    株式の表には、最初に "loading..." という行が表示された後、少ししてから初期株式データが表示され、その後株価が変動し始めます。

    Loading

    Initial stock table

    Stock table receiving changes from server

  2. ブラウザーのアドレス バーから URL をコピーし、1 つ以上の新しいブラウザー ウィンドウに貼り付けます。

    株式の初期表示は 1 つめのブラウザーと同じであり、変動が同時に起こります。

  3. すべてのブラウザーを閉じて新しいブラウザーを開き、同じ URL に移動します。

    StockTicker シングルトン オブジェクトはサーバー上で引き続き実行されているため、株式の表を見ると株式が変動し続けていたことがわかります。 (change の数値が 0 の初期一覧表は表示されません。)

  4. ブラウザーを閉じます。

ログの有効化

SignalR には、トラブルシューティングを支援するためにクライアント上で有効にできるログ機能が組み込まれています。 このセクションでは、ログを有効にして、SignalR で次のトランスポート方法のうちどれが使用されているのかをログから判断する方法を示す例を確認します。

どのような接続の場合も、SignalR では、サーバーとクライアントの両方でサポートされる最適なトランスポート方法を選択します。

  1. StockTicker.js を開き、ファイルの末尾にある接続を初期化するコードの直前に、ログを有効にするコード行を追加します。

    // Start the connection
    $.connection.hub.logging = true;
    $.connection.hub.start().done(init);
    
  2. F5 キーを押してプロジェクトを実行します。

  3. ブラウザーの開発者ツール ウィンドウを開き、[Console] (コンソール) を選択してログを表示します。 新しい接続のトランスポート方法をネゴシエートする SignalR のログを表示するには、ページの更新が必要となる場合があります。

    Windows 8 (IIS 8) で Internet Explorer 10 を実行している場合、トランスポート方法は WebSocket です。

    IE 10 IIS 8 Console

    Windows 7 (IIS 7.5) で Internet Explorer 10 を実行している場合、トランスポート方法は iframe です。

    IE 10 Console, IIS 7.5

    Firefox では、[Console] (コンソール) ウィンドウを使用するために Firebug アドインをインストールします。 Windows 8 (IIS 8) で Firefox 19 を実行している場合、トランスポート方法は WebSocket です。

    Firefox 19 IIS 8 Websockets

    Windows 7 (IIS 7.5) で Firefox 19 を実行している場合、トランスポート方法はサーバー送信イベントです。

    Firefox 19 IIS 7.5 Console

完全な StockTicker サンプルをインストールして確認する

Microsoft.AspNet.SignalR.Sample NuGet パッケージによってインストールされる StockTicker アプリケーションには、一から作成した簡略化バージョンよりも多くの機能が含まれています。 チュートリアルのこのセクションでは、NuGet パッケージをインストールして、新しい各機能とそれらを実装するコードを確認します。

SignalR.Sample NuGet パッケージをインストールする

  1. ソリューション エクスプローラーで、プロジェクトを右クリックし、[NuGet パッケージの管理] をクリックします。

  2. [NuGet パッケージの管理] ダイアログ ボックスで、[オンライン] をクリックし、[オンラインで検索] ボックスに「SignalR.Sample」と入力して、SignalR.Sample パッケージの [インストール] をクリックします。

    Install SignalR.Sample package

  3. Global.asax ファイル内で、以前に Application_Start メソッドに追加した RouteTable.Routes.MapHubs(); の行をコメントアウトします。

    SignalR.Sample パッケージが SignalR ルートを App_Start/RegisterHubs.cs ファイルに登録するため、Global.asax 内のコードはもう必要ありません。

    [assembly: WebActivator.PreApplicationStartMethod(typeof(SignalR.StockTicker.RegisterHubs), "Start")]
    
    namespace SignalR.StockTicker
    {
        public static class RegisterHubs
        {
            public static void Start()
            {
                // Register the default hubs route: ~/signalr/hubs
                RouteTable.Routes.MapHubs();
            }
        }
    }
    

    アセンブリ属性によって参照される WebActivator クラスは、SignalR.Sample パッケージの依存関係としてインストールされる WebActivatorEx NuGet パッケージに含まれています。

  4. ソリューション エクスプローラーで、SignalR.Sample パッケージをインストールすることで作成された SignalR.Sample フォルダーを展開します。

  5. SignalR.Sample フォルダーで、StockTicker.html を右クリックして、[スタート ページとして設定] をクリックします。

    Note

    SignalR.Sample NuGet パッケージをインストールすると、Scripts フォルダーにある jQuery のバージョンが変更される可能性があります。 パッケージによって SignalR.Sample フォルダーにインストールされた新しい StockTicker.html ファイルは、パッケージによってインストールされた jQuery バージョンと同期されますが、もう一度独自の StockTicker.html ファイルを実行する場合は、script タグの jQuery 参照の更新が必要となることがあります。

アプリケーションの実行

  1. F5 キーを押してアプリケーションを実行します。

    完全な株価ティッカー アプリケーションでは、前に確認したグリッドに加えて、同じ株式データを表示する水平スクロール ウィンドウも表示されます。 アプリケーションを初めて実行すると、"市場" は "閉じている" ため、静的なグリッドとスクロールしないティッカー ウィンドウが表示されます。

    StockTicker screen start

    [Open Market] (市場を開く) をクリックすると、[Live Stock Ticker] (ライブ株式ティッカー) ボックスが水平方向にスクロールし始め、サーバーは株価のランダムな変更を定期的にブロードキャストし始めます。 株価が変化するたびに、[Live Stock Table] (ライブ株式一覧) グリッドと [Live Stock Ticker] (ライブ株式ティッカー) ボックスの両方が更新されます。 株価の変化が正の場合、その株式は緑色の背景で表示され、変化が負の場合、その株式は赤い背景で表示されます。

    StockTicker app, market open

    [Close Market] (市場を閉じる) ボタンを押すと、変化とティッカーのスクロールが停止し、[Reset] (リセット) ボタンを押すと、すべての株式データが株価の変更を開始する前の初期状態にリセットされます。 ブラウザー ウィンドウをさらに開いて同じ URL に移動すると、各ブラウザーで同じデータが同時に動的に更新されるのがわかります。 いずれかのボタンをクリックすると、すべてのブラウザーが同時に同じように反応します。

[Live Stock Ticker] (ライブ株式ティッカー) の表示

[Live Stock Ticker] (ライブ株式ティッカー) の表示は、CSS スタイルで 1 行に書式設定された div 要素内の順序なしリストです。 このティッカーは表と同じ方法で初期化され、更新されます。すなわち、<li> テンプレート文字列内のプレースホルダーを置き換え、<li> 要素を <ul> 要素に動的に追加します。 スクロールは、jQuery のアニメーション関数を使って div 内の順序なしリストの左余白を変更することで実行します。

株式ティッカーの HTML:

<h2>Live Stock Ticker</h2>
<div id="stockTicker">
    <div class="inner">
        <ul>
            <li class="loading">loading...</li>
        </ul>
    </div>
</div>

株式ティッカーの CSS:

#stockTicker {
    overflow: hidden;
    width: 450px;
    height: 24px;
    border: 1px solid #999;
    }

    #stockTicker .inner {
        width: 9999px;
    }

    #stockTicker ul {
        display: inline-block;
        list-style-type: none;
        margin: 0;
        padding: 0;
    }

    #stockTicker li {
        display: inline-block;
        margin-right: 8px;   
    }

    /*<li data-symbol="{Symbol}"><span class="symbol">{Symbol}</span><span class="price">{Price}</span><span class="change">{PercentChange}</span></li>*/
    #stockTicker .symbol {
        font-weight: bold;
    }

    #stockTicker .change {
        font-style: italic;
    }

スクロールさせる jQuery コード:

function scrollTicker() {
    var w = $stockTickerUl.width();
    $stockTickerUl.css({ marginLeft: w });
    $stockTickerUl.animate({ marginLeft: -w }, 15000, 'linear', scrollTicker);
}

クライアントが呼び出すことができるサーバー上の追加のメソッド

StockTickerHub クラスでは、クライアントが呼び出すことができる追加の 4 つのメソッドが定義されます。

public string GetMarketState()
{
    return _stockTicker.MarketState.ToString();
}

public void OpenMarket()
{
    _stockTicker.OpenMarket();
}

public void CloseMarket()
{
    _stockTicker.CloseMarket();
}

public void Reset()
{
    _stockTicker.Reset();
}

OpenMarket、CloseMarket、Reset は、ページの上部にあるボタンに応答して呼び出されます。 これらは、1 つのクライアントが状態の変更をトリガーすると即座にすべてのクライアントに反映されるというパターンを示しています。 これらの各メソッドでは、市場の状態の変化に影響を与えてから新しい状態をブロードキャストする StockTicker クラスのメソッドが呼び出されます。

StockTicker クラスでは、MarketState 列挙値を返す MarketState プロパティによって市場の状態が保持されます。

public MarketState MarketState
{
    get { return _marketState; }
    private set { _marketState = value; }
}

public enum MarketState
{
    Closed,
    Open
}

市場の状態を変更する各メソッドは、その変更を lock ブロック内で行います。StockTicker クラスはスレッド セーフである必要があるためです。

public void OpenMarket()
{
    lock (_marketStateLock)
    {
        if (MarketState != MarketState.Open)
        {
            _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
            MarketState = MarketState.Open;
            BroadcastMarketStateChange(MarketState.Open);
        }
    }
}

public void CloseMarket()
{
    lock (_marketStateLock)
    {
        if (MarketState == MarketState.Open)
        {
            if (_timer != null)
            {
                _timer.Dispose();
            }
            MarketState = MarketState.Closed;
            BroadcastMarketStateChange(MarketState.Closed);
        }
    }
}

public void Reset()
{
    lock (_marketStateLock)
    {
        if (MarketState != MarketState.Closed)
        {
            throw new InvalidOperationException("Market must be closed before it can be reset.");
        }
        LoadDefaultStocks();
        BroadcastMarketReset();
    }
}

このコードがスレッド セーフになるように、MarketState プロパティをサポートする _marketState フィールドは volatile としてマークされます。

private volatile MarketState _marketState;

BroadcastMarketStateChange メソッドと BroadcastMarketReset メソッドは、既に確認した BroadcastStockPrice メソッドと似ていますが、クライアント側で定義された異なるメソッドが呼び出されるという点が異なります。

private void BroadcastMarketStateChange(MarketState marketState)
{
    switch (marketState)
    {
        case MarketState.Open:
            Clients.All.marketOpened();
            break;
        case MarketState.Closed:
            Clients.All.marketClosed();
            break;
        default:
            break;
    }
}

private void BroadcastMarketReset()
{
    Clients.All.marketReset();
}

サーバーが呼び出すことができるクライアント上の追加の関数

updateStockPrice 関数は、グリッドとティッカーの両方の表示を処理できるようになりました。これは jQuery.Color を使って赤と緑の色を点滅させます。

SignalR.StockTicker.js 内の新しい関数は、市場の状態に基づいてボタンを有効または無効にし、ティッカー ウィンドウの水平スクロールを停止または開始します。 複数の関数が ticker.client に追加されるため、jQuery の extend 関数を使って追加しています。

$.extend(ticker.client, {
    updateStockPrice: function (stock) {
        var displayStock = formatStock(stock),
            $row = $(rowTemplate.supplant(displayStock)),
            $li = $(liTemplate.supplant(displayStock)),
            bg = stock.LastChange === 0
                ? '255,216,0' // yellow
                : stock.LastChange > 0
                    ? '154,240,117' // green
                    : '255,148,148'; // red

        $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
            .replaceWith($row);
        $stockTickerUl.find('li[data-symbol=' + stock.Symbol + ']')
            .replaceWith($li);

        $row.flash(bg, 1000);
        $li.flash(bg, 1000);
    },

    marketOpened: function () {
        $("#open").prop("disabled", true);
        $("#close").prop("disabled", false);
        $("#reset").prop("disabled", true);
        scrollTicker();
    },

    marketClosed: function () {
        $("#open").prop("disabled", false);
        $("#close").prop("disabled", true);
        $("#reset").prop("disabled", false);
        stopTicker();
    },

    marketReset: function () {
        return init();
    }
});

接続を確立した後のクライアントの追加の設定

クライアントは、接続を確立した後にいつくかの追加の作業を行う必要があります。それは、marketOpened 関数または marketClosed 関数を適切に呼び出すために市場が開いているか閉じているかを調べることと、サーバー メソッドの呼び出しをボタンに追加することです。

$.connection.hub.start()
    .pipe(init)
    .pipe(function () {
        return ticker.server.getMarketState();
    })
    .done(function (state) {
        if (state === 'Open') {
            ticker.client.marketOpened();
        } else {
            ticker.client.marketClosed();
        }

        // Wire up the buttons
        $("#open").click(function () {
            ticker.server.openMarket();
        });

        $("#close").click(function () {
            ticker.server.closeMarket();
        });

        $("#reset").click(function () {
            ticker.server.reset();
        });
    });

サーバー メソッドは接続が確立されるまでボタンに関連付けられないため、使用できるようになる前にコードでサーバー メソッドを呼び出そうとすることはできません。

次のステップ

このチュートリアルでは、一定のペースまたはクライアントからの通知に応答して、サーバーからすべての接続されているクライアントにメッセージをブロードキャストする SignalR アプリケーションをプログラムする方法を学習しました。 マルチスレッドのシングルトン インスタンスを使ってサーバーの状態を管理するパターンは、マルチプレーヤー オンライン ゲームのシナリオでも使用できます。 その一例としては、SignalR に基づく ShootR ゲームを参照してください。

ピアツーピア通信のシナリオを示すチュートリアルについては、SignalR の概要および SignalR を使用したリアルタイム更新に関する記事を参照してください。

より高度な SignalR 開発の概念については、以下のサイトで SignalR に関するソース コードとリソースを参照してください。