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,然后输入以下代码:

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

您设置了两个属性:股票代码及价格。其他的属性则依赖于你如何及何时设置股票价格。当您首次设定价格时,价格将被存储在DayOpen中。之后随着股票价格的改变,Change和PercentChange会自动计算DayOpen及价格之间的差额并输出结果。

创建StockTicker及StockTickerHub类

您将使用SignalR集线器类的API来处理服务器到客户端的交互。StockTickerHub衍生自SignalR集线器基类,用来处理接收客户端的连接和调用方法。你还需要维护存储的数据,建立一个独立于客户端连接的Timer对象,来触发价格更新。你不能将这些功能放在集线器中,因为每个针对集线器的操作,比如从客户端到服务器端的连接与调用都会建立一个集线器的新实例,每个集线器的实例生存期是短暂的。因此,保存数据,价格,广播等更新机制需要放在一个单独的类中。在此项目中我们将其命名为StockTicker。

你只需要一个StockTicker类的实例。所以你需要使用设计模式中的单例模式,从每个StockTickerHub的类中添加对StockTicker单一实例的引用。由于StockTicker类包含股票数据并触发更新,所以它必须能够广播到每个客户端。但StockTicker本身并不是一个集线器类,所以StockTicker类必须得到一个SignalR集线器连接上下文对象的引用,之后就可以使用这个上下文对象来将数据广播给客户端。

1.添加一个新的SignalR集线器类,命名为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();
         }
     }
 }

此集线器类用来定义用于客户端调用的服务器方法。我们定义了一个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,并使用以下代码替换内容:

 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Threading;
 using Microsoft.AspNet.SignalR;
 using Microsoft.AspNet.SignalR.Hubs;
 
 
 namespace SignalR.StockTicker
 {
     public class StockTicker
     {
         // Singleton instance
         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();
 
         //stock can go up or down by a percentage of this factor on each change
         private readonly double _rangePercent = .;
 
         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 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 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)
         {
             // Randomly choose whether to update this stock or not
             var r = _updateOrNotRandom.NextDouble();
             if (r > .)
             {
                 return false;
             }
 
             // Update the stock price by a random factor of the range percent
             var random = new Random((int)Math.Floor(stock.Price));
             var percentChange = random.NextDouble() * _rangePercent;
             var pos = random.NextDouble() > .;
             var change = Math.Round(stock.Price * (decimal)percentChange, );
             change = pos ? change : -change;
 
             stock.Price += change;
             return true;
         }
 
         private void BroadcastStockPrice(Stock stock)
         {
             Clients.All.updateStockPrice(stock);
         }
 
     }
 }

由于运行时会有多个线程对StockTicker的同一个实例进行操作,StockTicker类必须是线程安全的。

在静态字段中存储单例

下面的代码用于在静态_instance字段中初始化一个StockTicker的实例。这是该类的唯一一个实例,因为构造函数已经被标记为私有的。_instance中的延迟初始化不是由于性能原因,而是要确保该线程的创建是线程安全的。

 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的新实例,之后从StockTicker.Instance静态属性中获取StockTicker的单例,如同你之前在StockTickerHub之前见到的那样。

在ConcurrentDictory中存放股票数据

构造函数初始化了_stock集合并且初始化了一些样本数据并使用GetAllStocks返回股票数据。如前所述,客户端可以调用服务器端StockTickerHub集线器中的GetAllStocks方法用来返回股票数据集合到客户端。

 private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();
 private StockTicker(IHubConnectionContext 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对象并在对其进行修改时显式的锁定它来确保线程安全。

对于本示例,股票数据都存储在内存中,所以当应用程序重启时你会丢失所有的数据。在实际的应用中,你应该将数据安全的存放在后端(比如SQL数据库中)。

定期更新股票价格

构造函数启动一个定时器来定期更新股票数据,股价以随机抽样的方式来随机变更。

 _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)
 {
     // Randomly choose whether to update this stock or not
     var r = _updateOrNotRandom.NextDouble();
     if (r > .)
     {
         return false;
     }
 
     // Update the stock price by a random factor of the range percent
     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, );
     change = pos ? change : -change;
 
     stock.Price += change;
     return true;
 }

定时器会定时调用UpdateStockPrices方法,在更新价格之前,_updateStockPricesLock对象被锁住。代码检查是否有另一个线程在更新价格,然后调用TryUpdateStockPrice方法来对列表中的股票进行逐一更新。TryUpdateStockPrice方法将判断是否需要更新股价以及更新多少。如果股票价格发生变化,BroadcastPrice方法将变动的数据广播到所有已连接的客户端上。

_updateStockPrices标识被标记为volatile以确保访问是线程安全的。

private volatile bool _updatingStockPrices = false;

在实际应用中,TryUpdateStockPrice方法可能会调用Web服务来查找股价;在本示例中,它使用一个随机数来模拟股价的变化。

获取SignalR上下文,以便StockTicker类对其调用来广播到客户端

由于价格变动发生于StockTicker对象,该对象需要在所有已连接客户端上调用updateStockPrice方法。在集线器类中,你有现成的API来调用客户端方法。但StockTicker类没有从集线器类派生,所以没有引用到集线器的基类对象。因此,为了对客户端广播,StockTicker类需要获取SignalR上下文的实例并用它来调用客户端上的方法。

该代码会在创建单例的时候获取SignalR上下文的引用,将引用传递给构造函数,使构造函数能够将它放置在Clients属性中。

有两个原因使你只应该得到一次上下文:获取上下文是一个昂贵的操作,并且仅获得一次可以确保发送到客户端的消息顺序是有序的。

 private readonly static Lazy<StockTicker> _instance =
     new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));
 
 private StockTicker(IHubConnectionContext clients)
 {
     Clients = clients;
 
     // Remainder of constructor ...
 }
 
 private IHubConnectionContext Clients
 {
     get;
     set;
 }
 
 private void BroadcastStockPrice(Stock stock)
 {
     Clients.All.updateStockPrice(stock);
 }

获取上下文中的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中的内容:

 using System;
 using System.Threading.Tasks;
 using Microsoft.Owin;
 using Owin;
 
 [assembly: OwinStartup(typeof(SignalR.StockTicker.Startup))]
 
 namespace SignalR.StockTicker
 {
     public class Startup
     {
         public void Configuration(IAppBuilder app)
         {
             // Any connection or hub wire up and configuration should go here
             app.MapSignalR();
         }
 
     }
 }

现在你已经完成了全部的服务器端代码,接下来我们将配置客户端。

配置客户端代码

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></tr>
            </tbody>
        </table>
    </div>

    <!--Script references. -->
    <!--Reference the jQuery library. -->
    <script src="/Scripts/jquery-1.10.2.min.js" ></script>
    <!--Reference the SignalR library. -->
    <script src="/Scripts/jquery.signalR-2.0.0.js"></script>
    <!--Reference the autogenerated SignalR hub script. -->
    <script src="/signalr/hubs"></script>
    <!--Reference the StockTicker script. -->
    <script src="StockTicker.js"></script>
</body>
</html>

我们在HTML中创建了一个具有5列,一个标题和跨越所有5列的单个单元格的Table,数据行显示为“正在加载”,并且只会在应用程序启动时一度显示。JS代码将会删除改行并在相同的卫视添加从服务器检索到的股票数据。

script标签指定了jQuery脚本文件,SignalR核心脚本文件,SignalR代理脚本文件以及你即将创建的StockTicker脚本文件。在SignalR代理脚本文件中,指定了"/signalr/hub"URL,这是动态生成的,是集线器方法中定义好的方法的代理方法。在本示例中为StockTickerHub.GetAllStocks。如果你愿意,你可以手动生成该JS文件,通过使用SignalR 组件和在调用MapHubs方法时禁用动态文件创建来实现相同的功能。

3.重要提示:请确保JS文件都得到了正确的引用,即检查script标签中引用的jQuery等文件路径和你项目中的JS脚本文件名称一致。

4.右击StockTicker.html,将其设置为起始页。

5.在项目文件夹中创建一个新的JS文件,命名为StockTicker.js并保存。

6.使用下面的代码替换掉StockTicker.js文件中的内容:

 // A simple templating method for replacing placeholders enclosed in curly braces.
 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, // the generated client-side hub proxy
         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));
             });
         });
     }
 
     // Add a client-side hub method that the server will call
     ticker.client.updateStockPrice = function (stock) {
         var displayStock = formatStock(stock),
             $row = $(rowTemplate.supplant(displayStock));
 
         $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
             .replaceWith($row);
         }
 
     // Start the connection
     $.connection.hub.start().done(init);
 
 });

$.connection引用SignalR代理,来获取引用到代理类的StockTickerHub类,并放置在ticker变量中。代理名称是由HubName特性所指定的。

var ticker = $.connection.stockTickerMini
[HubName("stockTickerMini")]
public class StockTickerHub : Hub

当所有变量及函数都定义完成之后,代码文件中的最后一行通过调用SignalR start函数来初始化SignalR连接。start函数将异步执行并返回一个jQuery的递延对象,这意味着你可以在异步操作后调用函数来完成指定的功能。

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

init函数调用服务器上的getAllStocks方法,并使用服务器返回的数据来更新股票表格中的信息。请注意,在默认情况下你必须在客户端上使用camel命名规范来调用服务器端的Pascal命名规范的方法。另外camel命名规范仅适用于方法而不是对象。例如要使用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();
}

如果你想在客户端上使用Pascal命名规范,或者你想使用一个完全不同的方法名,你可以使用HubMethodName特性来修饰集线器方法, 如同使用HubName来修饰集线器类一样。

在init方法中,接收到从服务器传来股票信息后,会清除table row的HTML,然后通过ormatStock来格式化股票对象,之后将其附加到表格中。

在执行异步启动函数后 ,作为回调函数,调用init方法。如果你将init作为单独的JS对象在start函数中调用,函数将会失败,因为它会立即执行而不会等待启动功能来完成连接。在本例中,init函数会在服务器连接建立后再去调用getAllStocks函数。

当服务器改变了股票的价格,它调用已连接客户端的updateStockPrice。该函数被添加到stockTicker代理的客户端属性中,使其可以从服务器端调用。

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

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

如同inti函数一样,updateStockPrice函数格式化从服务器接收到的股票对象并插入表格中。而不是附加到表格的行后面,它会发现当前表格中的股票行并使用新的数据替换掉。

测试应用程序

1. 按下F5启动应用程序。 原文有问题,这里建议使用右击HTML文档,然后选择在浏览器中查看,否则第3步关闭浏览器后就停止调试了,无法看到单例模式的效果。

表格最初将显示“正在加载”,在初始化股票数据后,显示最初的股票价格,之后便会随着股价变动而开始改变。

2.复制多个浏览器窗口,你会看到同第一步一样的情况,之后所有浏览器会同时根据股价发生变化。

3.关闭所有浏览器,再打开一个新的,打开相同的URL你会看到股票价格仍在改变(你看不到初始化时表显示初始股价的数字及信息),这是由于stockTicker单例继续在服务器上运行。

4.关闭浏览器。

启用日志记录

SignalR有一个内置的日志功能,您可以启动它以便进行故障排除,本节我们将展示这一功能。

关于SignalR针对IIS及浏览器所不同的传输方式,请参见前几章教程。

1.打开stockTicker.js并添加一行代码来启动日志。

// Start the connection
$.connection.hub.logging = true;
$.connection.hub.start().done(init);

2.按下F5开始运行项目。

3.打开浏览器中的开发者工具,可能需要刷新页面建立一个新连接才能看到SignalR的传输方式。

安装并检视完整版StockTicker示例

你刚才创建的只是一个简化版的StockTicker应用,在本节教程中,您将安装NuGet包来获取一个完整功能的StockTicker。

安装NuGet包

1.在解决方案资源管理器中右击该项目,然后单击管理NuGet程序包。

2.在管理NuGet程序包对话框中,单机联机,然后再搜索框中输入SignalR.Sample,找到Microsoft.AspNet.SignalR.Sample,安装它。

3.在解决方案资源管理器中,展开SignalR.Sample文件夹。

4.右键单击SignalR.Sample文件夹下的StockTicker.html,将其设置为起始页。

注意:安装Sample可能会改变jQuery,SignalR等包的版本,如果你想运行之前你创建的StockTicker,你需要打开HTML并核对引用的JS文件是否同Sctipts文件夹中的脚本版本一致。

运行应用程序

1.按下F5运行应用程序。

注意:如果提示如下的错误,请升级相应的NuGet包到指定版本。

如果程序正常运行,除了您之前看到的包含股票信息的表格,还会有一条水平滚动的窗口来显示实时股价,如同大多数股票市场里的那样。当你首次运行应用程序时,市场是关闭的(注意那个按钮),你会看到一个静态的表格和股票窗口。

当你单击开市按钮,实时股价框开始水平移动,并且服务器开始周期性地广播股价变动,每次股价的变化都会引起表格及水平框中数字的更新。当股价变化为正时,会显示一个绿色的背景,为负时则显示红色。

闭市按钮将停止变化,终止股票滚动,重设按钮将复位所有的股价到开始变动前的初始状态。如果你打开更多浏览器窗口,你将在窗口中看到相同的变化。

实时股票行情显示器

实时股票行情显示器是一个无序列表,放置在一个div元素中并由css格式化为单行显示。如同表格一样,它也被初始化和更新:通过替换在li标签之间的占位符及动态添加li元素到ul元素中。滚动是通过使用jQuery的animate函数来实现的。

HTML:

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

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

    /*<li data-symbol="{Symbol}"><span class="symbol">{Symbol}</span><span class="price">{Price}</span><span class="change">{PercentChange}</span></li>*/
    #stockTicker .symbol {
        font-weight: bold;
    }

    #stockTicker .change {
        font-style: italic;
    }

使它滚动起来的JS:

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

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

StockTickerHub类定义了客户端可以调用的额外四个方法:

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被页面的顶部按钮调用。每一种方法都是调用StockTicker类的对应方法,影响市场变化并广播新状态。

在StockTicker类,市场的状态由一个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属性后的_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函数现在同时处理股票表格及股票显示器,它使用jQuery.Color来刷新红色与绿色。

在SignalR.StockTicker.js中的新函数启用或禁用市场状态按钮,他们停止或启动股票窗口的水平滚动。由于多个函数被添加到客户端,我们使用了jQuery.extend 函数来添加它们。

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

在建立连接后附加客户端设置

在客户端成功建立连接后,有一些附加工作要做:查找市场是开放还是关闭并调用marketOpened或marketClosed函数,并将服务器方法附加到按钮上。

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

在连接建立以前,服务器方法不会和按钮动作进行连接,所以代码不会在它们之前的时候去尝试调用服务器方法。

接下来

在本教程中,您学会了如何编写广播来将服务器消息传递给所有客户端,包括周期及通知响应。采用多线程单例模式来维持服务器的状态,也可以同时使用在多用户在线的游戏场景中,有关示例请参阅the ShootR game that is based on SignalR

作者:

帕特里克·弗莱彻 -帕特里克·弗莱彻是ASP.NET开发团队的程序员,作家,目前正在SignalR项目工作。

汤姆· 戴卡斯特拉 - 汤姆· 戴卡斯特拉是微软Web平台及工具团队的高级程序员,作家。