SignalR 2.0 系列:SignalR的服务器广播
英文渣水平,大伙凑合着看吧……
这是微软官方SignalR 2.0教程Getting Started with ASP.NET SignalR 2.0系列的翻译,这里是第八篇:SignalR的服务器广播
原文: Tutorial: Server Broadcast with SignalR 2.0
概述
VS可以通过 Microsoft.AspNet.SignalR.Sample NuGet包来安装一个简单的模拟股票行情应用。在本教程的第一部分,您将从头开始创建一个应用程序的简化版本。在本教程的剩余部分,您将安装NuGet包,审阅Sample中的一些附加功能。
本模拟股票行情应用代表了实时应用中的"推",或称之为广播,即我们将消息通知传播给所有已连接客户端。
第一步,您将要创建该应用程序的显示表格用于显示数据。
接下来,服务器会随机更新股票价格,并且将新数据推送至所有连接的客户端以更新表格。在浏览器中的表格上,价格及百分比列中的数字都会随着服务器推送数据而自动更新。如果你打开更多的浏览器,它们都会显示相同的数据及自动更新。
注意:如果您你不想自己手动来构建这一应用程序,你可以再一个新的空ASP.NET WEB应用项目中安装Simple包,通过阅读这些步骤来获取代码的解释。本教程的第一部分涵盖了Sample的子集,第二部分解释了包中的一些附加功能。
创建项目
1.新建一个新的ASP.NET应用程序,命名为SignalR.StockTicker并创建。
2.选择空项目并确定。
编写服务器代码
在本节中,我们来编写服务器端代码。
创建Stock类
首先我们来创建一个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及价格之间的差额并输出结果。
创建StockTicker及StockTickerHub类
您将使用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之前见到的那样。
在ConcurrentDictory中存放股票数据
构造函数初始化了_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服务来查找股价;在本示例中,它使用一个随机数来模拟股价的变化。
获取SignalR上下文,以便StockTicker类对其调用来广播到客户端
由于价格变动发生于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 。
注册SignalR路由
服务器需要知道那个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></