使用SignalR实现服务端消息推送

概述

这篇文章参考的是Server Broadcast with SignalR 2这篇教程,很不错的一篇教程,如果有兴趣的话可以查看原文,今天记录下来作为一个学习笔记,这样今后翻阅会更方便一点。

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

运行结果

教程篇

创建项目

1.打开Visual Studio,然后选择新建项目。
2.在New Project对话框中,点击Visual C#下的Web,然后新建一个名为SignalR.StockTickerASP.NET Web Application项目。
3.在New ASP.NET窗口中,选择Empty模板,然后点击OK来创建项目。

服务器端代码

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

using System;

namespace SignalR.StockTicker
{
    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);
            }
        }
    }
}

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

创建StockTicker和StockTickerHub类

我们将会使用SignalR Hub API来处理服务器与客户端的交互,所以新建一个继承自SignalR Hub的StockTickerHub类来处理客户端的连接及调用。除此之外,我们还需要维护股票的价格数据以及新建一个Timer对象来定期的更新价格,而这些都是独立于客户端的连接的。由于Hub的生命周期很短暂,只有在客户端连接和调用的时候才会创建新的实例(没有研究过SignalR的源代码,我觉得更确切一点儿应该是每当有新的客户端连接成功时,服务器就会创建一个新的Hub实例,并通过该实例来与客户端进行通信,就像Socket通信中服务器端会将所有的客户端Socket放到一个统一的集合中进行维护),所以不要把与客户端连接及调用无关的代码放置到SignalR Hub类中。在这里,我们将维护股票数据、模拟更新股票价格以及向客户端推送股票价格的代码放置到一个名为StockTicker的类中。

StockTicker与Hub的实例图解

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

Ps: 渣英语,上面2段出处的原文帖在下面,留做以后查看吧。

You'll use the SignalR Hub API to handle server-to-client interaction. A StockTickerHub class that derives from the SignalR Hub class will handle receiving connections and method calls from clients. You also need to maintain stock data and run a Timer object to periodically trigger price updates, independently of client connections. You can't put these functions in a Hub class, because Hub instances are transient. A Hub class instance is created for each operation on the hub, such as connections and calls from the client to the server. So the mechanism that keeps stock data, updates prices, and broadcasts the price updates has to run in a separate class, which you'll name StockTicker.

You only want one instance of the StockTicker class to run on the server, so you'll need to set up a reference from each StockTickerHub instance to the singleton StockTicker instance. The StockTicker class has to be able to broadcast to clients because it has the stock data and triggers updates, but StockTicker is not a Hub class. Therefore, the StockTicker class has to get a reference to the SignalR Hub connection context object. It can then use the SignalR connection context object to broadcast to clients.

1.在Solution Explorer中,右键项目,通过Add | SignalR Hub Class(V2)新建一个名为StockTickerHub.cs的文件。

2.在StockTickerHub类中输入下面这段代码。

using System.Collections.Generic;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;

namespace SignalR.StockTicker
{
    [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();
        }
    }
}

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

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

4.在项目中创建一个名为StockTicker的新类,并在类中输入下面这段代码。

using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;

namespace SignalR.StockTicker
{
    public class StockTicker
    {
        // 单例模式
        private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));
        private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();
        private readonly object _updateStockPricesLock = new object();

        // 控制股票价格波动的百分比
        private readonly double _rangePercent = 0.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 > 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);
        }
    }
}

由于StockTicker类的实例涉及到多线程,所以该类需要是线程安全的。

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

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

private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));

public static StockTicker Instance
{
	get
	{
		return _instance.Value;
	}
}

每当有客户端与服务器建立连接的时候,一个新的StockTickerHub实例将会在一个独立的线程中被创建,并通过SockTicker.Instance属性来获得唯一的StockTicker实例,就像之前介绍的那样。

使用ConcurrentDictionary来存放股票数据

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

private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();

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 IEnumerable<Stock> GetAllStocks()
{
    return _stocks.Values;
}

为了线程安全,我们使用了ConcurrentDictionary来存放股票数据,当然你也可以使用Dictionary对象来进行存储,但是在更新数据之前需要进行锁定。

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

定期的更新股票价格

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

_timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);

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; }

    // 使用Random来模拟股票价格的更新
    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;
}

Timer对象通过调用UpdateStockPrices方法,并向该方法传递一个null来更新股票的价格。在更新之前,我们使用了_updateStockPricesLock对象将需要更新的部份进行锁定,并通过_updatingStockPrices变量来确定是否有其它线程已经更新了股票的价格。然后通过对每一个股票代码执行TryUpdateStockPrice方法来确定是否更新股票价格以及股票价格的波动幅度。如果检测到股票价格变动,将会通过BroadcastStockPrice方法将最新的股票价格推送给每一个连接的客户端。

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

private volatile bool _updatingStockPrices = false;

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

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

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

下面这段代码演示了在创建StockTicker类静态实例的时候,把SignalR的Context引用通过构造函数传递给Clients这个属性。在这里只需要获取一次SignalR.Context,这样做有2个好处,首先是因为获取SignalR.Context很耗费资源,其次是获取一次SignalR.Context可以保留消息发送到客户端的预定义顺序(The intended order of messages sent to clients is preserved)。

private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));

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

    // 构造函数的余下代码...
}

private IHubConnectionContext<dynamic> Clients
{
    get;
    set;
}

private void BroadcastStockPrice(Stock stock)
{
    Clients.All.updateStockPrice(stock);
}

使用Clients属性,可以使您和在Hub类中一样,通过它来调用客户端的方法。在BroadcastStockPrice方法中调用的updateStockPrice方法实际并不存在,呆会我们将会在客户端的脚本中实现该方法。因为Clients.All是dynamic类型的,也就是说在程序运行的时候会对这个表达式进行动态赋值,所以这里可以直接使用它。当这个方法被调用的时候,SignalR将会把这个方法的名称以及参数一并发给客户端,如果客户端存在一个名称为updateStockPrice的方法的话,那么就会将参数传递给该方法并调用它。

Clients.All意味着发送给所有的客户端,同时SignalR还提供了用来指定具体的客户端或组的属性,具体信息可以参考HubConnectionContext

注册SignalR路由

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

1.首先打开vs的Solution Explorer,在项目上右击,然后依次点击Add | OWIN Startup Class按钮,添加一个名为Startup.cs的类。

2.在Startup.cs类中输入下面这段代码。

using Owin;
using Microsoft.Owin;

[assembly: OwinStartup(typeof(SignalR.StockTicker.Startup))]

namespace SignalR.StockTicker
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.MapSignalR();
        }
    }
}

现在我们已经完成了服务端的编码工作,接下来我们需要完成客户端的代码。

编写客户端的代码

1.在项目的根目录下,创建一个名为StockTicker.html的HTML文件。

2.在HTML文件中输入下面这段代码。

<!DOCTYPE html>
<html>
<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.10.2.js"></script>
    <script src="/Scripts/jquery.signalR-2.1.2.js"></script>
    <script src="/signalr/hubs"></script>
    <script src="StockTicker.js"></script>
</body>
</html>

上面的HTML代码创建了一个2行5列的表格,因为默认并没有数据,所以第2行显示的是“loading”,当程序运行的时候,这个提示行将被实际的数据覆盖掉。接下来则分别引入了jQuery、SignalR、SignalR代理,以及StockTicker脚本文件。SignalR代理文件(/signalr/hubs)将会根据服务器端编写的Hub文件动态的生成相应的脚本(生成关于StockTickerHub.GetAllStocks的相关代码),如果你愿意,你还可以通过SignalR Utilities来手动生成脚本文件,但是需要在MapHubs方法中禁用动态文件创建的功能。

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

3.在Solution Explorer中,右击StockTicker.html,然后点击Set as Start Pagae菜单。

4.在项目的根目录下创建一个名为StockTicker.js的脚本文件。

5.在脚本文件中,输入下面这段代码。

// 自定义的模板方法
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,  // 客户端的Hub代理
        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代理,下面这行代码表示将StockTickerHub类的代理的引用保存在变量ticker中,代理的名称即为服务器端通过[HubName]属性设置的名称。

// 客户端引用的代码
var ticker = $.connection.stockTickerMini

// 服务器端的代码
[HubName("stockTickerMini")]
public class StockTickerHub : Hub

客户端的代码编写好之后,就可以通过最后的这行代码来与服务器建立连接,由于这个start方法执行的是异步操作,并会返回一个jQuery延时对象,所以我们要使用jQuery.done函数来处理连接成功之后的操作。

$.connection.hub.start().done(init);

init方法会调用服务端的getAllStocks方法,并将返回的数据显示在Table中。也许你可能注意到这里的getAllStocks方法名和服务器端的GetAllStocks其实并不一样,这是因为服务端我们默认会使用帕斯卡命名法,而SignalR会在生成客户端的代理类时,自动将服务端的方法改成骆驼命名法,不过该规则只对方法名及Hub名称有效,而对于对象的属性名,则仍然和服务器端的一样,比如stock.Symbol、stock.Price,而不是stock.symbol、stock.price。

// 客户端的代码
function init() {
    ticker.server.getAllStocks().done(function (stocks) {
        $stockTableBody.empty();
        $.each(stocks, function () {
            var stock = formatStock(this);
            $stockTableBody.append(rowTemplate.supplant(stock));
        });
    });
}

// 服务器端的代码
public IEnumerable<Stock> GetAllStocks()
{
    return _stockTicker.GetAllStocks();
}

如果你想在客户端使用与服务器商相同的名称(包括大小写),或者想自己定义其它的名称,那么你可以通过给Hub方法加上HubMethodName标签来实现这个功能,而HubName标签则可以实现自定义的Hub名称。

在这个init方法中,我们会遍历服务端返回的股票数据,然后通过调用formatStock来格式化成我们想要的格式,接着通过supplant方法(在StockTicker.js的最顶端)来生成一条新行,并把这个新行插入到表格里面。

这个init方法其实是在start方法完成异步操作后作为回调函数执行的,如果你把init作为一个独立的JavaScript语句放在start方法之后的话,那么程序将会出错,因为这样会导致服务端的方法在客户端还没有与服务器建立连接之前就被调用。

当服务器端的股票价格变动的时候,它就会通过调用已连接的客户端上的updateStockPrice方法来更新数据。为了让服务器能够调用客户的代码,我们需要把updateStockPrice添加到stockTicker代理的client对象中,代码如下。

ticker.client.updateStockPrice = function (stock) {
    var displayStock = formatStock(stock),
        $row = $(rowTemplate.supplant(displayStock));

    $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
                   .replaceWith($row);
}

该updateStockPrice方法和init方法一样,通过调用formatStock来格式化成我们想要的格式,接着通过supplant方法(在StockTicker.js的最顶端)来生成一条新行,不过它并不是将该新行追加到Table中,而是找到Table中现有的行,然后使用新行替换它。

测试程序

1.按下F5启动程序,就会看到Table中显示的“loading...”,不过紧接着便会被服务器端的股票数据替换掉,并且这些股票数据会随着服务器的推送而不停的发生改变。

运行结果,显示loading...

运行结果,获得服务器端的股票数据

运行结果,显示服务器端推送数据

2.复制页面的地址,并用其它的浏览器打开,就会看到相同的数据,以及相同的价格变化。

3.关闭所有的浏览器,然后重新在浏览器中打开这个页面,就会看到这次页面显示的速度(股票价格推送)要比第一次快的多,而且第一次看到的股票的价格是有数据的,而不是像第一次那样都显示为0,这是因为服务器端的StockTicker静态实例仍然在运行。

输出日志

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

IE-WebSocket连接方式

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

IE-IFrame连接方式

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

Firefox-WebSocket连接方式

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

Firefox-Server-sent连接方式

安装并查看完整的StockTicker.Sample代码

至此,我们已经实现了最基本的服务端推送功能,如果你想查看更多功能的话,可以通过NuGet包管理器来安装这个程序的完整版本(Microsoft.AspNet.SignalR.Sample),如果你没有按照上面的教程操作而是直接从NuGet服务器上获取这个测试程序的话,那么你可能需要在OWIN的Startup类中进行相关的设置,具体说明可以参考文件夹内的readme.txt文件。

安装SignalR.Sample的NuGet包

1.在Solution Explorer内右击项目,然后点击Manage NuGet Packages

2.在Manage NuGet Packages对话框中,选择Online,然后在搜索框中输入SignalR.Sample,当搜索结果出来后,在SignalR.Sample包的后面点击Install来进行安装。

安装SignalR.Sample包

3.在Solution Explorer里,将会看到项目根目录多了一个名为SignalR.Sample的文件夹,这里便是全部的代码。

4.在SignalR.Sample文件夹下,右击StockTicker.html,选择Set As Start Page,将其设置为启动文件。

注意:安装SignalR.Sample包可能会改变你本地原用的jQuery的版本,在启动程序之前要注意核对文件的版本是否正确。

运行程序

运行程序后,你会看到和之前类似的表格,不过除了在表格里显示最新的股票价格之外,该页面还会有一个水平的滚动条来显示相同的股票价格。不过与前面的教程不同的是,这个页面需要你点击Open Market按钮之后才会开始接收服务器的推送数据。

SignalR.Sample运行结果

当点击Open Market之后,Live Stock Ticker开始滚动显示股票价格,而在Table里面则会通过不同的颜色来区分股价的上涨以及下跌。

SignalR.Sample运行结果2

点击Close Market按钮以后,Live Stock TableLive Stock Ticker将会停止显示股票价格的波动,而此时如果点击Reset按钮的话,那么页面上的股票价格将会变成初始状态。而对于Live Stock Ticker的实现,和上面的Table类似,只不过是使用了<li>标签,以及用到了jQuery的animate函数。

Live Stock Ticker的HTML代码:

<h2>Live Stock Ticker</h2>
<div id="stockTicker">
    <div class="inner">
        <ul>
            <li class="loading">loading...</li>
        </ul>
    </div>
</div>

Live Stock Ticker的CSS代码:

#stockTicker {overflow: hidden;width: 450px;height: 24px;border: 1px solid #999;}
#stockTicker .inner {width: 9999px;}
#stockTicker ul {display: inline-block;list-style-type: none;margin: 0;padding: 0;}
#stockTicker li {display: inline-block;margin-right: 8px;}
#stockTicker .symbol {font-weight: bold;}
#stockTicker .change {font-style: italic;}

使其滚动的jQuery代码:

function scrollTicker() {
    var w = $stockTickerUl.width();
    $stockTickerUl.css({ marginLeft: w });
    $stockTickerUl.animate({ marginLeft: -w }, 15000, 'linear', scrollTicker);
}

在服务器端添加客户端可以调用的方法

和之前一样,我们需要SignalRHub类中添加可供客户端调用的方法。

public string GetMarketState()
{
    return _stockTicker.MarketState.ToString();
}

public void OpenMarket()
{
    _stockTicker.OpenMarket();
}

public void CloseMarket()
{
    _stockTicker.CloseMarket();
}

public void Reset()
{
    _stockTicker.Reset();
}

OpenMarket、CloseMarket和Reset方法对应着页面顶部的3个按钮,只要有一个客户端操作了这3个按钮,那么所有连接的客户端就都会受到影响,因为每一个用户都可以设置推送状态,并将该状态广播给所有的客户端。

在StockTicker类中,这个推送状态通过MarketState属性来维护,它是一个MarketState枚举类型。

public MarketState MarketState
{
    get { return _marketState; }
    private set { _marketState = value; }
}

public enum MarketState
{
    Closed,
    Open
}

为了确保线程安全,在StockTicker类中修改推送状态的时候,需要进行加锁处理。

public void OpenMarket()
{
    lock (_marketStateLock)
    {
        if (MarketState != MarketState.Open)
        {
            _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
            MarketState = MarketState.Open;
            BroadcastMarketStateChange(MarketState.Open);
        }
    }
}

public void CloseMarket()
{
    lock (_marketStateLock)
    {
        if (MarketState == MarketState.Open)
        {
            if (_timer != null)
            {
                _timer.Dispose();
            }
            MarketState = MarketState.Closed;
            BroadcastMarketStateChange(MarketState.Closed);
        }
    }
}

public void Reset()
{
    lock (_marketStateLock)
    {
        if (MarketState != MarketState.Closed)
        {
            throw new InvalidOperationException("Market must be closed before it can be reset.");
        }
        LoadDefaultStocks();
        BroadcastMarketReset();
    }
}

为了确保线程安全,我们为_marketState字段加上volatile标识符。

private volatile MarketState _marketState;

BroadcastMarketStateChange和BroadcastMarketReset方法与之前写的BroadcastStockPrice方法很像,只不过调用客户端的方法有区别而已。

private void BroadcastMarketStateChange(MarketState marketState)
{
    switch (marketState)
    {
        case MarketState.Open:
            Clients.All.marketOpened();
            break;
        case MarketState.Closed:
            Clients.All.marketClosed();
            break;
        default:
            break;
    }
}

private void BroadcastMarketReset()
{
    Clients.All.marketReset();
}

客户端可以调用的新增函数

现在updateStockPrice方法要负责维护table和ul数据的显示,并通过jQuery.Color来为股票价格的上涨或者下跌进行着色。

SignalR.StockTicker.js里新增的方法通过推送状态(MarketState)来启用或者禁用操作按钮,同时还决定着table及ul里的数据是否刷新。我们可以通过jQuery.extend方法来把这几个方法添加到ticker.client对象中。

$.extend(ticker.client, {
    updateStockPrice: function (stock) {
        var displayStock = formatStock(stock),
            $row = $(rowTemplate.supplant(displayStock)),
            $li = $(liTemplate.supplant(displayStock)),
            bg = stock.LastChange === 0
                ? '255,216,0' // yellow
                : stock.LastChange > 0
                    ? '154,240,117' // green
                    : '255,148,148'; // red

        $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);
    },

    marketOpened: function () {
        $("#open").prop("disabled", true);
        $("#close").prop("disabled", false);
        $("#reset").prop("disabled", true);
        scrollTicker();
    },

    marketClosed: function () {
        $("#open").prop("disabled", false);
        $("#close").prop("disabled", true);
        $("#reset").prop("disabled", false);
        stopTicker();
    },

    marketReset: function () {
        return init();
    }
});

建立连接后客户端的其它设置

当客户端与服务器端建立连接后,还有一些额外的工作需要做,比如根据当前的推送状态来决定调用服务器端的方法,以及将调用服务器端的方法绑定到相应的按钮上。

$.connection.hub.start()
    .pipe(init)
    .pipe(function () {
        return ticker.server.getMarketState();
    })
    .done(function (state) {
        if (state === 'Open') {
            ticker.client.marketOpened();
        } else {
            ticker.client.marketClosed();
        }

        // Wire up the buttons
        $("#open").click(function () {
            ticker.server.openMarket();
        });

        $("#close").click(function () {
            ticker.server.closeMarket();
        });

        $("#reset").click(function () {
            ticker.server.reset();
        });
	});

在done方法中绑定按钮事件是为了确保客户端在与服务器建立连接后才能够调用服务器的方法。

以上就是SignalR.Sample的主要代码,如果有兴趣的话,可以通过Install-Package Microsoft.AspNet.SignalR.Sample来安装并查看完整的代码。

posted @ 2016-12-21 21:22  isrocking  阅读(7831)  评论(3编辑  收藏  举报