這篇文章參考的是Server Broadcast with SignalR 2這篇教程,很不錯的一篇教程,若是有興趣的話能夠查看原文,今天記錄下來做爲一個學習筆記,這樣從此翻閱會更方便一點。javascript
這篇教程經過實現一個股票報價的小程序來說解如何使用SignalR進行服務器端的推送,服務器會模擬股票價格的波動,並把最新的股票價格推送給全部鏈接的客戶端,最終的運行效果以下圖所示。php
1.打開Visual Studio,而後選擇新建項目。
2.在New Project對話框中,點擊Visual C#下的Web,而後新建一個名爲SignalR.StockTicker的ASP.NET Web Application項目。
3.在New ASP.NET窗口中,選擇Empty模板,而後點擊OK來建立項目。css
新建一個名爲Stock.cs的實體類,用來做爲服務器端推送消息的載體,具體代碼以下。html
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和Price這兩個屬性須要設置,其它屬性將會依據Price自動進行計算。java
咱們將會使用SignalR Hub API來處理服務器與客戶端的交互,因此新建一個繼承自SignalR Hub的StockTickerHub類來處理客戶端的鏈接及調用。除此以外,咱們還須要維護股票的價格數據以及新建一個Timer對象來按期的更新價格,而這些都是獨立於客戶端的鏈接的。因爲Hub的生命週期很短暫,只有在客戶端鏈接和調用的時候纔會建立新的實例(沒有研究過SignalR的源代碼,我以爲更確切一點兒應該是每當有新的客戶端鏈接成功時,服務器就會建立一個新的Hub實例,並經過該實例來與客戶端進行通訊,就像Socket通訊中服務器端會將全部的客戶端Socket放到一個統一的集合中進行維護),因此不要把與客戶端鏈接及調用無關的代碼放置到SignalR Hub類中。在這裏,咱們將維護股票數據、模擬更新股票價格以及向客戶端推送股票價格的代碼放置到一個名爲StockTicker的類中。jquery
咱們只須要在服務器端運行一個StockTicker類的實例(單例模式),因爲這個StockTicker類維護着股票的價格,因此它也要可以將最新的股票價格推送給全部的客戶端。爲了達到這個目的,咱們須要在這單個實例中引用全部的StockTickerHub實例,而這能夠經過SignalR Hub的Context對象來得到。shell
==Ps: 渣英語,上面2段出處的原文帖在下面,留作之後查看吧。==數據庫
You'll use the SignalR Hub API to handle server-to-client interaction. A StockTickerHub class that derives from the SignalR Hub class will handle receiving connections and method calls from clients. You also need to maintain stock data and run a Timer object to periodically trigger price updates, independently of client connections. You can't put these functions in a Hub class, because Hub instances are transient. A Hub class instance is created for each operation on the hub, such as connections and calls from the client to the server. So the mechanism that keeps stock data, updates prices, and broadcasts the price updates has to run in a separate class, which you'll name StockTicker.小程序
You only want one instance of the StockTicker class to run on the server, so you'll need to set up a reference from each StockTickerHub instance to the singleton StockTicker instance. The StockTicker class has to be able to broadcast to clients because it has the stock data and triggers updates, but StockTicker is not a Hub class. Therefore, the StockTicker class has to get a reference to the SignalR Hub connection context object. It can then use the SignalR connection context object to broadcast to clients.api
1.在Solution Explorer中,右鍵項目,經過Add | SignalR Hub Class(V2)新建一個名爲StockTickerHub.cs的文件。
2.在StockTickerHub類中輸入下面這段代碼。
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(); } } }
這個Hub類用來定義客戶端能夠調用的服務端方法,當客戶端與服務器創建鏈接後,將會調用GetAllStocks()方法來得到股票數據以及當前的價格,由於這個方法是直接從內存中讀取數據的,因此會當即返回IEnumerable<Stock>數據。若是這個方法是經過其它可能會有延時的方式來調用最新的股票數據的話,好比從數據庫查詢,或者調用第三方的Web Service,那麼就須要指定Task<IEnumerable<Stock>>來做爲返回值,從而實現異步通訊,更多信息請參考ASP.NET SignalR Hubs API Guide - Server - When to execute asynchronously。
HubName屬性指定了該Hub的別名,即客戶端腳本調用的Hub名,若是不使用HubName屬性指定別名的話,默認將會使用駱駝命名法,那麼它在客戶端調用的名稱將會是stockTickerHub。
接下來咱們將會建立StockTicker類,而且建立一個靜態實例屬性。這樣無論有多少個客戶端鏈接或者斷開,內存中都只有一個StockTicker類的實例,而且還能夠經過該實例的GetAllStocks方法來得到當前的股票數據。
4.在項目中建立一個名爲StockTicker的新類,並在類中輸入下面這段代碼。
using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Hubs; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; namespace SignalR.StockTicker { public class StockTicker { // 單例模式 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(); // 控制股票價格波動的百分比 private readonly double _rangePercent = 0.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) { var r = _updateOrNotRandom.NextDouble(); if (r > 0.1) { return false; } var random = new Random((int)Math.Floor(stock.Price)); var percentChange = random.NextDouble() * _rangePercent; var pos = random.NextDouble() > 0.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類的實例涉及到多線程,因此該類須要是線程安全的。
在這個類中,咱們新建了一個名爲_instance的字段用來存放該類的實例,而且將構造函數的訪問權限設置成私有狀態,這樣其它的類就只能經過Instance這個靜態屬性來得到該類的實例,而沒法經過關鍵字new來建立一個新的實例。在這個_instance字段上面,咱們使用了Lazy特性,雖然會損失一點兒性能,可是它卻能夠保證以線程安全的方式來建立實例。
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實例將會在一個獨立的線程中被建立,並經過SockTicker.Instance屬性來得到惟一的StockTicker實例,就像以前介紹的那樣。
這個類定義了一個_stocks字段來存放測試用的股票數據,而且經過GetAllStocks這個方法來進行獲取。咱們前面講過客戶端會經過StockTickerHub.GetAllStocks來獲取當前的股票數據,其實就是這裏的股票數據。
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; }
爲了線程安全,咱們使用了ConcurrentDictionary來存放股票數據,固然你也可使用Dictionary對象來進行存儲,可是在更新數據以前須要進行鎖定。
在這個測試程序中,咱們將數據存直接存放在內存中,這樣作並無什麼問題,但在實際的應用場景中,則須要將數據存放在數據庫之類的文件中以便長久的保存。
在這個類中,咱們定義了一個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) { var r = _updateOrNotRandom.NextDouble(); if (r > .1) { return false; } // 使用Random來模擬股票價格的更新 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方法,並向該方法傳遞一個null來更新股票的價格。在更新以前,咱們使用了_updateStockPricesLock對象將須要更新的部份進行鎖定,並經過_updatingStockPrices變量來肯定是否有其它線程已經更新了股票的價格。而後經過對每個股票代碼執行TryUpdateStockPrice方法來肯定是否更新股票價格以及股票價格的波動幅度。若是檢測到股票價格變更,將會經過BroadcastStockPrice方法將最新的股票價格推送給每個鏈接的客戶端。
咱們使用了volatile修飾符來標記_updatingStockPrices變量,該修飾符指示一個字段能夠由多個同時執行的線程修改,聲明爲volatile的字段不受編譯器優化(假定由單個線程訪問)的限制,這樣能夠確保該字段在任什麼時候間呈現的都是最新的值。該修飾符一般用於由多個線程訪問但不使用lock語句對訪問進行序列化的字段。
private volatile bool _updatingStockPrices = false;
在實際的場景中,TryUpdateStockPrice方法一般會經過調用第三方的Web Service來獲取最新的股票價格,而在這個程序中,咱們則是經過隨機數來進行模擬該實現。
由於股票價格變更是在StockTicker對象中,因此這個對象須要調用客戶端的updateStockPrice回調方法來推送數據。在Hub類中,咱們能夠直接使用API來調用客戶端的方法,可是這個StockTicker類並無繼承自Hub,因此沒法直接使用這些對象。爲了可以向客戶端廣播數據,StockTicker類須要使用SignalR Hub的Context對象來得到StokTickerHub類的實例,並用它來調用客戶端的方法。
下面這段代碼演示了在建立StockTicker類靜態實例的時候,把SignalR的Context引用經過構造函數傳遞給Clients這個屬性。在這裏只須要獲取一次SignalR.Context,這樣作有2個好處,首先是由於獲取SignalR.Context很耗費資源,其次是獲取一次SignalR.Context能夠保留消息發送到客戶端的預約義順序(The intended order of messages sent to clients is preserved)。
private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients)); private StockTicker(IHubConnectionContext<dynamic> clients) { Clients = clients; // 構造函數的餘下代碼... } private IHubConnectionContext<dynamic> Clients { get; set; } private void BroadcastStockPrice(Stock stock) { Clients.All.updateStockPrice(stock); }
使用Clients屬性,可使您和在Hub類中同樣,經過它來調用客戶端的方法。在BroadcastStockPrice方法中調用的updateStockPrice方法實際並不存在,呆會咱們將會在客戶端的腳本中實現該方法。由於Clients.All是dynamic類型的,也就是說在程序運行的時候會對這個表達式進行動態賦值,因此這裏能夠直接使用它。當這個方法被調用的時候,SignalR將會把這個方法的名稱以及參數一併發給客戶端,若是客戶端存在一個名稱爲updateStockPrice的方法的話,那麼就會將參數傳遞給該方法並調用它。
Clients.All意味着發送給全部的客戶端,同時SignalR還提供了用來指定具體的客戶端或組的屬性,具體信息能夠參考HubConnectionContext。
服務器須要知道把哪些請求交由SignalR進行操做,爲了實現這個功能,咱們須要在OWIN的Startup文件中進行相應的設置。
1.首先打開vs的Solution Explorer,在項目上右擊,而後依次點擊Add | OWIN Startup Class按鈕,添加一個名爲Startup.cs的類。
2.在Startup.cs類中輸入下面這段代碼。
using Owin; using Microsoft.Owin; [assembly: OwinStartup(typeof(SignalR.StockTicker.Startup))] namespace SignalR.StockTicker { public class Startup { public void Configuration(IAppBuilder app) { app.MapSignalR(); } } }
如今咱們已經完成了服務端的編碼工做,接下來咱們須要完成客戶端的代碼。
1.在項目的根目錄下,建立一個名爲StockTicker.html的HTML文件。
2.在HTML文件中輸入下面這段代碼。
<!DOCTYPE html> <html> <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 src="/Scripts/jquery-1.10.2.js"></script> <script src="/Scripts/jquery.signalR-2.1.2.js"></script> <script src="/signalr/hubs"></script> <script src="StockTicker.js"></script> </body> </html>
上面的HTML代碼建立了一個2行5列的表格,由於默認並無數據,因此第2行顯示的是「loading」,當程序運行的時候,這個提示行將被實際的數據覆蓋掉。接下來則分別引入了jQuery、SignalR、SignalR代理,以及StockTicker腳本文件。SignalR代理文件(/signalr/hubs)將會根據服務器端編寫的Hub文件動態的生成相應的腳本(生成關於StockTickerHub.GetAllStocks的相關代碼),若是你願意,你還能夠經過SignalR Utilities來手動生成腳本文件,可是須要在MapHubs方法中禁用動態文件建立的功能。
注意:請確保StockTicker.html文件中引入的腳本文件在你的項目中是實際存在的。
3.在Solution Explorer中,右擊StockTicker.html,而後點擊Set as Start Pagae菜單。
4.在項目的根目錄下建立一個名爲StockTicker.js的腳本文件。
5.在腳本文件中,輸入下面這段代碼。
// 自定義的模板方法 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, // 客戶端的Hub代理 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)); }); }); } // 客戶端的回調方法,該方法會被服務端進行調用 ticker.client.updateStockPrice = function (stock) { var displayStock = formatStock(stock), $row = $(rowTemplate.supplant(displayStock)); $stockTableBody.find("tr[data-symbol=" + stock.Symbol + "]") .replaceWith($row); }; // 開始與服務端創建鏈接 $.connection.hub.start().done(init); });
$.connection便是指SignalR代理,下面這行代碼表示將StockTickerHub類的代理的引用保存在變量ticker中,代理的名稱即爲服務器端經過[HubName]屬性設置的名稱。
// 客戶端引用的代碼 var ticker = $.connection.stockTickerMini // 服務器端的代碼 [HubName("stockTickerMini")] public class StockTickerHub : Hub
客戶端的代碼編寫好以後,就能夠經過最後的這行代碼來與服務器創建鏈接,因爲這個start方法執行的是異步操做,並會返回一個jQuery延時對象,因此咱們要使用jQuery.done函數來處理鏈接成功以後的操做。
$.connection.hub.start().done(init);
init方法會調用服務端的getAllStocks方法,並將返回的數據顯示在Table中。也許你可能注意到這裏的getAllStocks方法名和服務器端的GetAllStocks其實並不同,這是由於服務端咱們默認會使用帕斯卡命名法,而SignalR會在生成客戶端的代理類時,自動將服務端的方法改爲駱駝命名法,不過該規則只對方法名及Hub名稱有效,而對於對象的屬性名,則仍然和服務器端的同樣,好比stock.Symbol、stock.Price,而不是stock.symbol、stock.price。
// 客戶端的代碼 function init() { ticker.server.getAllStocks().done(function (stocks) { $stockTableBody.empty(); $.each(stocks, function () { var stock = formatStock(this); $stockTableBody.append(rowTemplate.supplant(stock)); }); }); } // 服務器端的代碼 public IEnumerable<Stock> GetAllStocks() { return _stockTicker.GetAllStocks(); }
若是你想在客戶端使用與服務器商相同的名稱(包括大小寫),或者想本身定義其它的名稱,那麼你能夠經過給Hub方法加上HubMethodName標籤來實現這個功能,而HubName標籤則能夠實現自定義的Hub名稱。
在這個init方法中,咱們會遍歷服務端返回的股票數據,而後經過調用formatStock來格式化成咱們想要的格式,接着經過supplant方法(在StockTicker.js的最頂端)來生成一條新行,並把這個新行插入到表格裏面。
這個init方法實際上是在start方法完成異步操做後做爲回調函數執行的,若是你把init做爲一個獨立的JavaScript語句放在start方法以後的話,那麼程序將會出錯,由於這樣會致使服務端的方法在客戶端尚未與服務器創建鏈接以前就被調用。
當服務器端的股票價格變更的時候,它就會經過調用已鏈接的客戶端上的updateStockPrice方法來更新數據。爲了讓服務器可以調用客戶的代碼,咱們須要把updateStockPrice添加到stockTicker代理的client對象中,代碼以下。
ticker.client.updateStockPrice = function (stock) { var displayStock = formatStock(stock), $row = $(rowTemplate.supplant(displayStock)); $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']') .replaceWith($row); }
該updateStockPrice方法和init方法同樣,經過調用formatStock來格式化成咱們想要的格式,接着經過supplant方法(在StockTicker.js的最頂端)來生成一條新行,不過它並非將該新行追加到Table中,而是找到Table中現有的行,而後使用新行替換它。
1.按下F5啓動程序,就會看到Table中顯示的「loading...」,不過緊接着便會被服務器端的股票數據替換掉,而且這些股票數據會隨着服務器的推送而不停的發生改變。
2.複製頁面的地址,並用其它的瀏覽器打開,就會看到相同的數據,以及相同的價格變化。
3.關閉全部的瀏覽器,而後從新在瀏覽器中打開這個頁面,就會看到此次頁面顯示的速度(股票價格推送)要比第一次快的多,並且第一次看到的股票的價格是有數據的,而不是像第一次那樣都顯示爲0,這是由於服務器端的StockTicker靜態實例仍然在運行。
SignalR內置了日誌功能,你能夠在客戶端選擇開啓該功能來幫助你調試程序,接下來咱們將會經過開啓SignalR的日誌功能來展現一下在不一樣的環境下SignalR所使用的傳輸技術,大至總結以下:
在服務器端及客戶端都支持的狀況下,SignalR默認會選擇最佳的傳輸方式。
1.打開StockTicker.js,而後在客戶端與服務端創建鏈接以前加上下面這段代碼。
// Start the connection
$.connection.hub.logging = true; $.connection.hub.start().done(init);
2.從新運行程序,並打開瀏覽器的開發者工具,選擇控制檯標籤,就能夠看到SignalR輸出的日誌(若是想看到所有的日誌,請刷新頁面)。
若是你是在Windows 8(IIS 8)上用IE10打開的話,將會看到WebSocket的鏈接方式。
若是你是在Windows 7(IIS 7.5)上用IE10打開的話,將會看到使用iframe的鏈接方式。
Windows 8(IIS 8)上用Firefox的話,將會看到WebSocket的鏈接方式。
在Windows 7(IIS 7.5)上用Firefox打開的話,將會看到使用Server-sent events的鏈接方式。
至此,咱們已經實現了最基本的服務端推送功能,若是你想查看更多功能的話,能夠經過NuGet包管理器來安裝這個程序的完整版本(Microsoft.AspNet.SignalR.Sample),若是你沒有按照上面的教程操做而是直接從NuGet服務器上獲取這個測試程序的話,那麼你可能須要在OWIN的Startup類中進行相關的設置,具體說明能夠參考文件夾內的readme.txt文件。
1.在Solution Explorer內右擊項目,而後點擊Manage NuGet Packages。
2.在Manage NuGet Packages對話框中,選擇Online,而後在搜索框中輸入SignalR.Sample,當搜索結果出來後,在SignalR.Sample包的後面點擊Install來進行安裝。
3.在Solution Explorer裏,將會看到項目根目錄多了一個名爲SignalR.Sample的文件夾,這裏即是所有的代碼。
4.在SignalR.Sample文件夾下,右擊StockTicker.html,選擇Set As Start Page,將其設置爲啓動文件。
注意:安裝SignalR.Sample包可能會改變你本地原用的jQuery的版本,在啓動程序以前要注意覈對文件的版本是否正確。
運行程序後,你會看到和以前相似的表格,不過除了在表格裏顯示最新的股票價格以外,該頁面還會有一個水平的滾動條來顯示相同的股票價格。不過與前面的教程不一樣的是,這個頁面須要你點擊Open Market按鈕以後纔會開始接收服務器的推送數據。
當點擊Open Market以後,Live Stock Ticker開始滾動顯示股票價格,而在Table裏面則會經過不一樣的顏色來區分股價的上漲以及下跌。
點擊Close Market按鈕之後,Live Stock Table和Live Stock Ticker將會中止顯示股票價格的波動,而此時若是點擊Reset按鈕的話,那麼頁面上的股票價格將會變成初始狀態。而對於Live Stock Ticker的實現,和上面的Table相似,只不過是使用了<li>標籤,以及用到了jQuery的animate函數。
Live Stock Ticker的HTML代碼:
<h2>Live Stock Ticker</h2> <div id="stockTicker"> <div class="inner"> <ul> <li class="loading">loading...</li> </ul> </div> </div>
Live Stock Ticker的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;} #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); }
和以前同樣,咱們須要SignalRHub類中添加可供客戶端調用的方法。
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方法對應着頁面頂部的3個按鈕,只要有一個客戶端操做了這3個按鈕,那麼全部鏈接的客戶端就都會受到影響,由於每個用戶均可以設置推送狀態,並將該狀態廣播給全部的客戶端。
在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標識符。
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方法要負責維護table和ul數據的顯示,並經過jQuery.Color來爲股票價格的上漲或者下跌進行着色。
SignalR.StockTicker.js裏新增的方法經過推送狀態(MarketState)來啓用或者禁用操做按鈕,同時還決定着table及ul裏的數據是否刷新。咱們能夠經過jQuery.extend方法來把這幾個方法添加到ticker.client對象中。
$.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(); } });
當客戶端與服務器端創建鏈接後,還有一些額外的工做須要作,好比根據當前的推送狀態來決定調用服務器端的方法,以及將調用服務器端的方法綁定到相應的按鈕上。
$.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(); }); });
在done方法中綁定按鈕事件是爲了確保客戶端在與服務器創建鏈接後纔可以調用服務器的方法。
以上就是SignalR.Sample的主要代碼,若是有興趣的話,能夠經過Install-Package Microsoft.AspNet.SignalR.Sample
來安裝並查看完整的代碼。Demo下載:https://download.csdn.net/download/zxh91989/10868746。
……