SignalR系列教程:服务器广播与主动数据推送
本篇是本系列入门篇的最后一遍,由于工作关系,接触SignalR的时间不是很多。等下次有空的话我会写一个利用“SignalR”开发一个在线聊天室的系列博文。近期的话我更偏向于更新框架设计相关的文章,到时候我会在文章中分享我在工作中开发的“日志框架”、“缓存框架”,“分布式下载框架”等。有兴趣的朋友可以关注我,一起交流。
本篇博文参考:https://www.asp.net/signalr/overview/getting-started/tutorial-server-broadcast-with-signalr
本教程演示如何创建一个 web 应用程序使用 ASP.NET SignalR 2 提供的服务器广播功能。服务器广播意味着发送到客户端的通信由服务器启动。我们之前聊天室的项目是一个用户提交数据后,服务器接收到消息,然后把消息广播给当前所有的用户。如下图
本教程所讲的恰恰相反,我们是由服务器自动把消息推送给当前所有用户。如股票信息显示:
概述
在本教程中,我们将会创建一个股票行情自动收录的实时应用程序,在其中您想要定期"推"送数据,通知从服务器到所有连接的客户端。在本教程的第一部分,你将从头开始创建该应用程序的简化的版本。在本教程的其余部分中,您会安装 NuGet 包,其中包含额外的功能,并审查这些功能的代码。
创建项目
我们依然新建一个空项目,并使用“程序包管理控制台”执行“Install-PackAge Microsoft.AspNet.SignaLR”安装最新版本的SignaLR。
安装完成后会自动打开“readme.txt”文件,文中告诉我们要新建一个Startup并注册SignalR。原文如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | To enable SignalR in your application, create a class called Startup with the following: using Microsoft.Owin; using Owin; using MyWebApplication; namespace MyWebApplication { public class Startup { public void Configuration(IAppBuilder app) { app.MapSignalR(); } } } |
我们按照它的来,新建一个Startup然后在Configuration中注册SignalR路由。
创建代码
我们创建Stock用来存放股票详细信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | 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); } } } |
创建StockTicker和StockTickerHub的类
我们将在StockTickerHub类中定义和JS交互的代码。我们需要维护股票数据的更新和删除,但是我们不能在StockTickerHub类中进行操作,因为StockTickerHub类是不保存数据的,如果我们把股票的CURD代码放在StockTickerHub中可能会造成我们的数据丢失。我们利用VS添加新项选择集线器V2,并替换成以下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | [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(); } } |
在StockTickerHub类中,我们公开了一个GetAllStocks的方法,当客户端连接成功后我们将调用“GetAllStocks”方法用来显示股票信息。HubName标签是一个别名,按照传统我们连接需要用StockTickerHub,但是我们加了HubName后,前段就可以通过stockTickerMini来创建连接。
我们新建一个StockTicker类,把替换成以下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | public class StockTicker { private static readonly Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients)); /// <summary> /// ConcurrentDictionary 表示可以由多个线程访问的安全集合 /// 用来存放股票代码以及股票价格 /// </summary> 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<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 > .1) { return false ; } 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必须是线程安全的。
好了,现在完成了基本的配置,我们创建一个名为index.html的文件并把它设置为启动项。index.html的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | <!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 src= "Scripts/jquery-1.6.4.min.js" ></script> <script src= "Scripts/jquery.signalR-2.2.0.js" ></script> <script src= "/signalr/hubs" ></script> <script src= "StockTicker.js" ></script> </body> </html> |
在项目新建一个名为StockTicker.js的文件,并用以下代码替换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | 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, 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的代理,StockTickerMini是StockTickerHub类HubName所设置的别名。
在所有的变量和函数定义后,会在最后一行启动SignalR连接,并进行初始化。运行起来即可看到效果
请大家注意一下StockTicker类中的BroadcastStockPrice方法,这个方法最终会获取当前所有的连接用户,并触发updateStockPrice方法。在前两章的时候我们都是由客户端主动调用Hub类的某一个方法,然后在由方法内部进行触发前台JS代码。在本章中,我们定义了一个定时器用来定时更新数据,每当数据发生修改时就会主动触发updateStockPrice,这也是本章与前两章不同的地方。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· 展开说说关于C#中ORM框架的用法!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?