ASP.NET使用SignalR2實現(xiàn)服務(wù)器廣播
一、概述
這篇教程通過實現(xiàn)一個股票報價的小程序來講解如何使用SignalR進行服務(wù)器端的推送,服務(wù)器會模擬股票價格的波動,并把最新的股票價格推送給所有連接的客戶端,最終的運行效果如下圖所示。

可以通過Install-Package Microsoft.AspNet.SignalR.Sample來安裝并查看完整的代碼。
二、服務(wù)器端代碼
新建一個名為Stock.cs的實體類,用來作為服務(wù)器端推送消息的載體,具體代碼如下。
這個實體類只有Symbol和Price這兩個屬性需要設(shè)置,其它屬性將會依據(jù)Price自動進行計算。
using System;
namespace Microsoft.AspNet.SignalR.StockTicker
{
public class Stock
{
private decimal _price;
public string Symbol { get; set; }
public decimal DayOpen { get; private set; }
public decimal DayLow { get; private set; }
public decimal DayHigh { get; private set; }
public decimal LastChange { get; private set; }
public decimal Price
{
get
{
return _price;
}
set
{
if (_price == value)
{
return;
}
LastChange = value - _price;
_price = value;
if (DayOpen == 0)
{
DayOpen = _price;
}
if (_price < DayLow || DayLow == 0)
{
DayLow = _price;
}
if (_price > DayHigh)
{
DayHigh = _price;
}
}
}
public decimal Change
{
get
{
return Price - DayOpen;
}
}
public double PercentChange
{
get
{
return (double)Math.Round(Change / Price, 4);
}
}
}
}1、創(chuàng)建StockTicker和StockTickerHub類
我們將使用SignalR Hub API來處理服務(wù)器到客戶端的交互,所以新建一個繼承自SignalR Hub類的StockTickerHub類來處理客戶端的連接及調(diào)用。除此之外,我們還需要維護股票的價格數(shù)據(jù)以及新建一個Timer對象來定期的更新價格,而這些都與客戶端連接無關(guān)的。由于Hub實例的生命周期很短暫,只有在比如客戶端連接和調(diào)用的時候,Hub類實例是為Hub上的每個這樣的操作才會創(chuàng)建新的實例,所以不要把與客戶端連接及調(diào)用無關(guān)的代碼放置到SignalR Hub類中。在這里,我們將維護股票數(shù)據(jù)、模擬更新股票價格以及向客戶端推送股票價格的代碼放置到一個名為StockTicker的類中。

我們只需要在服務(wù)器端運行一個StockTicker類的實例(單例模式),由于這個StockTicker類維護著股票的價格,所以它也要能夠?qū)⒆钚碌墓善眱r格推送給所有的客戶端。為了達到這個目的,我們需要在這單個實例中引用所有的StockTickerHub實例,而這可以通過SignalR Hub的Context對象來獲得,然后它可以使用SignalR連接Context對象向客戶端廣播。
2、StockTickerHub類
這個Hub類用來定義客戶端可以調(diào)用的服務(wù)端方法,當客戶端與服務(wù)器建立連接后,將會調(diào)用GetAllStocks()方法來獲得股票數(shù)據(jù)以及當前的價格,因為這個方法是直接從內(nèi)存中讀取數(shù)據(jù)的,所以會立即返回IEnumerable數(shù)據(jù)。如果這個方法是通過其它可能會有延時的方式來調(diào)用最新的股票數(shù)據(jù)的話,比如從數(shù)據(jù)庫查詢,或者調(diào)用第三方的Web Service,那么就需要指定Task>來作為返回值,從而實現(xiàn)異步通信,更多信息請參考ASP.NET SignalR Hubs API Guide - Server - When to execute asynchronously。
HubName屬性指定了該Hub的別名,即客戶端腳本調(diào)用的Hub名,如果不使用HubName屬性指定別名的話,默認將會使用駱駝命名法,那么它在客戶端調(diào)用的名稱將會是stockTickerHub。
using Microsoft.AspNet.SignalR.Hubs;
using System.Collections.Generic;
namespace Microsoft.AspNet.SignalR.StockTicker
{
[HubName("stockTicker")]
public class StockTickerHub : Hub
{
private readonly StockTicker _stockTicker;
public StockTickerHub()
: this(StockTicker.Instance)
{
}
public StockTickerHub(StockTicker stockTicker)
{
_stockTicker = stockTicker;
}
public IEnumerable GetAllStocks()
{
return _stockTicker.GetAllStocks();
}
}
}接下來我們將會創(chuàng)建StockTicker類,并且創(chuàng)建一個靜態(tài)實例屬性。這樣不管有多少個客戶端連接或者斷開,內(nèi)存中都只有一個StockTicker類的實例,并且還可以通過該實例的GetAllStocks方法來獲得當前的股票數(shù)據(jù)。
3、StockTicker類
由于所有線程都運行 StockTicker 代碼的同一個實例,StockTicker 類必須要是線程安全的。
1、將單個實例保存在一個靜態(tài)字段中
在這個類中,我們新建了一個名為_instance的字段用來存放該類的實例,并且將構(gòu)造函數(shù)的訪問權(quán)限設(shè)置成私有狀態(tài),這樣其它的類就只能通過Instance這個靜態(tài)屬性來獲得該類的實例,而無法通過關(guān)鍵字new來創(chuàng)建一個新的實例。在這個_instance字段上面,我們使用了Lazy特性,雖然會損失一點兒性能,但是它卻可以保證以線程安全的方式來創(chuàng)建實例。
每次客戶端連接到服務(wù)器時,運行在單獨線程中的StockTickerHub類的新實例都會從StockTicker獲取StockTicker單例實例。實例靜態(tài)屬性,如前面在StockTickerHub類中看到的。
2、使用ConcurrentDictionary來存放股票數(shù)據(jù)
這個類定義了一個_stocks字段來存放測試用的股票數(shù)據(jù),并且通過GetAllStocks這個方法來進行獲取。我們前面講過客戶端會通過StockTickerHub.GetAllStocks來獲取當前的股票數(shù)據(jù),其實就是這里的股票數(shù)據(jù)。
在這個測試程序中,我們將數(shù)據(jù)存直接存放在內(nèi)存中,這樣做并沒有什么問題,但在實際的應(yīng)用場景中,則需要將數(shù)據(jù)存放在數(shù)據(jù)庫之類的文件中以便長久的保存。
3、定期的更新股票價格
在這個類中,我們定義了一個Timer對象來定期的更新股票的價格。
在實際的場景中,TryUpdateStockPrice方法通常會通過調(diào)用第三方的Web Service來獲取最新的股票價格,而在這個程序中,我們則是通過隨機數(shù)來進行模擬該實現(xiàn)。
4、通過SignalR Hub的Context對象來實現(xiàn)服務(wù)端的推送
因為股票價格變動是在StockTicker對象中,所以這個對象需要調(diào)用客戶端的updateStockPrice回調(diào)方法來推送數(shù)據(jù)。在Hub類中,我們可以直接使用API來調(diào)用客戶端的方法,但是這個StockTicker類并沒有繼承自Hub,所以無法直接使用這些對象。為了能夠向客戶端廣播數(shù)據(jù),StockTicker類需要使用SignalR Hub的Context對象來獲得StokTickerHub類的實例,并用它來調(diào)用客戶端的方法。
using Microsoft.AspNet.SignalR.Hubs;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
namespace Microsoft.AspNet.SignalR.StockTicker
{
public class StockTicker
{
#region 字段
// 初始化StockTicker為單例實例
private readonly static Lazy _instance = new Lazy(() => new StockTicker( GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients) );
//在創(chuàng)建StockTicker類靜態(tài)實例的時候,把SignalR的Context引用通過構(gòu)造函數(shù)傳遞給Clients這個屬性。
//在這里只需要獲取一次SignalR.Context,這樣做有2個好處,首先是因為獲取SignalR.Context很耗費資源,其次是獲取一次SignalR.Context可以保留消息發(fā)送到客戶端的預(yù)定義順序。
private readonly object _marketStateLock = new object();
private readonly object _updateStockPricesLock = new object();
//為了線程安全,我們使用了ConcurrentDictionary來存放股票數(shù)據(jù),當然你也可以使用Dictionary對象來進行存儲,但是在更新數(shù)據(jù)之前需要進行鎖定。
private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();
// 控制股票價格波動的百分比
private readonly double _rangePercent = 0.002;
private readonly TimeSpan _updateInterval = TimeSpan.FromMilliseconds(250);
private readonly Random _updateOrNotRandom = new Random();
private Timer _timer;
//使用volatile修飾符來標記_updatingStockPrices變量,該修飾符指示一個字段可以由多個同時執(zhí)行的線程修改,聲明為volatile的字段不受編譯器優(yōu)化(假定由單個線程訪問)的限制,這樣可以確保該字段在任何時間呈現(xiàn)的都是最新的值。
//該修飾符通常用于由多個線程訪問但不使用lock語句對訪問進行序列化的字段。
private volatile bool _updatingStockPrices;
#endregion 字段
#region 構(gòu)造函數(shù)
private StockTicker(IHubConnectionContext<dynamic> clients)
{
Clients = clients;
_stocks.Clear();
var stocks = new List
{
new Stock { Symbol = "MSFT", Price = 41.68m },
new Stock { Symbol = "AAPL", Price = 92.08m },
new Stock { Symbol = "GOOG", Price = 543.01m }
};
stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock));
_timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);//定時更新股價
}
#endregion 構(gòu)造函數(shù)
#region 屬性
private IHubConnectionContext<dynamic> Clients { get; set; } //使用Clients屬性,可以使您和在Hub類中一樣,通過它來調(diào)用客戶端的方法。
public static StockTicker Instance
{
get
{
return _instance.Value;
}
}
#endregion 屬性
#region 方法
//獲取所有股價
public IEnumerable GetAllStocks()
{
return _stocks.Values;
}
//在更新之前,我們使用了_updateStockPricesLock對象將需要更新的部份進行鎖定,并通過_updatingStockPrices變量來確定是否有其它線程已經(jīng)更新了股票的價格。
//然后通過對每一個股票代碼執(zhí)行TryUpdateStockPrice方法來確定是否更新股票價格以及股票價格的波動幅度。如果檢測到股票價格變動,將會通過BroadcastStockPrice方法將最新的股票價格推送給每一個連接的客戶端。
private void UpdateStockPrices(object state)
{
// 此函數(shù)必須可重入re-entrant,因為它是作為計時器間隔處理程序運行的。
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); //Clients.All是dynamic類型的,意味著發(fā)送給所有的客戶端,同時SignalR還提供了用來指定具體的客戶端或組的屬性,具體信息可以參考HubConnectionContext。
}
#endregion 方法
}
}4、注冊SignalR路由
服務(wù)器需要知道把哪些請求交由SignalR進行操作,為了實現(xiàn)這個功能,我們需要在OWIN的Startup文件中進行相應(yīng)的設(shè)置。
using Owin;
namespace Microsoft.AspNet.SignalR.StockTicker
{
public static class Startup
{
public static void ConfigureSignalR(IAppBuilder app)
{
// For more information on how to configure your application using OWIN startup, visit http://go.microsoft.com/fwlink/?LinkID=316888
app.MapSignalR();
}
}
}三、客戶端代碼
StockTicker.html
頁面分別引入了jQuery、SignalR、SignalR代理,以及StockTicker腳本文件。SignalR代理文件(/signalr/hubs)將會根據(jù)服務(wù)器端編寫的Hub文件動態(tài)的生成相應(yīng)的腳本(生成關(guān)于StockTickerHub.GetAllStocks的相關(guān)代碼),如果你愿意,你還可以通過SignalR Utilities來手動生成腳本文件,但是需要在MapHubs方法中禁用動態(tài)文件創(chuàng)建的功能。
注意:請確保StockTicker.html文件中引入的腳本文件在你的項目中是實際存在的。
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>ASP.NET SignalR Stock Ticker</title>
<link href="StockTicker.css" rel="external nofollow" rel="stylesheet" />
</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>High</th><th>Low</th><th>Change</th><th>%</th></tr>
</thead>
<tbody>
<tr class="loading"><td colspan="7">loading...</td></tr>
</tbody>
</table>
</div>
<h2>Live Stock Ticker</h2>
<div id="stockTicker">
<div class="inner">
<ul>
<li class="loading">loading...</li>
</ul>
</div>
</div>
<script src="jquery-1.10.2.min.js"></script>
<script src="jquery.color-2.1.2.min.js"></script>
<script src="../Scripts/jquery.signalR-2.4.1.js"></script>
<script src="../signalr/hubs"></script>
<script src="SignalR.StockTicker.js"></script>
</body>
</html>StockTicker.js
// Crockford's supplant method (poor man's templating)自定義的模板方法
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;
}
);
};
}
// A simple background color flash effect that uses jQuery Color plugin
jQuery.fn.flash = function (color, duration) {
var current = this.css('backgroundColor');
this.animate({ backgroundColor: 'rgb(' + color + ')' }, duration / 2)
.animate({ backgroundColor: current }, duration / 2);
};
$(function () {
var ticker = $.connection.stockTicker, //$.connection即是指SignalR代理,這行代碼表示將StockTickerHub類的代理的引用保存在變量ticker中,代理的名稱即為服務(wù)器端通過[HubName]屬性設(shè)置的名稱。
up = '▲',
down = '▼',
$stockTable = $('#stockTable'),
$stockTableBody = $stockTable.find('tbody'),
rowTemplate = '{Symbol}{Price}{DayOpen}{DayHigh}{DayLow}{Direction} {Change}{PercentChange}',
$stockTicker = $('#stockTicker'),
$stockTickerUl = $stockTicker.find('ul'),
liTemplate = '
{Symbol} {Price} {Direction} {Change} ({PercentChange})
';
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,
DirectionClass: stock.Change === 0 ? 'even' : stock.Change >= 0 ? 'up' : 'down'
});
}
function scrollTicker() {
var w = $stockTickerUl.width();
$stockTickerUl.css({ marginLeft: w });
$stockTickerUl.animate({ marginLeft: -w }, 15000, 'linear', scrollTicker);
}
function stopTicker() {
$stockTickerUl.stop();
}
//init方法調(diào)用服務(wù)端的getAllStocks方法,并將返回的數(shù)據(jù)顯示在Table中。服務(wù)端我們默認會使用帕斯卡命名法,而SignalR會在生成客戶端的代理類時,自動將服務(wù)端的方法改成駱駝命名法,不過該規(guī)則只對方法名及Hub名稱有效。
//而對于對象的屬性名,則仍然和服務(wù)器端的一樣,比如stock.Symbol、stock.Price,而不是stock.symbol、stock.price。
//如果你想在客戶端使用與服務(wù)器商相同的名稱(包括大小寫),或者想自己定義其它的名稱,那么你可以通過給Hub方法加上HubMethodName標簽來實現(xiàn)這個功能,而HubName標簽則可以實現(xiàn)自定義的Hub名稱。
function init() {
return ticker.server.getAllStocks().done(function (stocks) {
$stockTableBody.empty();
$stockTickerUl.empty();
//遍歷服務(wù)端返回的股票數(shù)據(jù),然后通過調(diào)用formatStock來格式化成我們想要的格式,接著通過supplant方法(在StockTicker.js的最頂端)來生成一條新行,并把這個新行插入到表格里面。
$.each(stocks, function () {
var stock = formatStock(this);
$stockTableBody.append(rowTemplate.supplant(stock));
$stockTickerUl.append(liTemplate.supplant(stock));
});
});
}
//為了讓服務(wù)器能夠調(diào)用客戶的代碼,我們需要把updateStockPrice添加到stockTicker代理的client對象中
$.extend(ticker.client, {
updateStockPrice: function (stock) {
var displayStock = formatStock(stock),
$row = $(rowTemplate.supplant(displayStock)),
$li = $(liTemplate.supplant(displayStock)),
bg = stock.LastChange < 0
? '255,148,148' // red
: '154,240,117'; // green
//該updateStockPrice方法和init方法一樣,通過調(diào)用formatStock來格式化成我們想要的格式,接著通過supplant方法(在StockTicker.js的最頂端)來生成一條新行,不過它并不是將該新行追加到Table中,而是找到Table中現(xiàn)有的行,然后使用新行替換它。
$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);
scrollTicker();
}
});
// 客戶端的代碼編寫好之后,就可以通過最后的這行代碼來與服務(wù)器建立連接,由于這個start方法執(zhí)行的是異步操作,并會返回一個jQuery延時對象,所以我們要使用jQuery.done函數(shù)來處理連接成功之后的操作。
//這個init方法其實是在start方法完成異步操作后作為回調(diào)函數(shù)執(zhí)行的,如果你把init作為一個獨立的JavaScript語句放在start方法之后的話,那么程序?qū)鲥e,因為這樣會導致服務(wù)端的方法在客戶端還沒有與服務(wù)器建立連接之前就被調(diào)用。
$.connection.hub.start().done(init);
});四、輸出日志
SignalR內(nèi)置了日志功能,你可以在客戶端選擇開啟該功能來幫助你調(diào)試程序,接下來我們將會通過開啟SignalR的日志功能來展示一下在不同的環(huán)境下SignalR所使用的傳輸技術(shù),大至總結(jié)如下:
- WebSocket,至少需要IIS8及以上版本的支持。
- Server-sent events,支持IE以外的瀏覽器。
- Forever frame,支持IE瀏覽器。
- Ajax long polling,支持所有瀏覽器。
在服務(wù)器端及客戶端都支持的情況下,SignalR默認會選擇最佳的傳輸方式。
1.打開StockTicker.js,然后在客戶端與服務(wù)端建立連接之前加上下面這段代碼。
// Start the connection $.connection.hub.logging = true; $.connection.hub.start().done(init);
2.重新運行程序,并打開瀏覽器的開發(fā)者工具,選擇控制臺標簽,就可以看到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的連接方式。
到此這篇關(guān)于ASP.NET使用 SignalR2實現(xiàn)服務(wù)器廣播的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
CodeFirst從零開始搭建Asp.Net Core2.0網(wǎng)站
這篇文章主要為大家詳細介紹了CodeFirst從零開始搭建Asp.Net Core2.0網(wǎng)站的詳細過程,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-07-07
.net下調(diào)用sqlserver存儲過程的小例子
2013-06-06
Visual Studio 2015和 .NET Core安裝教程
這篇文章主要為大家詳細介紹了Visual Studio Community 2015和 .NET Core安裝圖文教程,感興趣的小伙伴們可以參考一下2016-07-07
.net調(diào)用JScript腳本及JS url加密解密
.net調(diào)用JScript腳本及JS url加密解密,需要的朋友可以參考一下2013-03-03
asp.net下出現(xiàn)其中的組件“訪問被拒絕”的解決方法
asp.net下出現(xiàn)其中的組件“訪問被拒絕”的解決方法...2007-04-04



