英文渣水平,大伙湊合着看吧……
這是微軟官方SignalR 2.0教程Getting Started with ASP.NET SignalR 2.0系列的翻譯,這里是第八篇:SignalR的服務器廣播
原文:Tutorial: Server Broadcast with SignalR 2.0
概述
VS可以通過 Microsoft.AspNet.SignalR.Sample NuGet包來安裝一個簡單的模擬股票行情應用。在本教程的第一部分,您將從頭開始創建一個應用程序的簡化版本。在本教程的剩余部分,您將安裝NuGet包,審閱Sample中的一些附加功能。
本模擬股票行情應用代表了實時應用中的"推",或稱之為廣播,即我們將消息通知傳播給所有已連接客戶端。
第一步,您將要創建該應用程序的顯示表格用於顯示數據。
接下來,服務器會隨機更新股票價格,並且將新數據推送至所有連接的客戶端以更新表格。在瀏覽器中的表格上,價格及百分比列中的數字都會隨着服務器推送數據而自動更新。如果你打開更多的瀏覽器,它們都會顯示相同的數據及自動更新。
注意:如果您你不想自己手動來構建這一應用程序,你可以再一個新的空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平台及工具團隊的高級程序員,作家。