VS可以通过 Microsoft.AspNet.SignalR.Sample NuGet包来安装一个简单的模拟股票行情应用。在本教程的第一部分,您将从头开始创建一个应用程序的简化版本。在本教程的剩余部分,您将安装NuGet包,审阅Sample中的一些附加功能。
在本模拟股票行情应用代表了实时应用中的“推” ,或称之为广播,即我们将消息通知传播给所有已连接的客户端。
首先,您将要创建该应用程序的显示表格用于显示股票数据。
接下来,服务器会随机更新股票价格,并且将新数据推送至所有连接的客户端已更新表格。在浏览器中的表格上,价格以及百分比列中的数字都会随着服务器推送数据而自动更新。如果您打开更多的浏览器,它们都会显示相同的数据以及自动更新。
注意:如果您不想自动动手来构建这一应用程序,您可以再新建一个空的Asp.Net应程序项目中安装Simple包,通过阅读里面的步骤来获取代码解释。本例子的第一部分涵盖了Simple的子集,第二部分解释了包中的一些附加功能。
1)新建一个新的Asp.Net应用程序,命名为SignalR.StockTicker并创建
2)选择空项目并且确定。
首先:我们来创建一个Stock模型类,用来存储和传输股票信息。
1)新建一个类,命名为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); } } } } |
您设置了两个属性:股票代码和股票价格。其他的属性则依赖于您如何及何时设置股票价格。当您首次设置价格时,价格将被存储在DayOpen中。之后随着股票价格的改变,Change和PercentCHange会自动计算DayOpen及价格之间的的差额并输出结果。
您将使用SignalR集线器类的API来处理服务器到客户端的交互。StockTickerHub继续SinglaR集线器基类,用来处理接收客户端的连接和调用的方法。您还需要维护保存的数据,建立一个独立于客户端连接的Timer对象,来触发价格更新。您不能将这些功能放在集线器中,那是因为,每个针对集线器的操作,比如从客户端到服务器端的连接与调用都会建立一个新的集线器对象,每个集线器对象的生命周期是短暂的。因此,保存数据,价格,广播等更新机制需要放在另一个单独的类中。在此项目中,我们将其命名为StockTicker。
您只需要一个StockTicker类的实例。所以,你需要使用设计模式中的----单例模式,从每个StockTickerHub的类中添加对StockTicker单一实例的引用。由于StocckTicker类包含股票数据并触发更新,所以它必须能够广播到每个客户端。但StockTicker本身并不是一个集线器类,所以,StockTicker类必须得到一个SignalR集线器连接上下文对象的引用,后面,就可以使用这个上下文对象来将数据广播给客户端。
1)添加一个新的SignalR集线器类,命名为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(); } } } |
此集线器类用来定义用于客户端调用的服务器方法。我们定义了一个GetAllStocks方法,当一个客户端首次连接至服务器时,它会调用此方法来获取所有股票的清单及当期价格。该方法可以同步执行并返回IEnumerable<Sotck>,因为这些数据是从内存中返回的。如果该方法需要做 一些涉及等待的额外处理任务,比如数据库查询或调用Web服务来获取数据,您将指定 Task<IEnumerable<Stock>>作为返回值已启用异步处理。关于异步处理的更多信息,请参阅:ASP.NETSignalR Hubs API Guide - Server - When to execute asynchronously。
HubName特性定义了客户端的JS代码使用何种名称来调用集线器。如果你不使用这个特性,默认将通过采用使用Camel规范的类名来调用。在本例中,我们使用stockTickerHun。
稍后我们将创建StockTicker类,如您所见,我们在这里使用了单例模式。使用一个静态实例属性来创建这个类的单一实例。StockTicker的单例将一直保留在内存中,不管有多少客户端连接或断开连接。并且使用该实例中包含的GetAllStocks方法返回股票信息。
2)添加一个新类,命名为StockTicker.cs,并使用以下代码替换内容:
a.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Hubs;
namespace SignalR.StockTicker { public class StockTicker { // Singleton instance 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();
//stock can go up or down by a percentage of this factor on each change private readonly double _rangePercent = .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 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 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) { // Randomly choose whether to update this stock or not var r = _updateOrNotRandom.NextDouble(); if (r > .1) { return false; }
// Update the stock price by a random factor of the range percent 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; }
private void BroadcastStockPrice(Stock stock) { Clients.All.updateStockPrice(stock); }
} } |
注意:由于运行时会有多个线程对StockTicker的同一个实例进行操作,StockTicker类必须是线程安全的。
下面的代码用于在静态_instance字段中初始化一个StockTicker的实例。这是该类的唯一一个实例,因为构造函数已经被标记为私有的。_instance中的延迟初始化不是由于性能原因,而是要确保该线程的创建是线程安全的。
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的新实例,之后从StockTicker.Instance静态属性中获取StockTicker的单例,如同你之前在StockTickerHub之前见到的那样。
构造函数初始化了_stock集合并且初始化了一些样本数据并使用GetAllStocks返回股票数据。如前所述,客户端可以调用服务器端StockTickerHub集线器中的GetAllStocks方法用来返回股票数据集合到客户端。
private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>(); private StockTicker(IHubConnectionContext 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对象并在对其进行修改时显式的锁定它来确保线程安全。对于本示例,股票数据都存储在内存中,所以当应用程序重启时你会丢失所有的数据。在实际的应用中,你应该将数据安全的存放在后端(比如SQL数据库中)。
构造函数启动一个定时器来定期更新股票数据,股价以随机抽样的方式来随机变更。
_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) { // Randomly choose whether to update this stock or not var r = _updateOrNotRandom.NextDouble(); if (r > .1) { return false; }
// Update the stock price by a random factor of the range percent 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; } |
定时器会定时调用UpdateStockPrices方法,在更新价格之前,_updateStockPricesLock对象被锁住。代码检查是否有另 一个线程在更新价格,然后调用TryUpdateStockPrice方法来对列表中的股票进行逐一更新。TryUpdateStockPrice方法将判断是否需要更新股价以及更新多少。如果股票价格发生变化,BroadcastPrice方法将变动的数据广播到所有已连接的客户端上。
_updateStockPrices标识被标记为volatile以确保访问是线程安全的。
private volatile bool _updatingStockPrices = false; |
在实际应用中,TryUpdateStockPrice方法可能会调用Web服务来查找股价;在本示例中,它使用一个随机数来模拟股价的变化。
由于价格变动发生于StockTicker对象,该对象需要在所有已连接客户端上调用updateStockPrice方法。在集线器类中,你有现成的 API来调用客户端方法。但StockTicker类没有从集线器类派生,所以没有引用到集线器的基类对象。因此,为了对客户端广播,StockTicker类需要获取SignalR上下文的实例并用它来调用客户端上的方法。
该代码会在创建单例的时候获取SignalR上下文的引用,将引用传递给构造函数,使构造函数能够将它放置在Clients属性中。
有两个原因使你只应该得到一次上下文:获取上下文是一个昂贵的操作,并且仅获得一次可以确保发送到客户端的消息顺序是有序的。
private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));
private StockTicker(IHubConnectionContext clients) { Clients = clients;
// Remainder of constructor ... }
private IHubConnectionContext Clients { get; set; }
private void BroadcastStockPrice(Stock stock) { Clients.All.updateStockPrice(stock); } |
获取上下文中的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中的内容:
using System; using System.Threading.Tasks; using Microsoft.Owin; using Owin;
[assembly: OwinStartup(typeof(SignalR.StockTicker.Startup))]
namespace SignalR.StockTicker { public class Startup { public void Configuration(IAppBuilder app) { // Any connection or hub wire up and configuration should go here app.MapSignalR(); }
} } |
现在你已经完成了全部的服务器端代码,接下来我们将配置客户端。
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文件中的内容。
// A simple templating method for replacing placeholders enclosed in curly braces. 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, // the generated client-side hub proxy 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)); }); }); }
// Add a client-side hub method that the server will call ticker.client.updateStockPrice = function (stock) { var displayStock = formatStock(stock), $row = $(rowTemplate.supplant(displayStock));
$stockTableBody.find(‘tr[data-symbol=‘ + stock.Symbol + ‘]‘) .replaceWith($row); }
// Start the connection $.connection.hub.start().done(init);
}); |
$.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启动应用程序。
表格最初显示“正在加载”,在初始化股票数据后,显示最初的股票价格,之后便会随着股价变动而开始改变。
2)复制多个浏览器窗口,你会看到同第一步一样的情况,之后所有浏览器会同时根绝股价发生变化。
3)关闭所有浏览器,再打开一个新的,打开相同的URL你会看到股票价格仍在改变(你看不到初始化时显示初始股价的数字及信息),这是由于stockTicker单例继续在服务器上运行。
4)关闭浏览器。
SignalR有个一内置的日志功能,您可以启动它来进行Bug排查,本节我们将展示这一功能。关于SignalR针对IIS以及浏览器所不同的传输方式,请参见前几章教程。
1)打开stockTicker.js并添加一行代码来启动日志。
// Start the connection
$.connection.hub.logging = true;
$.connection.hub.start().done(init);
|
2)按下F5开始运行项目。
3)打开浏览器中的开发者工具,可能需要刷新页面建立一个新连接才能看到SignalR的传输方式。
原文地址:http://blog.csdn.net/hhhhbk/article/details/44829083