使用SignalR实现服务端消息推送

概述

这篇文章参考的是Server Broadcast with SignalR 2这篇教程,很不错的一篇教程,若是有兴趣的话能够查看原文,今天记录下来做为一个学习笔记,这样从此翻阅会更方便一点。javascript

这篇教程经过实现一个股票报价的小程序来说解如何使用SignalR进行服务器端的推送,服务器会模拟股票价格的波动,并把最新的股票价格推送给全部链接的客户端,最终的运行效果以下图所示。php

运行结果

教程篇

建立项目

1.打开Visual Studio,而后选择新建项目。
2.在New Project对话框中,点击Visual C#下的Web,而后新建一个名为SignalR.StockTickerASP.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); } } } }

这个实体类只有SymbolPrice这两个属性须要设置,其它属性将会依据Price自动进行计算。java

建立StockTicker和StockTickerHub类

咱们将会使用SignalR Hub API来处理服务器与客户端的交互,因此新建一个继承自SignalR Hub的StockTickerHub类来处理客户端的链接及调用。除此以外,咱们还须要维护股票的价格数据以及新建一个Timer对象来按期的更新价格,而这些都是独立于客户端的链接的。因为Hub的生命周期很短暂,只有在客户端链接和调用的时候才会建立新的实例(没有研究过SignalR的源代码,我以为更确切一点儿应该是每当有新的客户端链接成功时,服务器就会建立一个新的Hub实例,并经过该实例来与客户端进行通讯,就像Socket通讯中服务器端会将全部的客户端Socket放到一个统一的集合中进行维护),因此不要把与客户端链接及调用无关的代码放置到SignalR Hub类中。在这里,咱们将维护股票数据、模拟更新股票价格以及向客户端推送股票价格的代码放置到一个名为StockTicker的类中。jquery

StockTicker与Hub的实例图解

咱们只须要在服务器端运行一个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实例,就像以前介绍的那样。

使用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...”,不过紧接着便会被服务器端的股票数据替换掉,而且这些股票数据会随着服务器的推送而不停的发生改变。

运行结果,显示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的链接方式。

IE-WebSocket链接方式

若是你是在Windows 7(IIS 7.5)上用IE10打开的话,将会看到使用iframe的链接方式。

IE-IFrame链接方式

Windows 8(IIS 8)上用Firefox的话,将会看到WebSocket的链接方式。

Firefox-WebSocket链接方式

在Windows 7(IIS 7.5)上用Firefox打开的话,将会看到使用Server-sent events的链接方式。

Firefox-Server-sent链接方式

安装并查看完整的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来进行安装。

安装SignalR.Sample包

3.在Solution Explorer里,将会看到项目根目录多了一个名为SignalR.Sample的文件夹,这里即是所有的代码。

4.在SignalR.Sample文件夹下,右击StockTicker.html,选择Set As Start Page,将其设置为启动文件。

注意:安装SignalR.Sample包可能会改变你本地原用的jQuery的版本,在启动程序以前要注意核对文件的版本是否正确。

运行程序

运行程序后,你会看到和以前相似的表格,不过除了在表格里显示最新的股票价格以外,该页面还会有一个水平的滚动条来显示相同的股票价格。不过与前面的教程不一样的是,这个页面须要你点击Open Market按钮以后才会开始接收服务器的推送数据。

SignalR.Sample运行结果

当点击Open Market以后,Live Stock Ticker开始滚动显示股票价格,而在Table里面则会经过不一样的颜色来区分股价的上涨以及下跌。

SignalR.Sample运行结果2

点击Close Market按钮之后,Live Stock TableLive 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。

……

相关文章
相关标签/搜索