英文渣水平,大夥湊合着看吧……javascript
這是微軟官方SignalR 2.0教程Getting Started with ASP.NET SignalR 2.0系列的翻譯,這裏是第八篇:SignalR的服務器廣播php
原文: Tutorial: Server Broadcast with SignalR 2.0css
VS能夠經過 Microsoft.AspNet.SignalR.Sample NuGet包來安裝一個簡單的模擬股票行情應用。在本教程的第一部分,您將從頭開始建立一個應用程序的簡化版本。在本教程的剩餘部分,您將安裝NuGet包,審閱Sample中的一些附加功能。html
本模擬股票行情應用表明了實時應用中的"推",或稱之爲廣播,即咱們將消息通知傳播給全部已鏈接客戶端。java
第一步,您將要建立該應用程序的顯示錶格用於顯示數據。python
接下來,服務器會隨機更新股票價格,而且將新數據推送至全部鏈接的客戶端以更新表格。在瀏覽器中的表格上,價格及百分比列中的數字都會隨着服務器推送數據而自動更新。若是你打開更多的瀏覽器,它們都會顯示相同的數據及自動更新。jquery
注意:若是您你不想本身手動來構建這一應用程序,你能夠再一個新的空ASP.NET WEB應用項目中安裝Simple包,經過閱讀這些步驟來獲取代碼的解釋。本教程的第一部分涵蓋了Sample的子集,第二部分解釋了包中的一些附加功能。git
1.新建一個新的ASP.NET應用程序,命名爲SignalR.StockTicker並建立。程序員
2.選擇空項目並肯定。github
在本節中,咱們來編寫服務器端代碼。
首先咱們來建立一個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及價格之間的差額並輸出結果。
您將使用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以前見到的那樣。
構造函數初始化了_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服務來查找股價;在本示例中,它使用一個隨機數來模擬股價的變化。
因爲價格變更發生於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 。
服務器須要知道那個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>