一、概述

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

教程:使用 SignalR 2 广播的服务器

可以通过Install-Package Microsoft.AspNet.SignalR.Sample来安装并查看完整的代码。

二、服务器端代码

新建一个名为Stock.cs的实体类,用来作为服务器端推送消息的载体,具体代码如下。

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

using System;

namespace Microsoft.AspNet.SignalR.StockTicker
{
public class Stock
{
private decimal _price; public string Symbol { get; set; } public decimal DayOpen { get; private set; } public decimal DayLow { get; private set; } public decimal DayHigh { get; private set; } public decimal LastChange { get; private set; } public decimal Price
{
get
{
return _price;
}
set
{
if (_price == value)
{
return;
} LastChange = value - _price;
_price = value; if (DayOpen == 0)
{
DayOpen = _price;
}
if (_price < DayLow || DayLow == 0)
{
DayLow = _price;
}
if (_price > DayHigh)
{
DayHigh = _price;
}
}
}

        public decimal Change
{
get
{
return Price - DayOpen;
}
} public double PercentChange
{
get
{
return (double)Math.Round(Change / Price, 4);
}
}
}
}

1、创建StockTicker和StockTickerHub类

我们将使用SignalR Hub API来处理服务器到客户端的交互,所以新建一个继承自SignalR Hub类的StockTickerHub类来处理客户端的连接及调用。除此之外,我们还需要维护股票的价格数据以及新建一个Timer对象来定期的更新价格,而这些都与客户端连接无关的。由于Hub实例的生命周期很短暂,只有在比如客户端连接和调用的时候,Hub类实例是为Hub上的每个这样的操作才会创建新的实例,所以不要把与客户端连接及调用无关的代码放置到SignalR Hub类中。在这里,我们将维护股票数据、模拟更新股票价格以及向客户端推送股票价格的代码放置到一个名为StockTicker的类中。

我们只需要在服务器端运行一个StockTicker类的实例(单例模式),由于这个StockTicker类维护着股票的价格,所以它也要能够将最新的股票价格推送给所有的客户端。为了达到这个目的,我们需要在这单个实例中引用所有的StockTickerHub实例,而这可以通过SignalR Hub的Context对象来获得,然后它可以使用SignalR连接Context对象向客户端广播。

2、StockTickerHub类

这个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。

using Microsoft.AspNet.SignalR.Hubs;
using System.Collections.Generic; namespace Microsoft.AspNet.SignalR.StockTicker
{
[HubName("stockTicker")]
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();
} }
}

接下来我们将会创建StockTicker类,并且创建一个静态实例属性。这样不管有多少个客户端连接或者断开,内存中都只有一个StockTicker类的实例,并且还可以通过该实例的GetAllStocks方法来获得当前的股票数据。

3、StockTicker类

由于所有线程都运行 StockTicker 代码的同一个实例,StockTicker 类必须要是线程安全的。

1、将单个实例保存在一个静态字段中

在这个类中,我们新建了一个名为_instance的字段用来存放该类的实例,并且将构造函数的访问权限设置成私有状态,这样其它的类就只能通过Instance这个静态属性来获得该类的实例,而无法通过关键字new来创建一个新的实例。在这个_instance字段上面,我们使用了Lazy特性,虽然会损失一点儿性能,但是它却可以保证以线程安全的方式来创建实例。

每次客户端连接到服务器时,运行在单独线程中的StockTickerHub类的新实例都会从StockTicker获取StockTicker单例实例。实例静态属性,如前面在StockTickerHub类中看到的。

2、使用ConcurrentDictionary来存放股票数据

这个类定义了一个_stocks字段来存放测试用的股票数据,并且通过GetAllStocks这个方法来进行获取。我们前面讲过客户端会通过StockTickerHub.GetAllStocks来获取当前的股票数据,其实就是这里的股票数据。

在这个测试程序中,我们将数据存直接存放在内存中,这样做并没有什么问题,但在实际的应用场景中,则需要将数据存放在数据库之类的文件中以便长久的保存。

3、定期的更新股票价格

在这个类中,我们定义了一个Timer对象来定期的更新股票的价格。

在实际的场景中,TryUpdateStockPrice方法通常会通过调用第三方的Web Service来获取最新的股票价格,而在这个程序中,我们则是通过随机数来进行模拟该实现。

4、通过SignalR Hub的Context对象来实现服务端的推送

因为股票价格变动是在StockTicker对象中,所以这个对象需要调用客户端的updateStockPrice回调方法来推送数据。在Hub类中,我们可以直接使用API来调用客户端的方法,但是这个StockTicker类并没有继承自Hub,所以无法直接使用这些对象。为了能够向客户端广播数据,StockTicker类需要使用SignalR Hub的Context对象来获得StokTickerHub类的实例,并用它来调用客户端的方法。

using Microsoft.AspNet.SignalR.Hubs;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading; namespace Microsoft.AspNet.SignalR.StockTicker
{
public class StockTicker
{
#region 字段 // 初始化StockTicker为单例实例
private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker( GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients) );
         //在创建StockTicker类静态实例的时候,把SignalR的Context引用通过构造函数传递给Clients这个属性。
         //在这里只需要获取一次SignalR.Context,这样做有2个好处,首先是因为获取SignalR.Context很耗费资源,其次是获取一次SignalR.Context可以保留消息发送到客户端的预定义顺序。

        private readonly object _marketStateLock = new object();
private readonly object _updateStockPricesLock = new object();

        //为了线程安全,我们使用了ConcurrentDictionary来存放股票数据,当然你也可以使用Dictionary对象来进行存储,但是在更新数据之前需要进行锁定。
private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>(); // 控制股票价格波动的百分比
private readonly double _rangePercent = 0.002; private readonly TimeSpan _updateInterval = TimeSpan.FromMilliseconds(250);
private readonly Random _updateOrNotRandom = new Random(); private Timer _timer;

        //使用volatile修饰符来标记_updatingStockPrices变量,该修饰符指示一个字段可以由多个同时执行的线程修改,声明为volatile的字段不受编译器优化(假定由单个线程访问)的限制,这样可以确保该字段在任何时间呈现的都是最新的值。
        //该修饰符通常用于由多个线程访问但不使用lock语句对访问进行序列化的字段。
        private volatile bool _updatingStockPrices;
        #endregion 字段

        #region 构造函数

        private StockTicker(IHubConnectionContext<dynamic> clients)
{
Clients = clients;
            _stocks.Clear();

            var stocks = new List<Stock>
{
new Stock { Symbol = "MSFT", Price = 41.68m },
new Stock { Symbol = "AAPL", Price = 92.08m },
new Stock { Symbol = "GOOG", Price = 543.01m }
}; stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock)); _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);//定时更新股价
} #endregion 构造函数 #region 属性 private IHubConnectionContext<dynamic> Clients { get; set; } //使用Clients属性,可以使您和在Hub类中一样,通过它来调用客户端的方法。 public static StockTicker Instance
{
get
{
return _instance.Value;
}
} #endregion 属性 #region 方法

        //获取所有股价
public IEnumerable<Stock> GetAllStocks()
{
return _stocks.Values;
}

        //在更新之前,我们使用了_updateStockPricesLock对象将需要更新的部份进行锁定,并通过_updatingStockPrices变量来确定是否有其它线程已经更新了股票的价格。
        //然后通过对每一个股票代码执行TryUpdateStockPrice方法来确定是否更新股票价格以及股票价格的波动幅度。如果检测到股票价格变动,将会通过BroadcastStockPrice方法将最新的股票价格推送给每一个连接的客户端。
private void UpdateStockPrices(object state)
{
// 此函数必须可重入re-entrant,因为它是作为计时器间隔处理程序运行的。
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); //Clients.All是dynamic类型的,意味着发送给所有的客户端,同时SignalR还提供了用来指定具体的客户端或组的属性,具体信息可以参考HubConnectionContext
} #endregion 方法
}
}

4、注册SignalR路由

服务器需要知道把哪些请求交由SignalR进行操作,为了实现这个功能,我们需要在OWIN的Startup文件中进行相应的设置。

using Owin;

namespace Microsoft.AspNet.SignalR.StockTicker
{
public static class Startup
{
public static void ConfigureSignalR(IAppBuilder app)
{
// For more information on how to configure your application using OWIN startup, visit http://go.microsoft.com/fwlink/?LinkID=316888 app.MapSignalR();
}
}
}

三、客户端代码

StockTicker.html

页面分别引入了jQuery、SignalR、SignalR代理,以及StockTicker脚本文件。SignalR代理文件(/signalr/hubs)将会根据服务器端编写的Hub文件动态的生成相应的脚本(生成关于StockTickerHub.GetAllStocks的相关代码),如果你愿意,你还可以通过SignalR Utilities来手动生成脚本文件,但是需要在MapHubs方法中禁用动态文件创建的功能。

注意:请确保StockTicker.html文件中引入的脚本文件在你的项目中是实际存在的。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>ASP.NET SignalR Stock Ticker</title>
<link href="StockTicker.css" rel="stylesheet" />
</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>High</th><th>Low</th><th>Change</th><th>%</th></tr>
</thead>
<tbody>
<tr class="loading"><td colspan="7">loading...</td></tr>
</tbody>
</table>
</div> <h2>Live Stock Ticker</h2>
<div id="stockTicker">
<div class="inner">
<ul>
<li class="loading">loading...</li>
</ul>
</div>
</div>
<script src="jquery-1.10.2.min.js"></script>
<script src="jquery.color-2.1.2.min.js"></script>
<script src="../Scripts/jquery.signalR-2.4.1.js"></script>
<script src="../signalr/hubs"></script>
<script src="SignalR.StockTicker.js"></script>
</body>
</html>

StockTicker.js

// Crockford's supplant method (poor man's templating)自定义的模板方法
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;
}
);
};
} // A simple background color flash effect that uses jQuery Color plugin
jQuery.fn.flash = function (color, duration) {
var current = this.css('backgroundColor');
this.animate({ backgroundColor: 'rgb(' + color + ')' }, duration / 2)
.animate({ backgroundColor: current }, duration / 2);
}; $(function () { var ticker = $.connection.stockTicker, //$.connection即是指SignalR代理,这行代码表示将StockTickerHub类的代理的引用保存在变量ticker中,代理的名称即为服务器端通过[HubName]属性设置的名称。
up = '▲',
down = '▼',
$stockTable = $('#stockTable'),
$stockTableBody = $stockTable.find('tbody'),
rowTemplate = '<tr data-symbol="{Symbol}"><td>{Symbol}</td><td>{Price}</td><td>{DayOpen}</td><td>{DayHigh}</td><td>{DayLow}</td><td><span class="dir {DirectionClass}">{Direction}</span> {Change}</td><td>{PercentChange}</td></tr>',
$stockTicker = $('#stockTicker'),
$stockTickerUl = $stockTicker.find('ul'),
liTemplate = '<li data-symbol="{Symbol}"><span class="symbol">{Symbol}</span> <span class="price">{Price}</span> <span class="change"><span class="dir {DirectionClass}">{Direction}</span> {Change} ({PercentChange})</span></li>'; 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,
DirectionClass: stock.Change === 0 ? 'even' : stock.Change >= 0 ? 'up' : 'down'
});
} function scrollTicker() {
var w = $stockTickerUl.width();
$stockTickerUl.css({ marginLeft: w });
$stockTickerUl.animate({ marginLeft: -w }, 15000, 'linear', scrollTicker);
} function stopTicker() {
$stockTickerUl.stop();
}

    //init方法调用服务端的getAllStocks方法,并将返回的数据显示在Table中。服务端我们默认会使用帕斯卡命名法,而SignalR会在生成客户端的代理类时,自动将服务端的方法改成骆驼命名法,不过该规则只对方法名及Hub名称有效。
    //而对于对象的属性名,则仍然和服务器端的一样,比如stock.Symbol、stock.Price,而不是stock.symbol、stock.price。
    //如果你想在客户端使用与服务器商相同的名称(包括大小写),或者想自己定义其它的名称,那么你可以通过给Hub方法加上HubMethodName标签来实现这个功能,而HubName标签则可以实现自定义的Hub名称。
    function init() {
return ticker.server.getAllStocks().done(function (stocks) {
$stockTableBody.empty();
$stockTickerUl.empty();
            //遍历服务端返回的股票数据,然后通过调用formatStock来格式化成我们想要的格式,接着通过supplant方法(在StockTicker.js的最顶端)来生成一条新行,并把这个新行插入到表格里面。
$.each(stocks, function () {
var stock = formatStock(this);
$stockTableBody.append(rowTemplate.supplant(stock));
                $stockTickerUl.append(liTemplate.supplant(stock));
});
});
}

    //为了让服务器能够调用客户的代码,我们需要把updateStockPrice添加到stockTicker代理的client对象中
$.extend(ticker.client, {
updateStockPrice: function (stock) {
var displayStock = formatStock(stock),
$row = $(rowTemplate.supplant(displayStock)),
$li = $(liTemplate.supplant(displayStock)),
bg = stock.LastChange < 0
? '255,148,148' // red
: '154,240,117'; // green
//该updateStockPrice方法和init方法一样,通过调用formatStock来格式化成我们想要的格式,接着通过supplant方法(在StockTicker.js的最顶端)来生成一条新行,不过它并不是将该新行追加到Table中,而是找到Table中现有的行,然后使用新行替换它。
$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);
scrollTicker();
}
}); // 客户端的代码编写好之后,就可以通过最后的这行代码来与服务器建立连接,由于这个start方法执行的是异步操作,并会返回一个jQuery延时对象,所以我们要使用jQuery.done函数来处理连接成功之后的操作。
    //这个init方法其实是在start方法完成异步操作后作为回调函数执行的,如果你把init作为一个独立的JavaScript语句放在start方法之后的话,那么程序将会出错,因为这样会导致服务端的方法在客户端还没有与服务器建立连接之前就被调用。
    $.connection.hub.start().done(init);

});

四、输出日志

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的连接方式。

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

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

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

SignalR入门二、使用 SignalR 2 实现服务器广播的更多相关文章

  1. [置顶] MVC中使用signalR入门教程

    一.前言:每次写总要说一点最近的感想 进入工作快半年了,昨天是最郁闷的一天,我怀疑我是不是得了"星期一综合征",每个星期一很没有状态.全身都有点酸痛,这个可能一个星期只有周末才打一 ...

  2. SignalR系列教程:服务器广播与主动数据推送

    本篇是本系列入门篇的最后一遍,由于工作关系,接触SignalR的时间不是很多.等下次有空的话我会写一个利用“SignalR”开发一个在线聊天室的系列博文.近期的话我更偏向于更新框架设计相关的文章,到时 ...

  3. SignalR 设计理念(二)

    SignalR 设计理念(二) 实现客户端和服务器端的实时通讯. 前言: 客户端方法忽略大小写,主要原因基于是URL对大小写不敏感的问题,开发者之间为了更好的协同开发,定下的开发者协议. 问题阐述 客 ...

  4. SignalR入门一、通过 SignalR 2 进行实时聊天

    一:什么是signalR Asp.net SignalR是微软为实现实时通信的一个类库.一般情况下,signalR会使用JavaScript的长轮询(long polling)的方式来实现客户端和服务 ...

  5. SignalR 入门 .netCore实现聊天室

    SignalR 入门 .netCore实现聊天室 本文根据微软SignalR 简介 | Microsoft Docs 和 ASP.NET Core SignalR 简介 | Microsoft Doc ...

  6. [渣译文] SignalR 2.0 系列:SignalR的服务器广播

    英文渣水平,大伙凑合着看吧…… 这是微软官方SignalR 2.0教程Getting Started with ASP.NET SignalR 2.0系列的翻译,这里是第八篇:SignalR的服务器广 ...

  7. SignalR 2.0 系列:SignalR的服务器广播

    英文渣水平,大伙凑合着看吧…… 这是微软官方SignalR 2.0教程Getting Started with ASP.NET SignalR 2.0系列的翻译,这里是第八篇:SignalR的服务器广 ...

  8. 第六章SignalR的服务器广播

    第六章SignalR的服务器广播 1.概述: VS可以通过 Microsoft.AspNet.SignalR.Sample NuGet包来安装一个简单的模拟股票行情应用.在本教程的第一部分,您将从头开 ...

  9. SignalR的服务器广播

    可以试试 https://github.com/angular-ui/bootstrap 这个框架啊 [渣译文] SignalR 2.0 系列:SignalR的服务器广播 2014-03-13 09: ...

随机推荐

  1. MYSQL --Subquery returns more than 1 row查询结果多于一行

    Subquery returns more than 1 row表示子查询返回了多行数据 例如: select * from table1 where table1.colums=(select co ...

  2. jQuery Ajax async=>false异步改为同步时,导致浏览器假死的处理方法

    今天做一个需求遇到了这么个情况,就是用户个人中心有个功能,点击按钮,可以刷新用户当前的积分,这个肯定需要使用到ajax的同步请求了,当时喀喀喀三下五除二写玩了,大概代码如下: /** * 异步当前用户 ...

  3. day56——http协议、MVC和MTV框架模式、django下载安装、url路由分发

    day56 昨日复习 今日内容 HTTP协议 网页:https://www.cnblogs.com/clschao/articles/9230431.html 老师整理的重点 老师整理的重点 请求信息 ...

  4. C语言环境搭建

    UNIX/Linux 上的安装 如果您使用的是 Linux 或 UNIX,请在命令行使用下面的命令来检查您的系统上是否安装了 GCC: $ gcc -v 如果您的计算机上已经安装了 GNU 编译器,则 ...

  5. Python知识点总结篇(一)

    Python基础 变量 变量类型: 1.数字型 整形:int: 浮点型:float: 布尔型:bool,True和False: 复数型:complex: 2.非数字型 字符串: 列表: 元祖: 字典: ...

  6. Keyboarding

    题目描述 思路 一开始想先写一个bfs,目标字符串要加上一个'*',表示这是一个换行符,然后一个字母一个字母的找,每次重置一下vis数组,bfs返回的结果再加上1,表示要打印这个字母,结果第一个样例没 ...

  7. sqlite 安装与编译

    本文简述了SQLite的概念,并详细描述了SQLite在Linux和Windows平台下的编译方法 关于 SQLite SQLite是一个进程内的库,实现了自给自足的.无服务器的.零配置的.事务性的 ...

  8. Scala Spark WordCount

    Scala所需依赖 <dependency> <groupId>org.scala-lang</groupId> <artifactId>scala-l ...

  9. C#JsonConvert.DeserializeObject反序列化json字符

    需求:需要把第一个id替换掉,在序列化成json dynamic dyn = Newtonsoft.Json.JsonConvert.DeserializeObject(json); foreach ...

  10. linux安装mysql后报错启动不了Starting MySQL. ERROR! The server quit without updating PID file (/var/lib/mysql/localhost.localdomain.pid).

    今天安装完Mysql后,开启发生了错误: 2.打开错误信息文件,查看错误原因是:Plugin 'FEDERATED' is disabled. /usr/sbin/mysqld: Table 'mys ...