[渣譯文] SignalR 2.0 系列:SignalR的服務器廣播

英文渣水平,大夥湊合着看吧……css

這是微軟官方SignalR 2.0教程Getting Started with ASP.NET SignalR 2.0系列的翻譯,這裏是第八篇:SignalR的服務器廣播html

原文:Tutorial: Server Broadcast with SignalR 2.0jquery

概述

VS能夠經過 Microsoft.AspNet.SignalR.Sample NuGet包來安裝一個簡單的模擬股票行情應用。在本教程的第一部分,您將從頭開始建立一個應用程序的簡化版本。在本教程的剩餘部分,您將安裝NuGet包,審閱Sample中的一些附加功能。git

本模擬股票行情應用表明了實時應用中的"推",或稱之爲廣播,即咱們將消息通知傳播給全部已鏈接客戶端。程序員

第一步,您將要建立該應用程序的顯示錶格用於顯示數據。github

接下來,服務器會隨機更新股票價格,而且將新數據推送至全部鏈接的客戶端以更新表格。在瀏覽器中的表格上,價格及百分比列中的數字都會隨着服務器推送數據而自動更新。若是你打開更多的瀏覽器,它們都會顯示相同的數據及自動更新。web

注意:若是您你不想本身手動來構建這一應用程序,你能夠再一個新的空ASP.NET WEB應用項目中安裝Simple包,經過閱讀這些步驟來獲取代碼的解釋。本教程的第一部分涵蓋了Sample的子集,第二部分解釋了包中的一些附加功能。數據庫

建立項目

1.新建一個新的ASP.NET應用程序,命名爲SignalR.StockTicker並建立。後端

2.選擇空項目並肯定。設計模式

編寫服務器代碼

在本節中,咱們來編寫服務器端代碼。

建立Stock類

首先咱們來建立一個Stock模型類,用來存儲和傳輸股票信息。

1.新建一個類,命名爲Stock.cs,而後輸入如下代碼:

 1 using System;
 2 
 3 namespace SignalR.StockTicker
 4 {
 5     public class Stock
 6     {
 7         private decimal _price;
 8 
 9         public string Symbol { get; set; }
10 
11         public decimal Price
12         {
13             get
14             {
15                 return _price;
16             }
17             set
18             {
19                 if (_price == value)
20                 {
21                     return;
22                 }
23 
24                 _price = value;
25 
26                 if (DayOpen == 0)
27                 {
28                     DayOpen = _price;
29                 }
30             }
31         }
32 
33         public decimal DayOpen { get; private set; }
34 
35         public decimal Change
36         {
37             get
38             {
39                 return Price - DayOpen;
40             }
41         }
42 
43         public double PercentChange
44         {
45             get
46             {
47                 return (double)Math.Round(Change / Price, 4);
48             }
49         }
50     }
51 }

您設置了兩個屬性:股票代碼及價格。其餘的屬性則依賴於你如何及什麼時候設置股票價格。當您首次設訂價格時,價格將被存儲在DayOpen中。以後隨着股票價格的改變,Change和PercentChange會自動計算DayOpen及價格之間的差額並輸出結果。

建立StockTicker及StockTickerHub類

您將使用SignalR集線器類的API來處理服務器到客戶端的交互。StockTickerHub衍生自SignalR集線器基類,用來處理接收客戶端的鏈接和調用方法。你還須要維護存儲的數據,創建一個獨立於客戶端鏈接的Timer對象,來觸發價格更新。你不能將這些功能放在集線器中,由於每一個針對集線器的操做,好比從客戶端到服務器端的鏈接與調用都會創建一個集線器的新實例,每一個集線器的實例生存期是短暫的。所以,保存數據,價格,廣播等更新機制須要放在一個單獨的類中。在此項目中咱們將其命名爲StockTicker。

你只須要一個StockTicker類的實例。因此你須要使用設計模式中的單例模式,從每一個StockTickerHub的類中添加對StockTicker單一實例的引用。因爲StockTicker類包含股票數據並觸發更新,因此它必須可以廣播到每一個客戶端。但StockTicker自己並非一個集線器類,因此StockTicker類必須獲得一個SignalR集線器鏈接上下文對象的引用,以後就可使用這個上下文對象來將數據廣播給客戶端。

1.添加一個新的SignalR集線器類,命名爲StockTickerHub並使用如下的代碼替換其內容:

 1 using System.Collections.Generic;
 2 using Microsoft.AspNet.SignalR;
 3 using Microsoft.AspNet.SignalR.Hubs;
 4 
 5 namespace SignalR.StockTicker
 6 {
 7     [HubName("stockTickerMini")]
 8     public class StockTickerHub : Hub
 9     {
10         private readonly StockTicker _stockTicker;
11 
12         public StockTickerHub() : this(StockTicker.Instance) { }
13 
14         public StockTickerHub(StockTicker stockTicker)
15         {
16             _stockTicker = stockTicker;
17         }
18 
19         public IEnumerable<Stock> GetAllStocks()
20         {
21             return _stockTicker.GetAllStocks();
22         }
23     }
24 }

此集線器類用來定義用於客戶端調用的服務器方法。咱們定義了一個GetAllStocks方法,當一個客戶端首次鏈接至服務器時,它會調用此方法來獲取全部股票的清單及當期價格。該方法能夠同步執行並返回IEnumerable<Sotck>,由於這些數據是從內存中返回的。若是該方法須要作一些涉及等待的額外處理任務,好比數據庫查詢或調用Web服務來獲取數據,您將指定Task<IEnumerable<Stock>>做爲返回值已啓用異步處理。關於異步處理的更多信息,請參閱:ASP.NET SignalR Hubs API Guide - Server - When to execute asynchronously

HubName特性定義了客戶端的JS代碼使用何種名稱來調用集線器。若是你不使用這個特性,默認將經過採用使用Camel規範的類名來調用。在本例中,咱們使用stockTickerHun。

稍後咱們將建立StockTicker類,如您所見,咱們在這裏使用了單例模式。使用一個靜態實例屬性來建立這個類的單一實例。StockTicker的單例將一直保留在內存中,無論有多少客戶端鏈接或斷開鏈接。而且使用該實例中包含的GetAllStocks方法返回股票信息。

2.添加一個新類,命名爲StockTicker.cs,並使用如下代碼替換內容:

  1 using System;
  2 using System.Collections.Concurrent;
  3 using System.Collections.Generic;
  4 using System.Threading;
  5 using Microsoft.AspNet.SignalR;
  6 using Microsoft.AspNet.SignalR.Hubs;
  7 
  8 
  9 namespace SignalR.StockTicker
 10 {
 11     public class StockTicker
 12     {
 13         // Singleton instance
 14         private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));
 15 
 16         private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();
 17 
 18         private readonly object _updateStockPricesLock = new object();
 19 
 20         //stock can go up or down by a percentage of this factor on each change
 21         private readonly double _rangePercent = .002;
 22 
 23         private readonly TimeSpan _updateInterval = TimeSpan.FromMilliseconds(250);
 24         private readonly Random _updateOrNotRandom = new Random();
 25 
 26         private readonly Timer _timer;
 27         private volatile bool _updatingStockPrices = false;
 28 
 29         private StockTicker(IHubConnectionContext clients)
 30         {
 31             Clients = clients;
 32 
 33             _stocks.Clear();
 34             var stocks = new List<Stock>
 35             {
 36                 new Stock { Symbol = "MSFT", Price = 30.31m },
 37                 new Stock { Symbol = "APPL", Price = 578.18m },
 38                 new Stock { Symbol = "GOOG", Price = 570.30m }
 39             };
 40             stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock));
 41 
 42             _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
 43 
 44         }
 45 
 46         public static StockTicker Instance
 47         {
 48             get
 49             {
 50                 return _instance.Value;
 51             }
 52         }
 53 
 54         private IHubConnectionContext Clients
 55         {
 56             get;
 57             set;
 58         }
 59 
 60         public IEnumerable<Stock> GetAllStocks()
 61         {
 62             return _stocks.Values;
 63         }
 64 
 65         private void UpdateStockPrices(object state)
 66         {
 67             lock (_updateStockPricesLock)
 68             {
 69                 if (!_updatingStockPrices)
 70                 {
 71                     _updatingStockPrices = true;
 72 
 73                     foreach (var stock in _stocks.Values)
 74                     {
 75                         if (TryUpdateStockPrice(stock))
 76                         {
 77                             BroadcastStockPrice(stock);
 78                         }
 79                     }
 80 
 81                     _updatingStockPrices = false;
 82                 }
 83             }
 84         }
 85 
 86         private bool TryUpdateStockPrice(Stock stock)
 87         {
 88             // Randomly choose whether to update this stock or not
 89             var r = _updateOrNotRandom.NextDouble();
 90             if (r > .1)
 91             {
 92                 return false;
 93             }
 94 
 95             // Update the stock price by a random factor of the range percent
 96             var random = new Random((int)Math.Floor(stock.Price));
 97             var percentChange = random.NextDouble() * _rangePercent;
 98             var pos = random.NextDouble() > .51;
 99             var change = Math.Round(stock.Price * (decimal)percentChange, 2);
100             change = pos ? change : -change;
101 
102             stock.Price += change;
103             return true;
104         }
105 
106         private void BroadcastStockPrice(Stock stock)
107         {
108             Clients.All.updateStockPrice(stock);
109         }
110 
111     }
112 }

因爲運行時會有多個線程對StockTicker的同一個實例進行操做,StockTicker類必須是線程安全的。

在靜態字段中存儲單例

下面的代碼用於在靜態_instance字段中初始化一個StockTicker的實例。這是該類的惟一一個實例,由於構造函數已經被標記爲私有的。_instance中的延遲初始化不是因爲性能緣由,而是要確保該線程的建立是線程安全的。

1 private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));
2 
3 public static StockTicker Instance
4 {
5     get
6     {
7         return _instance.Value;
8     }
9 }

每次客戶端鏈接到服務器時,都會在單獨的一個線程中建立StockTickerHub的新實例,以後從StockTicker.Instance靜態屬性中獲取StockTicker的單例,如同你以前在StockTickerHub以前見到的那樣。

在ConcurrentDictory中存放股票數據

構造函數初始化了_stock集合而且初始化了一些樣本數據並使用GetAllStocks返回股票數據。如前所述,客戶端能夠調用服務器端StockTickerHub集線器中的GetAllStocks方法用來返回股票數據集合到客戶端。

 1 private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();
 2 private StockTicker(IHubConnectionContext clients)
 3 {
 4     Clients = clients;
 5 
 6     _stocks.Clear();
 7     var stocks = new List<Stock>
 8     {
 9         new Stock { Symbol = "MSFT", Price = 30.31m },
10         new Stock { Symbol = "APPL", Price = 578.18m },
11         new Stock { Symbol = "GOOG", Price = 570.30m }
12     };
13     stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock));
14 
15     _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
16 }
17 
18 public IEnumerable<Stock> GetAllStocks()
19 {
20     return _stocks.Values;
21 }

股票集合被定義爲一個ConcurrentDictionary類以確保線程安全。做爲替代,你可使用Dictionary對象並在對其進行修改時顯式的鎖定它來確保線程安全。

對於本示例,股票數據都存儲在內存中,因此當應用程序重啓時你會丟失全部的數據。在實際的應用中,你應該將數據安全的存放在後端(好比SQL數據庫中)。

按期更新股票價格

構造函數啓動一個定時器來按期更新股票數據,股價以隨機抽樣的方式來隨機變動。

 1 _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
 2 
 3 private void UpdateStockPrices(object state)
 4 {
 5     lock (_updateStockPricesLock)
 6     {
 7         if (!_updatingStockPrices)
 8         {
 9             _updatingStockPrices = true;
10 
11             foreach (var stock in _stocks.Values)
12             {
13                 if (TryUpdateStockPrice(stock))
14                 {
15                     BroadcastStockPrice(stock);
16                 }
17             }
18 
19             _updatingStockPrices = false;
20         }
21     }
22 }
23 
24 private bool TryUpdateStockPrice(Stock stock)
25 {
26     // Randomly choose whether to update this stock or not
27     var r = _updateOrNotRandom.NextDouble();
28     if (r > .1)
29     {
30         return false;
31     }
32 
33     // Update the stock price by a random factor of the range percent
34     var random = new Random((int)Math.Floor(stock.Price));
35     var percentChange = random.NextDouble() * _rangePercent;
36     var pos = random.NextDouble() > .51;
37     var change = Math.Round(stock.Price * (decimal)percentChange, 2);
38     change = pos ? change : -change;
39 
40     stock.Price += change;
41     return true;
42 }

定時器會定時調用UpdateStockPrices方法,在更新價格以前,_updateStockPricesLock對象被鎖住。代碼檢查是否有另外一個線程在更新價格,而後調用TryUpdateStockPrice方法來對列表中的股票進行逐一更新。TryUpdateStockPrice方法將判斷是否須要更新股價以及更新多少。若是股票價格發生變化,BroadcastPrice方法將變更的數據廣播到全部已鏈接的客戶端上。

_updateStockPrices標識被標記爲volatile以確保訪問是線程安全的。

private volatile bool _updatingStockPrices = false;

在實際應用中,TryUpdateStockPrice方法可能會調用Web服務來查找股價;在本示例中,它使用一個隨機數來模擬股價的變化。

獲取SignalR上下文,以便StockTicker類對其調用來廣播到客戶端

因爲價格變更發生於StockTicker對象,該對象須要在全部已鏈接客戶端上調用updateStockPrice方法。在集線器類中,你有現成的API來調用客戶端方法。但StockTicker類沒有從集線器類派生,因此沒有引用到集線器的基類對象。所以,爲了對客戶端廣播,StockTicker類須要獲取SignalR上下文的實例並用它來調用客戶端上的方法。

該代碼會在建立單例的時候獲取SignalR上下文的引用,將引用傳遞給構造函數,使構造函數可以將它放置在Clients屬性中。

有兩個緣由使你只應該獲得一次上下文:獲取上下文是一個昂貴的操做,而且僅得到一次能夠確保發送到客戶端的消息順序是有序的。

 1 private readonly static Lazy<StockTicker> _instance =
 2     new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));
 3 
 4 private StockTicker(IHubConnectionContext clients)
 5 {
 6     Clients = clients;
 7 
 8     // Remainder of constructor ...
 9 }
10 
11 private IHubConnectionContext Clients
12 {
13     get;
14     set;
15 }
16 
17 private void BroadcastStockPrice(Stock stock)
18 {
19     Clients.All.updateStockPrice(stock);
20 }

獲取上下文中的Client屬性,這樣可讓你編寫代碼呼叫客戶端方法,就如同你在集線器類中那樣。例如,若是想廣播到全部客戶端,你能夠寫Clients.All.updateStockprice(stock)。

你在BroadcastStockPrice中調用的updateStockPrice客戶端方法還不存在,稍後咱們會在編寫客戶端代碼時加上它。但如今你就能夠在這裏引用updateStockPrice,這是由於Clients.All是動態的,這意味着該表達式將在運行時進行評估。當這個方法被執行,SignalR將發送方法名和參數給客戶端,若是客戶端可以匹配到相同名稱的方法,該方法會被調用,參數也將被傳遞給它。

Client.All意味着將把消息發送到所有客戶端。SignalR也一樣給你提供了其餘選項來選擇指定客戶端或羣組。請參閱HubConnectionContext

註冊SignalR路由

服務器須要知道那個URL用於攔截並指向SignalR,咱們將添加OWIN啓動類來實現。

1.添加一個OWIN啓動類,並命名爲Startup.cs。

2.使用下面的代碼替換Startup.cs中的內容:

 1 using System;
 2 using System.Threading.Tasks;
 3 using Microsoft.Owin;
 4 using Owin;
 5 
 6 [assembly: OwinStartup(typeof(SignalR.StockTicker.Startup))]
 7 
 8 namespace SignalR.StockTicker
 9 {
10     public class Startup
11     {
12         public void Configuration(IAppBuilder app)
13         {
14             // Any connection or hub wire up and configuration should go here
15             app.MapSignalR();
16         }
17 
18     }
19 }

如今你已經完成了所有的服務器端代碼,接下來咱們將配置客戶端。

配置客戶端代碼

1.新建一個HTML文檔,命名爲StockTicker.html。

2.使用下面的代碼替換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.0.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中建立了一個具備5列,一個標題和跨越全部5列的單個單元格的Table,數據行顯示爲「正在加載」,而且只會在應用程序啓動時一度顯示。JS代碼將會刪除改行並在相同的衛視添加從服務器檢索到的股票數據。

script標籤指定了jQuery腳本文件,SignalR核心腳本文件,SignalR代理腳本文件以及你即將建立的StockTicker腳本文件。在SignalR代理腳本文件中,指定了"/signalr/hub"URL,這是動態生成的,是集線器方法中定義好的方法的代理方法。在本示例中爲StockTickerHub.GetAllStocks。若是你願意,你能夠手動生成該JS文件,經過使用SignalR 組件和在調用MapHubs方法時禁用動態文件建立來實現相同的功能。

3.重要提示:請確保JS文件都獲得了正確的引用,即檢查script標籤中引用的jQuery等文件路徑和你項目中的JS腳本文件名稱一致。

4.右擊StockTicker.html,將其設置爲起始頁。

5.在項目文件夾中建立一個新的JS文件,命名爲StockTicker.js並保存。

6.使用下面的代碼替換掉StockTicker.js文件中的內容:

 1 // A simple templating method for replacing placeholders enclosed in curly braces.
 2 if (!String.prototype.supplant) {
 3     String.prototype.supplant = function (o) {
 4         return this.replace(/{([^{}]*)}/g,
 5             function (a, b) {
 6                 var r = o[b];
 7                 return typeof r === 'string' || typeof r === 'number' ? r : a;
 8             }
 9         );
10     };
11 }
12 
13 $(function () {
14 
15     var ticker = $.connection.stockTickerMini, // the generated client-side hub proxy
16         up = '▲',
17         down = '▼',
18         $stockTable = $('#stockTable'),
19         $stockTableBody = $stockTable.find('tbody'),
20         rowTemplate = '<tr data-symbol="{Symbol}"><td>{Symbol}</td><td>{Price}</td><td>{DayOpen}</td><td>{Direction} {Change}</td><td>{PercentChange}</td></tr>';
21 
22     function formatStock(stock) {
23         return $.extend(stock, {
24             Price: stock.Price.toFixed(2),
25             PercentChange: (stock.PercentChange * 100).toFixed(2) + '%',
26             Direction: stock.Change === 0 ? '' : stock.Change >= 0 ? up : down
27         });
28     }
29 
30     function init() {
31         ticker.server.getAllStocks().done(function (stocks) {
32             $stockTableBody.empty();
33             $.each(stocks, function () {
34                 var stock = formatStock(this);
35                 $stockTableBody.append(rowTemplate.supplant(stock));
36             });
37         });
38     }
39 
40     // Add a client-side hub method that the server will call
41     ticker.client.updateStockPrice = function (stock) {
42         var displayStock = formatStock(stock),
43             $row = $(rowTemplate.supplant(displayStock));
44 
45         $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
46             .replaceWith($row);
47         }
48 
49     // Start the connection
50     $.connection.hub.start().done(init);
51 
52 });

$.connection引用SignalR代理,來獲取引用到代理類的StockTickerHub類,並放置在ticker變量中。代理名稱是由HubName特性所指定的。

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

當全部變量及函數都定義完成以後,代碼文件中的最後一行經過調用SignalR start函數來初始化SignalR鏈接。start函數將異步執行並返回一個jQuery的遞延對象,這意味着你能夠在異步操做後調用函數來完成指定的功能。

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

init函數調用服務器上的getAllStocks方法,並使用服務器返回的數據來更新股票表格中的信息。請注意,在默認狀況下你必須在客戶端上使用camel命名規範來調用服務器端的Pascal命名規範的方法。另外camel命名規範僅適用於方法而不是對象。例如要使用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();
}

若是你想在客戶端上使用Pascal命名規範,或者你想使用一個徹底不一樣的方法名,你可使用HubMethodName特性來修飾集線器方法, 如同使用HubName來修飾集線器類同樣。

在init方法中,接收到從服務器傳來股票信息後,會清除table row的HTML,而後經過ormatStock來格式化股票對象,以後將其附加到表格中。

在執行異步啓動函數後 ,做爲回調函數,調用init方法。若是你將init做爲單獨的JS對象在start函數中調用,函數將會失敗,由於它會當即執行而不會等待啓動功能來完成鏈接。在本例中,init函數會在服務器鏈接創建後再去調用getAllStocks函數。

當服務器改變了股票的價格,它調用已鏈接客戶端的updateStockPrice。該函數被添加到stockTicker代理的客戶端屬性中,使其能夠從服務器端調用。

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

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

如同inti函數同樣,updateStockPrice函數格式化從服務器接收到的股票對象並插入表格中。而不是附加到表格的行後面,它會發現當前表格中的股票行並使用新的數據替換掉。

測試應用程序

 1.按下F5啓動應用程序。原文有問題,這裏建議使用右擊HTML文檔,而後選擇在瀏覽器中查看,不然第3步關閉瀏覽器後就中止調試了,沒法看到單例模式的效果。

表格最初將顯示「正在加載」,在初始化股票數據後,顯示最初的股票價格,以後便會隨着股價變更而開始改變。

2.複製多個瀏覽器窗口,你會看到同第一步同樣的狀況,以後全部瀏覽器會同時根據股價發生變化。

3.關閉全部瀏覽器,再打開一個新的,打開相同的URL你會看到股票價格仍在改變(你看不到初始化時表顯示初始股價的數字及信息),這是因爲stockTicker單例繼續在服務器上運行。

4.關閉瀏覽器。

啓用日誌記錄

SignalR有一個內置的日誌功能,您能夠啓動它以便進行故障排除,本節咱們將展現這一功能。

關於SignalR針對IIS及瀏覽器所不一樣的傳輸方式,請參見前幾章教程。

1.打開stockTicker.js並添加一行代碼來啓動日誌。

// Start the connection
$.connection.hub.logging = true;
$.connection.hub.start().done(init);

2.按下F5開始運行項目。

3.打開瀏覽器中的開發者工具,可能須要刷新頁面創建一個新鏈接才能看到SignalR的傳輸方式。

 安裝並檢視完整版StockTicker示例

你剛纔建立的只是一個簡化版的StockTicker應用,在本節教程中,您將安裝NuGet包來獲取一個完整功能的StockTicker。

安裝NuGet包

1.在解決方案資源管理器中右擊該項目,而後單擊管理NuGet程序包。

2.在管理NuGet程序包對話框中,單機聯機,而後再搜索框中輸入SignalR.Sample,找到Microsoft.AspNet.SignalR.Sample,安裝它。

3.在解決方案資源管理器中,展開SignalR.Sample文件夾。

4.右鍵單擊SignalR.Sample文件夾下的StockTicker.html,將其設置爲起始頁。

注意:安裝Sample可能會改變jQuery,SignalR等包的版本,若是你想運行以前你建立的StockTicker,你須要打開HTML並覈對引用的JS文件是否同Sctipts文件夾中的腳本版本一致。

運行應用程序

1.按下F5運行應用程序。

注意:若是提示以下的錯誤,請升級相應的NuGet包到指定版本。

 

若是程序正常運行,除了您以前看到的包含股票信息的表格,還會有一條水平滾動的窗口來顯示實時股價,如同大多數股票市場裏的那樣。當你首次運行應用程序時,市場是關閉的(注意那個按鈕),你會看到一個靜態的表格和股票窗口。

當你單擊開市按鈕,實時股價框開始水平移動,而且服務器開始週期性地廣播股價變更,每次股價的變化都會引發表格及水平框中數字的更新。當股價變化爲正時,會顯示一個綠色的背景,爲負時則顯示紅色。

閉市按鈕將中止變化,終止股票滾動,重設按鈕將復位全部的股價到開始變更前的初始狀態。若是你打開更多瀏覽器窗口,你將在窗口中看到相同的變化。

實時股票行情顯示器

實時股票行情顯示器是一個無序列表,放置在一個div元素中並由css格式化爲單行顯示。如同表格同樣,它也被初始化和更新:經過替換在li標籤之間的佔位符及動態添加li元素到ul元素中。滾動是經過使用jQuery的animate函數來實現的。

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

使它滾動起來的JS:

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屬性來維護。

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屬性後的_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中的新函數啓用或禁用市場狀態按鈕,他們中止或啓動股票窗口的水平滾動。因爲多個函數被添加到客戶端,咱們使用了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();
        });
    });

在鏈接創建之前,服務器方法不會和按鈕動做進行鏈接,因此代碼不會在它們以前的時候去嘗試調用服務器方法。

接下來

在本教程中,您學會了如何編寫廣播來將服務器消息傳遞給全部客戶端,包括週期及通知響應。採用多線程單例模式來維持服務器的狀態,也能夠同時使用在多用戶在線的遊戲場景中,有關示例請參閱the ShootR game that is based on SignalR

 

做者:

帕特里克·弗萊徹 -帕特里克·弗萊徹是ASP.NET開發團隊的程序員,做家,目前正在SignalR項目工做。

湯姆·戴卡斯特拉 -湯姆·戴卡斯特拉是微軟Web平臺及工具團隊的高級程序員,做家。

相關文章
相關標籤/搜索