英文渣水平,大伙凑合着看吧……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>