這篇教程通過實現一個股票報價的小程式來講解如何使用SignalR進行伺服器端的推送,伺服器會模擬股票價格的波動,並把最新的股票價格推送給所有連接的客戶端。該文章參考的是[Server Broadcast with SignalR 2]這篇教程,很不錯的一篇教程,如果有興趣的話可以查看原文,今天記錄下... ...
概述
這篇文章參考的是Server Broadcast with SignalR 2這篇教程,很不錯的一篇教程,如果有興趣的話可以查看原文,今天記錄下來作為一個學習筆記,這樣今後翻閱會更方便一點。
這篇教程通過實現一個股票報價的小程式來講解如何使用SignalR進行伺服器端的推送,伺服器會模擬股票價格的波動,並把最新的股票價格推送給所有連接的客戶端,最終的運行效果如下圖所示。
教程篇
創建項目
1.打開Visual Studio,然後選擇新建項目。
2.在New Project對話框中,點擊Visual C#下的Web,然後新建一個名為SignalR.StockTicker的ASP.NET Web Application項目。
3.在New ASP.NET視窗中,選擇Empty模板,然後點擊OK來創建項目。
伺服器端代碼
新建一個名為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和Price這兩個屬性需要設置,其它屬性將會依據Price自動進行計算。
創建StockTicker和StockTickerHub類
我們將會使用SignalR Hub API來處理伺服器與客戶端的交互,所以新建一個繼承自SignalR Hub的StockTickerHub類來處理客戶端的連接及調用。除此之外,我們還需要維護股票的價格數據以及新建一個Timer對象來定期的更新價格,而這些都是獨立於客戶端的連接的。由於Hub的生命周期很短暫,只有在客戶端連接和調用的時候才會創建新的實例(沒有研究過SignalR的源代碼,我覺得更確切一點兒應該是每當有新的客戶端連接成功時,伺服器就會創建一個新的Hub實例,並通過該實例來與客戶端進行通信,就像Socket通信中伺服器端會將所有的客戶端Socket放到一個統一的集合中進行維護),所以不要把與客戶端連接及調用無關的代碼放置到SignalR Hub類中。在這裡,我們將維護股票數據、模擬更新股票價格以及向客戶端推送股票價格的代碼放置到一個名為StockTicker的類中。
我們只需要在伺服器端運行一個StockTicker類的實例(單例模式),由於這個StockTicker類維護著股票的價格,所以它也要能夠將最新的股票價格推送給所有的客戶端。為了達到這個目的,我們需要在這單個實例中引用所有的StockTickerHub實例,而這可以通過SignalR Hub的Context對象來獲得。
==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.
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實例,就像之前介紹的那樣。
使用ConcurrentDictionary來存放股票數據
這個類定義了一個_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來獲取最新的股票價格,而在這個程式中,我們則是通過隨機數來進行模擬該實現。
通過SignalR Hub的Context對象來實現服務端的推送
因為股票價格變動是在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路由
伺服器需要知道把哪些請求交由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所使用的傳輸技術,大至總結如下:
- WebSocket,至少需要IIS8及以上版本的支持。
- Server-sent events,支持IE以外的瀏覽器。
- Forever frame,支持IE瀏覽器。
- Ajax long polling,支持所有瀏覽器。
在伺服器端及客戶端都支持的情況下,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的連接方式。
安裝並查看完整的StockTicker.Sample代碼
至此,我們已經實現了最基本的服務端推送功能,如果你想查看更多功能的話,可以通過NuGet包管理器來安裝這個程式的完整版本(Microsoft.AspNet.SignalR.Sample),如果你沒有按照上面的教程操作而是直接從NuGet伺服器上獲取這個測試程式的話,那麼你可能需要在OWIN的Startup類中進行相關的設置,具體說明可以參考文件夾內的readme.txt文件。
安裝SignalR.Sample的NuGet包
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
來安裝並查看完整的代碼。