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

英文渣水平,大伙凑合着看吧……javascript

这是微软官方SignalR 2.0教程Getting Started with ASP.NET SignalR 2.0系列的翻译,这里是第八篇:SignalR的服务器广播php

原文: Tutorial: Server Broadcast with SignalR 2.0css

概述

VS能够经过 Microsoft.AspNet.SignalR.Sample  NuGet包来安装一个简单的模拟股票行情应用。在本教程的第一部分,您将从头开始建立一个应用程序的简化版本。在本教程的剩余部分,您将安装NuGet包,审阅Sample中的一些附加功能。html

本模拟股票行情应用表明了实时应用中的"推",或称之为广播,即咱们将消息通知传播给全部已链接客户端。java

第一步,您将要建立该应用程序的显示表格用于显示数据。python

接下来,服务器会随机更新股票价格,而且将新数据推送至全部链接的客户端以更新表格。在浏览器中的表格上,价格及百分比列中的数字都会随着服务器推送数据而自动更新。若是你打开更多的浏览器,它们都会显示相同的数据及自动更新。jquery

注意:若是您你不想本身手动来构建这一应用程序,你能够再一个新的空ASP.NET WEB应用项目中安装Simple包,经过阅读这些步骤来获取代码的解释。本教程的第一部分涵盖了Sample的子集,第二部分解释了包中的一些附加功能。git

建立项目

1.新建一个新的ASP.NET应用程序,命名为SignalR.StockTicker并建立。程序员

2.选择空项目并肯定。github

编写服务器代码

在本节中,咱们来编写服务器端代码。

建立Stock类

首先咱们来建立一个Stock模型类,用来存储和传输股票信息。

1.新建一个类,命名为Stock.cs,而后输入如下代码:

 1 using System; 2 3 namespace SignalR.StockTicker 4 { 5 public class Stock 6 { 7 private decimal _price; 8 9 public string Symbol { get; set; } 10 11 public decimal Price 12 { 13 get 14 { 15 return _price; 16 } 17 set 18 { 19 if (_price == value) 20 { 21 return; 22 } 23 24 _price = value; 25 26 if (DayOpen == 0) 27 { 28 DayOpen = _price; 29 } 30 } 31 } 32 33 public decimal DayOpen { get; private set; } 34 35 public decimal Change 36 { 37 get 38 { 39 return Price - DayOpen; 40 } 41 } 42 43 public double PercentChange 44 { 45 get 46 { 47 return (double)Math.Round(Change / Price, 4); 48 } 49 } 50 } 51 }

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

建立StockTicker及StockTickerHub类

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

你只须要一个StockTicker类的实例。因此你须要使用设计模式中的单例模式,从每一个StockTickerHub的类中添加对StockTicker单一实例的引用。因为StockTicker类包含股票数据并触发更新,因此它必须可以广播到每一个客户端。但StockTicker自己并非一个集线器类,因此StockTicker类必须获得一个SignalR集线器链接上下文对象的引用,以后就可使用这个上下文对象来将数据广播给客户端。

1.添加一个新的SignalR集线器类,命名为StockTickerHub并使用如下的代码替换其内容:

 1 using System.Collections.Generic; 2 using Microsoft.AspNet.SignalR; 3 using Microsoft.AspNet.SignalR.Hubs; 4 5 namespace SignalR.StockTicker 6 { 7 [HubName("stockTickerMini")] 8 public class StockTickerHub : Hub 9 { 10 private readonly StockTicker _stockTicker; 11 12 public StockTickerHub() : this(StockTicker.Instance) { } 13 14 public StockTickerHub(StockTicker stockTicker) 15 { 16 _stockTicker = stockTicker; 17 } 18 19 public IEnumerable<Stock> GetAllStocks() 20 { 21 return _stockTicker.GetAllStocks(); 22 } 23 } 24 }

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

  1 using System; 2 using System.Collections.Concurrent; 3 using System.Collections.Generic; 4 using System.Threading; 5 using Microsoft.AspNet.SignalR; 6 using Microsoft.AspNet.SignalR.Hubs; 7 8 9 namespace SignalR.StockTicker 10 { 11 public class StockTicker 12 { 13 // Singleton instance 14 private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients)); 15 16 private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>(); 17 18 private readonly object _updateStockPricesLock = new object(); 19 20 //stock can go up or down by a percentage of this factor on each change 21 private readonly double _rangePercent = .002; 22 23 private readonly TimeSpan _updateInterval = TimeSpan.FromMilliseconds(250); 24 private readonly Random _updateOrNotRandom = new Random(); 25 26 private readonly Timer _timer; 27 private volatile bool _updatingStockPrices = false; 28 29 private StockTicker(IHubConnectionContext clients) 30 { 31 Clients = clients; 32 33 _stocks.Clear(); 34 var stocks = new List<Stock> 35 { 36 new Stock { Symbol = "MSFT", Price = 30.31m }, 37 new Stock { Symbol = "APPL", Price = 578.18m }, 38 new Stock { Symbol = "GOOG", Price = 570.30m } 39 }; 40 stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock)); 41 42 _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval); 43 44 } 45 46 public static StockTicker Instance 47 { 48 get 49 { 50 return _instance.Value; 51 } 52 } 53 54 private IHubConnectionContext Clients 55 { 56 get; 57 set; 58 } 59 60 public IEnumerable<Stock> GetAllStocks() 61 { 62 return _stocks.Values; 63 } 64 65 private void UpdateStockPrices(object state) 66 { 67 lock (_updateStockPricesLock) 68 { 69 if (!_updatingStockPrices) 70 { 71 _updatingStockPrices = true; 72 73 foreach (var stock in _stocks.Values) 74 { 75 if (TryUpdateStockPrice(stock)) 76 { 77 BroadcastStockPrice(stock); 78 } 79 } 80 81 _updatingStockPrices = false; 82 } 83 } 84 } 85 86 private bool TryUpdateStockPrice(Stock stock) 87 { 88 // Randomly choose whether to update this stock or not 89 var r = _updateOrNotRandom.NextDouble(); 90 if (r > .1) 91 { 92 return false; 93 } 94 95 // Update the stock price by a random factor of the range percent 96 var random = new Random((int)Math.Floor(stock.Price)); 97 var percentChange = random.NextDouble() * _rangePercent; 98 var pos = random.NextDouble() > .51; 99 var change = Math.Round(stock.Price * (decimal)percentChange, 2); 100 change = pos ? change : -change; 101 102 stock.Price += change; 103 return true; 104 } 105 106 private void BroadcastStockPrice(Stock stock) 107 { 108 Clients.All.updateStockPrice(stock); 109 } 110 111 } 112 }

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

在静态字段中存储单例

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

1 private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients)); 2 3 public static StockTicker Instance 4 { 5 get 6 { 7 return _instance.Value; 8 } 9 }

每次客户端链接到服务器时,都会在单独的一个线程中建立StockTickerHub的新实例,以后从StockTicker.Instance静态属性中获取StockTicker的单例,如同你以前在StockTickerHub以前见到的那样。

在ConcurrentDictory中存放股票数据

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

 1 private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>(); 2 private StockTicker(IHubConnectionContext clients) 3 { 4 Clients = clients; 5 6 _stocks.Clear(); 7 var stocks = new List<Stock> 8 { 9 new Stock { Symbol = "MSFT", Price = 30.31m }, 10 new Stock { Symbol = "APPL", Price = 578.18m }, 11 new Stock { Symbol = "GOOG", Price = 570.30m } 12 }; 13 stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock)); 14 15 _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval); 16 } 17 18 public IEnumerable<Stock> GetAllStocks() 19 { 20 return _stocks.Values; 21 }

股票集合被定义为一个ConcurrentDictionary类以确保线程安全。做为替代,你可使用Dictionary对象并在对其进行修改时显式的锁定它来确保线程安全。

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

按期更新股票价格

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

 1 _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval); 2 3 private void UpdateStockPrices(object state) 4 { 5 lock (_updateStockPricesLock) 6 { 7 if (!_updatingStockPrices) 8 { 9 _updatingStockPrices = true; 10 11 foreach (var stock in _stocks.Values) 12 { 13 if (TryUpdateStockPrice(stock)) 14 { 15 BroadcastStockPrice(stock); 16 } 17 } 18 19 _updatingStockPrices = false; 20 } 21 } 22 } 23 24 private bool TryUpdateStockPrice(Stock stock) 25 { 26 // Randomly choose whether to update this stock or not 27 var r = _updateOrNotRandom.NextDouble(); 28 if (r > .1) 29 { 30 return false; 31 } 32 33 // Update the stock price by a random factor of the range percent 34 var random = new Random((int)Math.Floor(stock.Price)); 35 var percentChange = random.NextDouble() * _rangePercent; 36 var pos = random.NextDouble() > .51; 37 var change = Math.Round(stock.Price * (decimal)percentChange, 2); 38 change = pos ? change : -change; 39 40 stock.Price += change; 41 return true; 42 }

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

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

private volatile bool _updatingStockPrices = false;

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

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

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

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

有两个缘由使你只应该获得一次上下文:获取上下文是一个昂贵的操做,而且仅得到一次能够确保发送到客户端的消息顺序是有序的。

 1 private readonly static Lazy<StockTicker> _instance = 2 new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients)); 3 4 private StockTicker(IHubConnectionContext clients) 5 { 6 Clients = clients; 7 8 // Remainder of constructor ... 9 } 10 11 private IHubConnectionContext Clients 12 { 13 get; 14 set; 15 } 16 17 private void BroadcastStockPrice(Stock stock) 18 { 19 Clients.All.updateStockPrice(stock); 20 }

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

 1 using System; 2 using System.Threading.Tasks; 3 using Microsoft.Owin; 4 using Owin; 5 6 [assembly: OwinStartup(typeof(SignalR.StockTicker.Startup))] 7 8 namespace SignalR.StockTicker 9 { 10 public class Startup 11 { 12 public void Configuration(IAppBuilder app) 13 { 14 // Any connection or hub wire up and configuration should go here 15 app.MapSignalR(); 16 } 17 18 } 19 }

如今你已经完成了所有的服务器端代码,接下来咱们将配置客户端。

配置客户端代码

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文件中的内容:

 1 // A simple templating method for replacing placeholders enclosed in curly braces. 2 if (!String.prototype.supplant) { 3 String.prototype.supplant = function (o) { 4 return this.replace(/{([^{}]*)}/g, 5 function (a, b) { 6 var r = o[b]; 7 return typeof r === 'string' || typeof r === 'number' ? r : a; 8 } 9 ); 10 }; 11 } 12 13 $(function () { 14 15 var ticker = $.connection.stockTickerMini, // the generated client-side hub proxy 16 up = '▲', 17 down = '▼', 18 $stockTable = $('#stockTable'), 19 $stockTableBody = $stockTable.find('tbody'), 20 rowTemplate = '<tr data-symbol="{Symbol}"><td>{Symbol}</td><td>{Price}</td><td>{DayOpen}</td><td>{Direction} {Change}</td><td>{PercentChange}</td></tr>'; 21 22 function formatStock(stock) { 23 return $.extend(stock, { 24 Price: stock.Price.toFixed(2), 25 PercentChange: (stock.PercentChange * 100).toFixed(2) + '%', 26 Direction: stock.Change === 0 ? '' : stock.Change >= 0 ? up : down 27 }); 28 } 29 30 function init() { 31 ticker.server.getAllStocks().done(function (stocks) { 32 $stockTableBody.empty(); 33 $.each(stocks, function () { 34 var stock = formatStock(this); 35 $stockTableBody.append(rowTemplate.supplant(stock)); 36 }); 37 }); 38 } 39 40 // Add a client-side hub method that the server will call 41 ticker.client.updateStockPrice = function (stock) { 42 var displayStock = formatStock(stock), 43 $row = $(rowTemplate.supplant(displayStock)); 44 45 $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']') 46 .replaceWith($row); 47 } 48 49 // Start the connection 50 $.connection.hub.start().done(init); 51 52 });

$.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平台及工具团队的高级程序员,做家。

相关文章
相关标签/搜索