教學課程:使用 SignalR 2 進行伺服器廣播

警告

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

本教學課程說明如何建立使用 ASP.NET SignalR 2 提供伺服器廣播功能的 Web 應用程式。 伺服器廣播表示伺服器會啟動傳送給用戶端的通訊。

您將在本教學課程中建立的應用程式會模擬股票勾號,這是伺服器廣播功能的一般案例。 伺服器會定期隨機更新股票價格,並將更新廣播給所有連線的用戶端。 在瀏覽器中, 變更 和資料 % 行中的數位和符號會動態變更,以回應來自伺服器的通知。 如果您將其他瀏覽器開啟至相同的 URL,它們都會同時顯示相同的資料和相同的資料變更。

顯示多個網頁瀏覽器如何同時顯示相同更新資料的螢幕擷取畫面。

在本教學課程中,您:

  • 建立專案
  • 設定伺服器程式碼
  • 檢查伺服器程式碼
  • 設定用戶端程式代碼
  • 檢查用戶端程式代碼
  • 測試應用程式
  • 啟用記錄

重要

如果您不想執行建置應用程式的步驟,可以在新的空白 ASP.NET Web 應用程式專案中安裝 SignalR.Sample 套件。 如果您安裝 NuGet 套件而不執行本教學課程中的步驟,您必須遵循 readme.txt 檔案中的指示。 若要執行封裝,您需要新增 OWIN 啟動類別,以呼叫 ConfigureSignalR 已安裝套件中的 方法。 如果您未新增 OWIN 啟動類別,您會收到錯誤。 請參閱本文 的安裝 StockTicker 範例 一節。

必要條件

建立專案

本節說明如何使用 Visual Studio 2017 建立空的 ASP.NET Web 應用程式。

  1. 在 Visual Studio 中,建立 ASP.NET Web 應用程式。

    顯示如何建立 ASP.NET Web 應用程式的螢幕擷取畫面。

  2. 在 [ 新增 ASP.NET Web 應用程式 - SignalR.StockTicker ] 視窗中,保留 [ 空白 ] 並選取 [ 確定]。

設定伺服器程式碼

在本節中,您會設定在伺服器上執行的程式碼。

建立 Stock 類別

首先,您會建立將用來儲存及傳輸股票相關資訊的 Stock 模型類別。

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

  2. 將類別命名為 Stock ,並將其新增至專案。

  3. 以下列程式碼取代 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 來計算 ChangePercentChangeDayOpen 屬性值。

建立 StockTickerHub 和 StockTicker 類別

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

從 StockTicker 廣播

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

建立 StockTickerHub.cs

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

  2. [新增專案 - SignalR.StockTicker] 中,選取[已安裝>的 Visual C#>Web>SignalR],然後選取[SignalR 中樞類別] (v2)

  3. 將類別命名為 StockTickerHub ,並將它新增至專案。

    此步驟會建立 StockTickerHub.cs 類別檔案。 同時,它會將一組支援 SignalR 的腳本檔案和元件參考新增至專案。

  4. 以下列程式碼取代 StockTickerHub.cs 檔案中的程式碼:

    using System.Collections.Generic;
    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();
            }
        }
    }
    
  5. 儲存檔案。

應用程式會使用 Hub 類別來定義用戶端可以在伺服器上呼叫的方法。 您正在定義一個方法: GetAllStocks() 。 當用戶端一開始連線到伺服器時,它會呼叫這個方法,以取得所有股票及其目前價格的清單。 方法可以同步執行並傳回 IEnumerable<Stock> ,因為它會從記憶體傳回資料。

如果 方法必須執行涉及等候的專案來取得資料,例如資料庫查閱或 Web 服務呼叫,您可以指定 Task<IEnumerable<Stock>> 做為傳回值,以啟用非同步處理。 如需詳細資訊,請參閱 ASP.NET SignalR Hubs API 指南 - 伺服器 - 非同步執行時機

屬性 HubName 會指定應用程式如何在用戶端上的 JavaScript 程式碼中參考中樞。 如果您未使用此屬性,則用戶端上的預設名稱是類別名稱的 camelCase 版本,在此案例中為 stockTickerHub

如您稍後在建立 StockTicker 類別時所見,應用程式會在其靜態 Instance 屬性中建立該類別的單一實例。 不論有多少用戶端連線或中斷連線,該單一實例 StockTicker 都位於記憶體中。 該實例是 方法用來傳回目前股票資訊的內容 GetAllStocks()

建立 StockTicker.cs

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

  2. 將類別命名為 StockTicker ,並將它新增至專案。

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

將庫存資料儲存在 ConcurrentDictionary 中

建構函式會使用一些範例股票資料來初始化 _stocks 集合,並 GetAllStocks 傳回股票。 如先前所見,這個股票集合會由 傳 StockTickerHub.GetAllStocks 回,這是用戶端可以呼叫之 類別中的 Hub 伺服器方法。

private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();
private StockTicker(IHubConnectionContext<dynamic> 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;
}

股票集合定義為 Thread Safety 的 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;
}

Timer 會呼叫 UpdateStockPrices ,這會在 state 參數中傳入 null。 更新價格之前,應用程式會鎖定 _updateStockPricesLock 物件。 程式碼會檢查另一個執行緒是否已更新價格,然後在清單中的每個股票上呼叫 TryUpdateStockPrice 。 方法 TryUpdateStockPrice 會決定是否要變更股票價格,以及變更多少。 如果股票價格變更,應用程式會呼叫 BroadcastStockPrice 來廣播所有已連線用戶端的股票價格變更。

指定 _updatingStockPrices為 volatile 的旗標,以確定它是安全線程。

private volatile bool _updatingStockPrices = false;

在實際的應用程式中,方法 TryUpdateStockPrice 會呼叫 Web 服務來查閱價格。 在此程式碼中,應用程式會使用亂數產生器來隨機進行變更。

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

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

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

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

private readonly static Lazy<StockTicker> _instance =
    new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));

private StockTicker(IHubConnectionContext<dynamic> clients)
{
    Clients = clients;

    // Remainder of constructor ...
}

private IHubConnectionContext<dynamic> Clients
{
    get;
    set;
}

private void BroadcastStockPrice(Stock stock)
{
    Clients.All.updateStockPrice(stock);
}

Clients取得內容的 屬性,並將它放在 屬性中 StockTickerClient ,可讓您撰寫程式碼來呼叫看起來與類別中 Hub 相同之用戶端方法。 例如,若要廣播至所有用戶端,您可以撰寫 Clients.All.updateStockPrice(stock)

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

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

註冊 SignalR 路由

伺服器必須知道要攔截並導向 SignalR 的 URL。 若要這樣做,請新增 OWIN 啟動類別:

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

  2. [新增專案 - SignalR.StockTicker] 中,選取[已安裝>的 Visual C#>Web],然後選取[OWIN 啟動類別]。

  3. 將類別命名為 Startup ,然後選取 [ 確定]。

  4. 以下列程式碼取代 Startup.cs 檔案中的預設程式碼:

    using System;
    using System.Threading.Tasks;
    using Microsoft.Owin;
    using Owin;
    
    [assembly: OwinStartup(typeof(SignalR.StockTicker.Startup))]
    
    namespace SignalR.StockTicker
    {
        public class Startup
        {
            public void Configuration(IAppBuilder app)
            {
                // Any connection or hub wire up and configuration should go here
                app.MapSignalR();
            }
    
        }
    }
    

您現在已完成伺服器程式碼的設定。 在下一節中,您將設定用戶端。

設定用戶端程式代碼

在本節中,您會設定在用戶端上執行的程式碼。

建立 HTML 頁面和 JavaScript 檔案

HTML 頁面會顯示資料,而 JavaScript 檔案會組織資料。

建立StockTicker.html

首先,您將新增 HTML 用戶端。

  1. [Solution Explorer] 中,以滑鼠右鍵按一下專案,然後選取 [新增>HTML 頁面]。

  2. 將檔案命名為 StockTicker ,然後選取 [ 確定]。

  3. 以下列程式碼取代 StockTicker.html 檔案中的預設程式碼:

    <!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.10.2.min.js" ></script>
        <!--Reference the SignalR library. -->
        <script src="/Scripts/jquery.signalR-2.1.0.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 會建立一個資料表,其中包含五個數據行、一個標題資料列,以及一個包含全部五個數據行的單一資料格的資料列。 資料列會顯示「正在載入...」應用程式啟動時的暫時性。 JavaScript 程式碼會移除該資料列,並在其位置資料列中新增,其中包含從伺服器擷取的庫存資料。

    腳本標籤會指定:

    • jQuery 腳本檔案。

    • SignalR 核心腳本檔案。

    • SignalR Proxy 腳本檔案。

    • 稍後您將建立的 StockTicker 腳本檔案。

    應用程式會動態產生 SignalR Proxy 腳本檔案。 它會指定 「/signalr/hubs」 URL,並針對 中樞類別上的方法定義 Proxy 方法,在此案例中為 StockTickerHub.GetAllStocks 。 如果您想要的話,您可以使用 SignalR Utilities手動產生此 JavaScript 檔案。 別忘了在方法呼叫中 MapHubs 停用動態檔案建立。

  4. [Solution Explorer] 中,展開 [腳本]。

    jQuery 和 SignalR 的腳本程式庫會顯示在專案中。

    重要

    套件管理員會安裝較新版本的 SignalR 腳本。

  5. 更新程式碼區塊中的腳本參考,以對應至專案中腳本檔案的版本。

  6. Solution Explorer中,以滑鼠右鍵按一下StockTicker.html,然後選取 [設定為起始頁]。

建立StockTicker.js

現在建立 JavaScript 檔案。

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

  2. 將檔案命名為 StockTicker ,然後選取 [ 確定]。

  3. 將此程式碼新增至 StockTicker.js 檔案:

    // 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。 程式碼會取得 類別 Proxy 的 StockTickerHub 參考,並將它放入 變數中 ticker 。 Proxy 名稱是 屬性所 HubName 設定的名稱:

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

定義所有變數和函式之後,檔案中的最後一行程式碼會呼叫 SignalR 函式來初始化 SignalR start 連線。 函式會 start 以非同步方式執行,並傳回 jQuery Deferred 物件。 您可以呼叫 done 函式,以指定要在應用程式完成非同步動作時呼叫的函式。

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

取得所有股票

函式會在伺服器上呼叫 函 init 式, getAllStocks 並使用伺服器傳回的資訊來更新股票表。 請注意,根據預設,即使方法名稱在伺服器上是 pascal 大小寫,您還是必須在用戶端上使用 camelCasing。 camelCasing 規則僅適用于方法,不適用於 物件。 例如,您是指 stock.Symbolstock.Price ,而不是 stock.symbolstock.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();
}

在 方法中 init ,應用程式會針對從伺服器接收的每個股票物件建立資料表資料列的 HTML,方法是呼叫 formatStock 來格式化物件的屬性 stock ,然後將 supplant 變數中的 rowTemplate 預留位置取代為 stock 物件屬性值。 產生的 HTML 接著會附加至股票表。

注意

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

取得更新的股票價格

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

ticker.client.updateStockPrice = function (stock) {
    var displayStock = formatStock(stock),
        $row = $(rowTemplate.supplant(displayStock));

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

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

測試應用程式

您可以測試應用程式,以確定其運作正常。 您會看到所有瀏覽器視窗顯示即時股票資料表,股票價格變動。

  1. 在工具列中,開啟 [腳本偵 錯],然後選取 [播放] 按鈕,以偵錯模式執行應用程式。

    使用者開啟偵錯模式並選取 [播放] 的螢幕擷取畫面。

    瀏覽器視窗隨即開啟,顯示 即時股票表。 股票表一開始會顯示「正在載入...」然後,在短時間內,應用程式會顯示初始股票資料,然後股票價格開始變更。

  2. 從瀏覽器複製 URL、開啟其他兩個瀏覽器,然後將 URL 貼到網址列。

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

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

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

  4. 關閉瀏覽器。

啟用記錄

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

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

  1. 開啟 StockTicker.js

  2. 新增此醒目提示的程式程式碼,以在初始化檔案結尾之連線的程式碼之前立即啟用記錄:

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

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

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

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

    • 如果您在 IIS 8) Windows 8 (上執行 Firefox 19,傳輸方法是WebSockets

      提示

      在 Firefox 中,安裝 Firebug 增益集以取得主控台視窗。

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

安裝 StockTicker 範例

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

重要

如果您安裝套件而不執行本教學課程的先前步驟,則必須將 OWIN 啟動類別新增至您的專案。 NuGet 套件的這個readme.txt檔案說明此步驟。

安裝 SignalR.Sample NuGet 套件

  1. 在 [方案總管] 中,以滑鼠右鍵按一下專案,然後選取 [管理 NuGet 套件]。

  2. NuGet 套件管理員:SignalR.StockTicker中,選取 [流覽]。

  3. [套件來源] 中,選取 [nuget.org]。

  4. 在搜尋方塊中輸入SignalR.Sample,然後選取[Microsoft.AspNet.SignalR.Sample>Install]。

  5. Solution Explorer中,展開SignalR.Sample資料夾。

    安裝 SignalR.Sample 套件會建立資料夾及其內容。

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

    注意

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

執行應用程式

您在第一個應用程式中看到的資料表具有實用的功能。 完整的股票刻度應用程式會顯示新功能:水準捲動視窗,顯示隨著股票資料增加和下降而變更色彩的股票和股票。

  1. 按下 F5 以執行應用程式。

    當您第一次執行應用程式時,「市場」是「已關閉」,您會看到靜態資料表和未捲動的刻度視窗。

  2. 選取 [開啟市場]。

    即時刻度器的螢幕擷取畫面。

    • [即時股票刻度]方塊會開始水準捲動,而伺服器會隨機開始定期廣播股票價格變更。

    • 每次股票價格變更時,應用程式都會更新 Live Stock TableLive Stock Ticker

    • 當股票的價格變更為正數時,應用程式會顯示具有綠色背景的股票。

    • 當變更為負數時,應用程式會顯示紅色背景的股票。

  3. 選取 [關閉市場]。

    • 資料表會停止更新。

    • 刻度器會停止捲動。

  4. 選取 [重設]

    • 所有股票資料都會重設。

    • 應用程式會在價格變更開始之前還原初始狀態。

  5. 從瀏覽器複製 URL,開啟其他兩個瀏覽器,然後將 URL 貼到網址列。

  6. 您會在每個瀏覽器中同時看到動態更新的相同資料。

  7. 當您選取任何控制項時,所有瀏覽器都會同時回應相同的方式。

即時股票刻度顯示

即時股票刻度顯示是依 CSS 樣式格式化為單行的未排序清單 <div> 。 應用程式會以與資料表相同的方式初始化和更新刻度器:藉由取代範本字串中的 <li> 預留位置,並動態地將 <li> 元素新增至 <ul> 元素。 應用程式包含使用 jQuery animate 函式捲動,以變更 內 <div> 未排序清單的邊界。

SignalR.Sample StockTicker.html

股票刻度器 HTML 程式碼:

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

SignalR.Sample StockTicker.css

股票刻度器 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;
    }

SignalR.Sample SignalR.StockTicker.js

讓其捲動的 jQuery 程式碼:

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

用戶端可以呼叫之伺服器上的其他方法

若要將彈性新增至應用程式,應用程式可以呼叫的其他方法。

SignalR.Sample StockTickerHub.cs

類別 StockTickerHub 會定義用戶端可以呼叫的四個額外方法:

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

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

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

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

應用程式會呼叫 OpenMarketCloseMarketReset ,以回應頁面頂端的按鈕。 其示範一個用戶端觸發狀態變更的模式,會立即傳播到所有用戶端。 所有這些方法都會在 類別中 StockTicker 呼叫會導致市場狀態變更的方法,然後廣播新的狀態。

SignalR.Sample StockTicker.cs

StockTicker在 類別中,應用程式會使用 MarketState 傳回 MarketState 列舉值的屬性來維護市場的狀態:

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

public enum MarketState
{
    Closed,
    Open
}

變更市場狀態的每個方法都會在鎖定區塊內執行,因為 類別 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 備份所 volatile 指定屬性的 MarketState 欄位:

private volatile MarketState _marketState;

BroadcastMarketStateChangeBroadcastMarketReset 方法類似于您已經看到的 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();
    }
});

建立連線之後的其他用戶端設定

用戶端建立連線之後,會執行一些額外的工作:

  • 瞭解市場是否已開啟或關閉,以呼叫適當的 marketOpenedmarketClosed 函式。

  • 將伺服器方法呼叫附加至按鈕。

$.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 的詳細資訊,請參閱下列資源:

後續步驟

在本教學課程中,您:

  • 已建立專案
  • 設定伺服器程式碼
  • 檢查伺服器程式碼
  • 設定用戶端程式代碼
  • 檢查用戶端程式代碼
  • 測試應用程式
  • 已啟用記錄

請前往下一篇文章,瞭解如何建立使用 ASP.NET SignalR 2 的即時 Web 應用程式。