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的代理,StockTickerMiniStockTickerHubHubName所设置的别名。

在所有的变量和函数定义后,会在最后一行启动SignalR连接,并进行初始化。运行起来即可看到效果

请大家注意一下StockTicker类中的BroadcastStockPrice方法,这个方法最终会获取当前所有的连接用户,并触发updateStockPrice方法。在前两章的时候我们都是由客户端主动调用Hub类的某一个方法,然后在由方法内部进行触发前台JS代码。在本章中,我们定义了一个定时器用来定时更新数据,每当数据发生修改时就会主动触发updateStockPrice,这也是本章与前两章不同的地方。

 

posted @   大壮他哥  阅读(4136)  评论(5编辑  收藏  举报
编辑推荐:
· 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,谁才是开发者新宠?
点击右上角即可分享
微信分享提示