教學課程:使用 SignalR 1.x 進行伺服器廣播

作者: Patrick FletcherTom Dykstra

警告

本檔不適用於最新版的 SignalR。 請查看ASP.NET Core SignalR

本教學課程說明如何建立使用 ASP.NET SignalR 提供伺服器廣播功能的 Web 應用程式。 伺服器廣播表示傳送至用戶端的通訊是由伺服器起始。 此案例需要與對等案例不同的程式設計方法,例如聊天應用程式,其中傳送至用戶端的通訊是由一或多個用戶端起始。

您將在本教學課程中建立的應用程式會模擬股票勾號,這是伺服器廣播功能的一般案例。

歡迎使用教學課程的批註。 如果您有與教學課程不直接相關的問題,您可以將問題張貼到 ASP.NET SignalR 論壇StackOverflow.com

概觀

Microsoft.AspNet.SignalR.Sample NuGet 套件會在 Visual Studio 專案中安裝範例模擬股票勾號應用程式。 在本教學課程的第一個部分中,您將從頭開始建立該應用程式的簡化版本。 在本教學課程的其餘部分中,您將安裝 NuGet 套件,並檢閱其建立的其他功能和程式碼。

股票勾點器應用程式是一種即時應用程式代表,您想要定期「推送」或廣播從伺服器到所有已連線用戶端的通知。

您將在本教學課程的第一個部分中建置的應用程式會顯示內建資料的方格。

StockTicker 初始版本

伺服器會定期隨機更新股票價格,並將更新推送至所有連線的用戶端。 在瀏覽器中, [變更 ] 和資料 % 行中的數位和符號會動態變更,以回應來自伺服器的通知。 如果您將其他瀏覽器開啟至相同的 URL,它們都會同時顯示相同的資料和相同的資料變更。

本教學課程包含下列各節:

注意

如果您不想要完成建置應用程式的步驟,可以在新的 空白 ASP.NET Web 應用程式 專案中安裝 SignalR.Sample 套件,並閱讀這些步驟以取得程式碼的說明。 本教學課程的第一個部分涵蓋 SignalR.Sample 程式碼的子集,第二個部分說明 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,然後按一下 [ 確定]。

    [新增專案] 對話方塊

新增 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);
                }
            }
        }
    }
    

    當您建立股票時所設定的兩個屬性是 Symbol (例如,適用于 Microsoft 的 MSFT) 和 Price。 其他屬性取決於您設定 Price 的方式和時機。 第一次設定 Price 時,值會傳播到 DayOpen。 後續當您設定 Price 時,會根據 Price 和 DayOpen 之間的差異來計算 Change 和 PercentChange 屬性值。

建立 StockTicker 和 StockTickerHub 類別

您將使用 SignalR 中樞 API 來處理伺服器對用戶端互動。 衍生自 SignalR Hub 類別的 StockTickerHub 類別將處理來自用戶端的接收連線和方法呼叫。 您也需要維護庫存資料並執行 Timer 物件,以定期觸發價格更新,而不需要用戶端連線。 您無法將這些函式放在 Hub 類別中,因為中樞實例是暫時性的。 中樞類別實例會針對中樞上的每個作業建立,例如從用戶端到伺服器的連線和呼叫。 因此,保留股票資料、更新價格及廣播價格更新的機制必須在個別類別中執行,您將將其命名為 StockTicker。

從 StockTicker 廣播

您只需要在伺服器上執行 StockTicker 類別的一個實例,因此您必須設定每個 StockTickerHub 實例到單一 StockTicker 實例的參考。 StockTicker 類別必須能夠廣播給用戶端,因為它有股票資料和觸發更新,但 StockTicker 不是中樞類別。 因此,StockTicker 類別必須取得 SignalR Hub 連接內容物件的參考。 然後,它可以使用 SignalR 連接內容物件來廣播至用戶端。

  1. Solution Explorer中,以滑鼠右鍵按一下專案,然後按一下 [新增專案]。

  2. 如果您有具有 ASP.NET 和 Web 工具 2012.2 Update 的 Visual Studio 2012,請按一下Visual C#底下的[Web],然後選取SignalR Hub 類別專案範本。 否則,請選取 [類別 ] 範本。

  3. 將新類別命名為 StockTickerHub.cs,然後按一下 [ 新增]。

    新增 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() 。 當用戶端一開始連線到伺服器時,它會呼叫這個方法,以取得所有股票及其目前價格的清單。 方法可以同步執行並傳回 IEnumerable<Stock> ,因為它會從記憶體傳回資料。 如果 方法必須執行涉及等候的專案來取得資料,例如資料庫查閱或 Web 服務呼叫,您可以指定 Task<IEnumerable<Stock>> 做為傳回值,以啟用非同步處理。 如需詳細資訊,請參閱 ASP.NET SignalR Hubs API 指南 - 伺服器 - 非同步執行時機

    HubName 屬性會指定如何在用戶端上的 JavaScript 程式碼中參考中樞。 如果您未使用此屬性,則用戶端上的預設名稱是類別名稱的 camel 大小寫版本,在此案例中為 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 類別必須是 threadsafe。

    將單一實例儲存在靜態欄位中

    程式碼會初始化靜態_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 類別的新實例都會從 StockTicker.Instance 靜態屬性取得 StockTicker.Instance 單一實例,如您稍早在 StockTickerHub 類別中所見。

    將庫存資料儲存在 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 服務來查閱價格;在此程式碼中,它會使用亂數產生器隨機進行變更。

    取得 SignalR 內容,讓 StockTicker 類別可以廣播給用戶端

    因為價格變更源自 StockTicker 物件,所以這是必須在所有已連線用戶端上呼叫 updateStockPrice 方法的物件。 在中樞類別中,您有用來呼叫用戶端方法的 API,但 StockTicker 不會衍生自 Hub 類別,而且沒有任何 Hub 物件的參考。 因此,為了廣播至連線的用戶端,StockTicker 類別必須取得 StockTickerHub 類別的 SignalR 內容實例,並使用該實例在用戶端上呼叫方法。

    程式碼會在建立單一類別實例、傳遞該參考至建構函式時,取得 SignalR 內容的參考,而建構函式會將它放入 Clients 屬性中。

    您只想要取得內容一次的原因有兩個:取得內容是昂貴的作業,一旦確保傳送給用戶端的訊息預定順序會保留。

    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 屬性中,可讓您撰寫程式碼來呼叫與中樞類別中相同之用戶端方法。 例如,若要廣播給所有用戶端,您可以撰寫 Clients.All.updateStockPrice (股票) 。

    您在 BroadcastStockPrice 中呼叫的 updateStockPrice 方法尚不存在;您稍後會在撰寫在用戶端上執行的程式碼時新增它。 您可以在這裡參考 updateStockPrice,因為 Clients.All 是動態的,這表示運算式將在執行時間進行評估。 當這個方法呼叫執行時,SignalR 會將方法名稱和參數值傳送給用戶端,而且如果用戶端有名為 updateStockPrice 的方法,則會呼叫該方法,並將參數值傳遞給它。

    Clients.All 表示傳送至所有用戶端。 SignalR 提供其他選項,以指定要傳送的用戶端或用戶端群組。 如需詳細資訊,請參閱 HubConnectionCoNtext

註冊 SignalR 路由

伺服器必須知道要攔截並導向 SignalR 的 URL。 若要這樣做,您會將一些程式碼新增至 Global.asax 檔案。

  1. Solution Explorer中,以滑鼠右鍵按一下專案,然後按一下 [新增專案]。

  2. 選取 [全域應用程式類別 ] 專案範本,然後按一下 [ 新增]。

    新增 global.asax

  3. 將 SignalR 路由註冊程式碼新增至 Application_Start 方法:

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

    根據預設,所有 SignalR 流量的基底 URL 為 「/signalr」,而 「/signalr/hubs」 則用來擷取動態產生的 JavaScript 檔案,以定義您應用程式中所有中樞的 Proxy。 MapHubs 方法包含多載,可讓您在 HubConfiguration 類別的實例中指定不同的基底 URL 和特定 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 個數據行、標題列和資料列的資料表,其中包含跨越所有 5 個數據行的單一資料格。 資料列會顯示「載入...」和 只會在應用程式啟動時立即顯示。 JavaScript 程式碼會移除該資料列,並新增其位置資料列,其中包含從伺服器擷取的庫存資料。

    腳本標籤會指定 jQuery 腳本檔案、SignalR 核心腳本檔案、SignalR Proxy 腳本檔案,以及稍後您將建立的 StockTicker 腳本檔案。 SignalR Proxy 腳本檔案會動態產生並定義 Hub 類別上方法的 「/signalr/hubs」 URL,在此案例中為 StockTickerHub.GetAllStocks。 如果您想要的話,您可以使用 SignalR Utilities 手動產生此 JavaScript 檔案,並在 MapHubs 方法呼叫中停用動態檔案建立。

  3. 重要

    請確定 StockTicker.html 中的 JavaScript 檔案參考正確無誤。 也就是說,請確定腳本標籤中的 jQuery 版本 (1.8.2 中的範例) 與專案 [ 腳本 ] 資料夾中的 jQuery 版本相同,並確定腳本標籤中的 SignalR 版本與專案 [ 腳本 ] 資料夾中的 SignalR 版本相同。 如有必要,請變更腳本標籤中的檔案名。

  4. Solution Explorer中,以滑鼠右鍵按一下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 Proxy。 程式碼會取得 StockTickerHub 類別 Proxy 的參考,並將它放在刻度變數中。 Proxy 名稱是由 [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 函式,並使用伺服器傳回的資訊來更新股票表。 請注意,根據預設,您必須在用戶端上使用 camel 大小寫,不過方法名稱在伺服器上是 pascal 大小寫。 camel 大小寫規則僅適用于方法,不適用於 物件。 例如,您會參考股票。符號和股票。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();
    }
    

    如果您想要在用戶端上使用 pascal 大小寫,或想要使用完全不同的方法名稱,您可以使用 HubMethodName 屬性裝飾 Hub 方法,就像使用 HubName 屬性裝飾 Hub 類別本身的方式一樣。

    在 init 方法中,會針對從伺服器接收的每個股票物件建立 HTML,方法是呼叫 formatStock 來格式化 stock 物件的屬性,然後呼叫 supplant (,其定義于 StockTicker.js 頂端) ,以將 rowTemplate 變數中的預留位置取代為 stock 物件屬性值。 產生的 HTML 接著會附加至股票表。

    您可以藉由將 in 傳遞為非同步啟動函式完成之後執行的回呼函式來呼叫 init。 如果您在呼叫 start 之後呼叫 init 做為個別的 JavaScript 語句,函式會失敗,因為它會立即執行,而不需要等待 start 函式完成建立連線。 在此情況下,init 函式會嘗試在建立伺服器連接之前呼叫 getAllStocks 函式。

    當伺服器變更股票價格時,它會在連線的用戶端上呼叫 updateStockPrice。 函式會新增至 stockTicker Proxy 的用戶端屬性,以便可供伺服器呼叫。

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

    updateStockPrice 函式會將從伺服器接收的 stock 物件格式化為資料表資料列,方式與 init 函式相同。 不過,它不會將資料列附加至資料表,而是在資料表中尋找股票的目前資料列,並將該資料列取代為新的資料列。

測試應用程式

  1. 按 F5 以在偵錯模式中執行應用程式。

    股票表一開始會顯示「載入...」行,然後在短暫延遲之後顯示初始股票資料,然後股票價格開始變更。

    載入

    初始股票表

    從伺服器接收變更的庫存表

  2. 從瀏覽器網址列複製 URL,並將它貼到一或多個新的瀏覽器視窗, (s) 。

    初始股票顯示與第一個瀏覽器相同,同時發生變更。

  3. 關閉所有瀏覽器並開啟新的瀏覽器,然後移至相同的 URL。

    StockTicker 單一物件已在伺服器中繼續執行,因此股票表顯示顯示股票已繼續變更。 (您看不到具有零個變更圖的初始資料表。)

  4. 關閉瀏覽器。

啟用記錄

SignalR 具有內建的記錄函式,可讓您在用戶端上啟用,以協助進行疑難排解。 在本節中,您會啟用記錄,並查看示範記錄如何告訴您 SignalR 使用下列哪一種傳輸方法的範例:

針對任何指定的連線,SignalR 會選擇伺服器和用戶端支援的最佳傳輸方法。

  1. 開啟 StockTicker.js 並新增一行程式碼,以在初始化檔案結尾連接的程式碼之前立即啟用記錄:

    // Start the connection
    $.connection.hub.logging = true;
    $.connection.hub.start().done(init);
    
  2. 按 F5 執行專案。

  3. 開啟瀏覽器的開發人員工具視窗,然後選取 [主控台] 以查看記錄。 您可能必須重新整理頁面,以查看訊號交涉傳輸方法以進行新連線的記錄。

    如果您在 iis 8 Windows 8 () 上執行 Internet Explorer 10,傳輸方法是 WebSockets。

    IE 10 IIS 8 主控台

    如果您在 Windows 7 (IIS 7.5) 上執行 Internet Explorer 10,傳輸方法是 iframe。

    IE 10 主控台,IIS 7.5

    在 Firefox 中,安裝 Firebug 增益集以取得主控台視窗。 如果您在Windows 8 (IIS 8) 上執行 Firefox 19,傳輸方法是 WebSockets。

    Firefox 19 IIS 8 Websocket

    如果您在 Windows 7 (IIS 7.5) 上執行 Firefox 19,傳輸方法是伺服器傳送的事件。

    Firefox 19 IIS 7.5 主控台

安裝並檢閱完整的 StockTicker 範例

由 Microsoft.AspNet.SignalR.Sample NuGet 套件安裝的 StockTicker 應用程式包含的功能比您剛從頭建立的簡化版本還多。 在本教學課程的本節中,您會安裝 NuGet 套件,並檢閱新功能和實作它們的程式碼。

安裝 SignalR.Sample NuGet 套件

  1. [Solution Explorer] 中,以滑鼠右鍵按一下專案,然後按一下 [管理 NuGet 套件]。

  2. 在 [管理 NuGet 套件] 對話方塊中,按一下 [線上],在 [搜尋線上] 方塊中輸入SignalR.Sample,然後按一下SignalR.Sample套件中的[安裝]。

    安裝 SignalR.Sample 套件

  3. Global.asax 檔案中,將 RouteTable.Routes.MapHubs () 批註化;您稍早在 Application_Start 方法中新增的行。

    不再需要 Global.asax 中的程式碼,因為 SignalR.Sample 套件會在 App_Start/RegisterHubs.cs 檔案中註冊 SignalR 路由:

    [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 類別包含在 WebActivatorEx NuGet 套件中,該套件會安裝為 SignalR.Sample 套件的相依性。

  4. Solution Explorer中,展開透過安裝SignalR.Sample 套件所建立的 SignalR.Sample資料夾。

  5. SignalR.Sample 資料夾中,以滑鼠右鍵按一下 StockTicker.html,然後按一下 [ 設定為起始頁]。

    注意

    安裝 SignalR.Sample NuGet 套件可能會變更您在 Scripts 資料夾中擁有的 jQuery 版本。 套件安裝在 SignalR.Sample資料夾中的新StockTicker.html檔案會與套件安裝的 jQuery 版本同步,但如果您想要再次執行原始StockTicker.html檔案,您可能必須先更新腳本標記中的 jQuery 參考。

執行應用程式

  1. 按 F5 執行應用程式。

    除了您稍早看到的方格之外,完整的股票勾點器應用程式還會顯示水準捲動視窗,以顯示相同的股票資料。 當您第一次執行應用程式時,「市場」會「已關閉」,您會看到靜態方格和未捲動的刻度視窗。

    StockTicker 畫面開始

    當您按一下 [開啟市場] 時, [即時股票刻度] 方塊會開始水準捲動,而伺服器會開始隨機廣播股票價格變更。 每次股票價格變更時, 都會更新 [即時股票表 ] 方格和 [即時股票刻度] 方塊 。 當股票的價格變更為正數時,股票會以綠色背景顯示,而當變更為負數時,股票會以紅色背景顯示。

    StockTicker 應用程式,市場開放

    [關閉市場] 按鈕會停止變更並停止刻度捲動,而 [重設] 按鈕會在價格變更開始之前,將所有股票資料重設為初始狀態。 如果您開啟更多瀏覽器視窗並移至相同的 URL,您會在每個瀏覽器中同時看到相同的資料動態更新。 當您按一下其中一個按鈕時,所有瀏覽器都會同時以相同的方式回應。

即時股票刻度顯示

即時股票刻度顯示是 div 元素中的未排序清單,以 CSS 樣式格式化為單行。 刻度器會以與資料表相同的方式初始化和更新:藉由取代 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 類別會定義用戶端可以呼叫的四個額外方法:

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 會呼叫,以回應頁面頂端的按鈕。 它們示範一個用戶端觸發狀態變更的模式,該狀態會立即傳播至所有用戶端。 所有這些方法都會呼叫 StockTicker 類別中的方法,以影響市場狀態變更,然後廣播新狀態。

在 StockTicker 類別中,市場的狀態是由傳回 MarketState 列舉值的 MarketState 屬性所維護:

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

public enum MarketState
{
    Closed,
    Open
}

變更市場狀態的每個方法都會在鎖定區塊內執行,因為 StockTicker 類別必須是 threadsafe:

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();
    }
}

為了確保此程式碼為 threadsafe,可備份 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(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 為基礎的「射擊」遊戲

如需示範點對點通訊案例的教學課程,請參閱 開始使用 SignalR使用 SignalR 進行即時更新

若要深入瞭解進階 SignalR 開發概念,請流覽下列 SignalR 原始程式碼和資源的網站: