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

警告

本文档不适用于最新版本的 SignalR。 查看 ASP.NET Core SignalR

本教程演示如何创建使用 ASP.NET SignalR 2 提供服务器广播功能的 Web 应用程序。 服务器广播意味着服务器启动发送到客户端的通信。

本教程中创建的应用程序模拟股票代码,这是服务器广播功能的典型方案。 服务器会定期随机更新股票价格,并将更新广播给所有连接的客户端。 在浏览器中, “更改 ”和列中的数字和 % 符号会动态更改,以响应来自服务器的通知。 如果向同一 URL 打开其他浏览器,它们都会同时显示相同的数据和对数据的相同更改。

显示多个 Web 浏览器如何同时显示相同的更新数据的屏幕截图。

在本教程中,你将了解:

  • 创建项目
  • 设置服务器代码
  • 检查服务器代码
  • 设置客户端代码
  • 检查客户端代码
  • 测试应用程序
  • 启用日志记录

重要

如果不想完成生成应用程序的步骤,可以在新的空 ASP.NET Web 应用程序项目中安装 SignalR.Sample 包。 如果在未执行本教程中的步骤的情况下安装 NuGet 包,则必须按照 readme.txt 文件中的说明进行操作。 若要运行包,需要添加一个 OWIN 启动类,该类在已安装的包中调用 ConfigureSignalR 方法。 如果未添加 OWIN 启动类,将收到错误。 请参阅本文 的安装 StockTicker 示例 部分。

先决条件

创建项目

本部分介绍如何使用 Visual Studio 2017 创建空 ASP.NET Web 应用程序。

  1. 在 Visual Studio 中,创建 ASP.NET Web 应用程序。

    显示如何创建 ASP.NET Web 应用程序的屏幕截图。

  2. “新建 ASP.NET Web 应用程序 - SignalR.StockTicker ”窗口中,选择“ ”并选择“ 确定”。

设置服务器代码

在本部分中,将设置在服务器上运行的代码。

创建 Stock 类

首先创建 Stock 模型类,用于存储和传输有关股票的信息。

  1. “解决方案资源管理器”中,右键单击项目并选择“添加>”。

  2. 将类命名为 Stock ,并将其添加到项目中。

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

    创建股票时将设置的两个属性 (Symbol 例如 MSFT for Microsoft) 和 Price。 其他属性取决于设置 Price的方式和时间。 首次设置 Price时,值将传播到 DayOpen。 之后,当你设置 Price时,应用会根据 和 之间的差异Price计算 ChangePercentChangeDayOpen属性值。

创建 StockTickerHub 和 StockTicker 类

你将使用 SignalR 中心 API 来处理服务器到客户端的交互。 StockTickerHub派生自 SignalR Hub 类的类将处理从客户端接收连接和方法调用。 还需要维护库存数据并运行 Timer 对象。 对象 Timer 将定期触发独立于客户端连接的价格更新。 不能将这些函数放在类中 Hub ,因为中心是暂时性的。 应用为中心上的每个任务创建类 Hub 实例,例如从客户端到服务器的连接和调用。 因此,保留股票数据、更新价格和广播价格更新的机制必须运行在单独的类中。 你将将类 StockTicker命名为 。

从 StockTicker 广播

你只希望类的 StockTicker 一个实例在服务器上运行,因此需要设置从每个 StockTickerHub 实例到单一实例 StockTicker 的引用。 类 StockTicker 必须广播到客户端,因为它具有库存数据并触发更新,但 StockTicker 不是类 Hub 。 类 StockTicker 必须获取对 SignalR Hub 连接上下文对象的引用。 然后,它可以使用 SignalR 连接上下文对象广播到客户端。

创建 StockTickerHub.cs

  1. “解决方案资源管理器”中,右键单击项目并选择“添加新>”。

  2. “添加新项 - SignalR.StockTicker”中,选择“ 已安装>的 Visual C#>Web>SignalR ”,然后选择“ signalR Hub 类” (v2)

  3. 将类命名 为 StockTickerHub 并将其添加到项目中。

    此步骤创建 StockTickerHub.cs 类文件。 同时,它向项目添加一组支持 SignalR 的脚本文件和程序集引用。

  4. StockTickerHub.cs 文件中的代码替换为以下代码:

    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();
            }
        }
    }
    
  5. 保存该文件。

应用使用 Hub 类定义客户端可以在服务器上调用的方法。 你正在定义一种方法: GetAllStocks()。 当客户端最初连接到服务器时,它将调用此方法以获取所有股票及其当前价格的列表。 方法可以同步运行并返回 IEnumerable<Stock> ,因为它从内存中返回数据。

如果 方法必须通过执行涉及等待的操作(如数据库查找或 Web 服务调用)来获取数据,则需指定 Task<IEnumerable<Stock>> 为返回值以启用异步处理。 有关详细信息,请参阅 ASP.NET SignalR Hubs API 指南 - 服务器 - 何时异步执行

属性 HubName 指定应用如何引用客户端上的 JavaScript 代码中的中心。 如果不使用此属性,客户端上的默认名称是类名的 camelCase 版本,在本例中为 stockTickerHub

稍后在创建 StockTicker 类时将看到,应用在其静态 Instance 属性中创建该类的单一实例实例。 的单一实例实例 StockTicker 在内存中,无论有多少客户端连接或断开连接。 该方法使用该实例 GetAllStocks() 返回当前股票信息。

创建 StockTicker.cs

  1. “解决方案资源管理器”中,右键单击项目并选择“添加>”。

  2. 将类命名 为 StockTicker 并将其添加到项目中。

  3. 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 = .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)
            {
                // Randomly choose whether to update this stock or not
                var r = _updateOrNotRandom.NextDouble();
                if (r > .1)
                {
                    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, 2);
                change = pos ? change : -change;
    
                stock.Price += change;
                return true;
            }
    
            private void BroadcastStockPrice(Stock stock)
            {
                Clients.All.updateStockPrice(stock);
            }
    
        }
    }
    

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

检查服务器代码

如果检查服务器代码,它将帮助你了解应用的工作原理。

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

代码使用 类的实例初始化支持 Instance 属性的静态_instance字段。 由于构造函数是私有的,因此它是应用可以创建的类的唯一实例。 应用对 _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 所示。

在 ConcurrentDictionary 中存储库存数据

构造函数使用一些示例股票数据初始化 _stocks 集合,并 GetAllStocks 返回股票。 如前所述,此股票集合由 StockTickerHub.GetAllStocks返回,这是客户端可以调用的 Hub 类中的服务器方法。

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 对象并在对字典进行更改时显式锁定字典。

对于此示例应用程序,可以将应用程序数据存储在内存中,并在应用释放 StockTicker 实例时丢失数据。 在实际应用程序中,可以使用后端数据存储(如数据库)。

定期更新股票价格

构造函数启动一个 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)
{
    // Randomly choose whether to update this stock or not
    var r = _updateOrNotRandom.NextDouble();
    if (r > .1)
    {
        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, 2);
    change = pos ? change : -change;

    stock.Price += change;
    return true;
}

Timer 调用 UpdateStockPrices,它将在 state 参数中传递 null。 在更新价格之前,应用会锁定 _updateStockPricesLock 对象。 该代码检查另一个线程是否已在更新价格,然后调用 TryUpdateStockPrice 列表中的每个股票。 方法 TryUpdateStockPrice 决定是否更改股票价格,以及要更改的股票价格。 如果股票价格发生变化,应用会调用 BroadcastStockPrice 以向所有连接的客户端广播股价变化。

标志 _updatingStockPrices 指定 为 volatile ,以确保它是线程安全的。

private volatile bool _updatingStockPrices = false;

在实际应用程序中, TryUpdateStockPrice 方法将调用 Web 服务来查找价格。 在此代码中,应用使用随机数生成器进行随机更改。

获取 SignalR 上下文,以便 StockTicker 类可以广播到客户端

由于价格更改源自 StockTicker 对象,因此该对象需要在所有连接的客户端上调用方法 updateStockPrice 。 在类中 Hub ,你有一个用于调用客户端方法的 API,但不 StockTicker 派生自 Hub 类,也没有对任何 Hub 对象的引用。 若要广播到连接的客户端, StockTicker 类必须获取类的 SignalR 上下文实例 StockTickerHub ,并使用该实例在客户端上调用方法。

代码在创建单一实例类实例时获取对 SignalR 上下文的引用,并将该引用传递给构造函数,然后构造函数将其放入 属性中 Clients

你只想要获取上下文一次有两个原因:获取上下文是一项昂贵的任务,获取上下文一次可确保应用保留发送到客户端的消息的预期顺序。

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

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

    // Remainder of constructor ...
}

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

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

Clients获取上下文的 属性并将其放入 StockTickerClient 属性后,可以编写代码来调用与类中Hub外观相同的客户端方法。 例如,若要广播到所有客户端,可以编写 Clients.All.updateStockPrice(stock)

updateStockPrice要调用BroadcastStockPrice的方法尚不存在。 稍后在编写在客户端上运行的代码时,将添加它。 可以在此处引用, updateStockPrice 因为 Clients.All 是动态的,这意味着应用将在运行时计算表达式。 执行此方法调用时,SignalR 会将方法名称和参数值发送到客户端,如果客户端有一个名为 updateStockPrice的方法,则应用将调用该方法并将参数值传递给它。

Clients.All 表示发送到所有客户端。 SignalR 提供了其他选项来指定要发送到哪些客户端或客户端组。 有关详细信息,请参阅 HubConnectionContext

注册 SignalR 路由

服务器需要知道要截获和定向到 SignalR 的 URL。 为此,请添加 OWIN 启动类:

  1. “解决方案资源管理器”中,右键单击项目并选择“添加新>”。

  2. “添加新项 - SignalR.StockTicker”中 ,选择 “已安装>的 Visual C#>Web ”,然后选择“ OWIN 启动类”。

  3. 将类命名为 “启动 ”,然后选择“ 确定”。

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

现已完成服务器代码设置。 在下一部分中,你将设置客户端。

设置客户端代码

在本部分中,将设置在客户端上运行的代码。

创建 HTML 页和 JavaScript 文件

HTML 页面将显示数据,JavaScript 文件将组织数据。

创建StockTicker.html

首先,添加 HTML 客户端。

  1. “解决方案资源管理器”中,右键单击项目并选择“添加>HTML 页面”。

  2. 将文件命名 为 StockTicker ,然后选择“ 确定”。

  3. 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.1.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 创建一个包含五列的表、一个标题行和一个包含跨所有五列的单个单元格的数据行。 数据行显示“正在加载...”应用启动时的一刻时间。 JavaScript 代码将删除该行,并在其位置行中添加从服务器检索的库存数据。

    脚本标记指定:

    • jQuery 脚本文件。

    • SignalR 核心脚本文件。

    • SignalR 代理脚本文件。

    • 稍后将创建的 StockTicker 脚本文件。

    应用动态生成 SignalR 代理脚本文件。 它指定“/signalr/hubs”URL,并为 Hub 类上的方法定义代理方法,在本例中为 。StockTickerHub.GetAllStocks 如果需要,可以使用 SignalR 实用工具手动生成此 JavaScript 文件。 不要忘记在方法调用中 MapHubs 禁用动态文件创建。

  4. “解决方案资源管理器”中,展开“脚本”。

    jQuery 和 SignalR 的脚本库在项目中可见。

    重要

    包管理器将安装更高版本的 SignalR 脚本。

  5. 更新代码块中的脚本引用以对应于项目中脚本文件的版本。

  6. “解决方案资源管理器”中,右键单击“StockTicker.html”,然后选择“设为起始页”。

创建StockTicker.js

现在创建 JavaScript 文件。

  1. “解决方案资源管理器”中,右键单击项目并选择“添加>JavaScript 文件”。

  2. 将文件命名 为 StockTicker ,然后选择“ 确定”。

  3. 将此代码添加到 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 函数初始化 SignalR start 连接。 函数 start 异步执行并返回 jQuery Deferred 对象。 可以调用 done 函数来指定要在应用完成异步操作时调用的函数。

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

获取所有股票

函数 init 在服务器上调用 getAllStocks 函数,并使用服务器返回的信息更新库存表。 请注意,默认情况下,必须在客户端上使用 camelCasing,即使方法名称在服务器上采用 pascal 大小写。 camelCasing 规则仅适用于方法,不适用于对象。 例如,引用 stock.Symbolstock.Price,而不是 stock.symbolstock.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();
}

在 方法中init,应用为从服务器接收的每个股票对象的表行创建 HTML,方法是调用 formatStock 格式化对象的属性stock,然后调用 supplant 以将变量stock中的rowTemplate占位符替换为对象属性值。 然后将生成的 HTML 追加到库存表中。

注意

可以通过将它作为callback在异步start函数完成后执行的函数传入来调用init。 如果在调用 init 后作为单独的 JavaScript 语句调用 start,则函数将失败,因为它会立即运行,而不会等待 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);
    }

函数 updateStockPrice 采用与 函数中相同的方式 init 将从服务器接收的常用对象格式化为表行。 它不会将行追加到表中,而是在表中查找库存的当前行,并将该行替换为新行。

测试应用程序

可以测试应用以确保它正常工作。 你将看到所有浏览器窗口都显示股票价格波动的实时股票表。

  1. 在工具栏中,打开 “脚本调试 ”,然后选择“播放”按钮以在调试模式下运行应用。

    用户打开调试模式并选择“播放”的屏幕截图。

    浏览器窗口将打开,显示 实时库存表。 库存表最初显示“正在加载...”然后,在一小段时间后,应用会显示初始股票数据,然后股票价格开始变化。

  2. 从浏览器复制 URL,打开其他两个浏览器,然后将 URL 粘贴到地址栏中。

    初始库存显示与第一个浏览器相同,更改同时发生。

  3. 关闭所有浏览器,打开新浏览器,然后转到同一 URL。

    StockTicker 单一实例对象继续在服务器中运行。 实时库存表显示股票持续变化。 看不到包含零个更改数字的初始表。

  4. 关闭浏览器。

启用日志记录

SignalR 具有内置日志记录功能,可在客户端上启用该功能,以帮助进行故障排除。 在本部分中,将启用日志记录并查看显示日志如何告诉你 SignalR 使用的以下哪种传输方法的示例:

对于任何给定的连接,SignalR 会选择服务器和客户端都支持的最佳传输方法。

  1. 打开 StockTicker.js

  2. 添加此突出显示的代码行,以便在文件末尾初始化连接的代码之前立即启用日志记录:

    // Start the connection
    $.connection.hub.logging = true;
    $.connection.hub.start().done(init);
    
  3. 按 F5 运行项目。

  4. 打开浏览器的开发人员工具窗口,然后选择“控制台”以查看日志。 可能需要刷新页面才能查看 SignalR 协商新连接的传输方法的日志。

    • 如果在 Windows 8 (IIS 8) 上运行 Internet Explorer 10,则传输方法是 WebSocket

    • 如果在 Windows 7 (IIS 7.5) 上运行 Internet Explorer 10,则传输方法是 iframe

    • 如果在 Windows 8 (IIS 8) 上运行 Firefox 19,则传输方法是 WebSocket

      提示

      在 Firefox 中,安装 Firebug 加载项以获取控制台窗口。

    • 如果你在 Windows 7 (IIS 7.5) 上运行 Firefox 19,则传输方法是 服务器发送 的事件。

安装 StockTicker 示例

Microsoft.AspNet.SignalR.Sample 安装 StockTicker 应用程序。 NuGet 包包含的功能比从头开始创建的简化版本更多。 在本教程的此部分中,将安装 NuGet 包并查看新功能和实现这些功能的代码。

重要

如果在未执行本教程前面的步骤的情况下安装包,则必须将 OWIN 启动类添加到项目。 NuGet 包的此readme.txt文件说明了此步骤。

安装 SignalR.Sample NuGet 包

  1. 在“解决方案资源管理器”中,右键单击项目,并选择“管理 NuGet 包”。

  2. NuGet 包管理器:SignalR.StockTicker 中,选择“ 浏览”。

  3. “包源”中选择“ nuget.org”。

  4. 在搜索框中输入 SignalR.Sample ,然后选择 “Microsoft.AspNet.SignalR.Sample>安装”。

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

    安装 SignalR.Sample 包创建了文件夹及其内容。

  6. SignalR.Sample 文件夹中,右键单击 “StockTicker.html”,然后选择“ 设为起始页”。

    注意

    安装 SignalR.Sample NuGet 包可能会更改 Scripts 文件夹中的 jQuery 版本。 包在 SignalR.Sample 文件夹中安装的新StockTicker.html文件将与包安装的 jQuery 版本同步,但如果要再次运行原始StockTicker.html文件,可能需要先更新脚本标记中的 jQuery 引用。

运行此应用程序

在第一个应用中看到的表具有有用的功能。 完整的股票股票代码应用程序显示新功能:水平滚动窗口,显示股票数据和股票在涨跌时颜色发生变化。

  1. F5 运行该应用。

    首次运行应用时,“市场”处于“关闭状态”,并且你会看到一个静态表和一个不滚动的时钟窗口。

  2. 选择 “开放市场”。

    实时时钟的屏幕截图。

    • “实时股票交易”框开始水平滚动,服务器开始定期随机广播股票价格变化。

    • 每次股票价格更改时,应用都会更新动态股票表实时股票股票代码。

    • 当股票的价格变化为正值时,应用会显示具有绿色背景的股票。

    • 当更改为负值时,应用会显示红色背景的股票。

  3. 选择“ 关闭市场”。

    • 表更新停止。

    • 滚动条停止滚动。

  4. 选择“重置”

    • 重置所有库存数据。

    • 应用在价格更改开始前还原初始状态。

  5. 从浏览器中复制 URL,打开其他两个浏览器,然后将 URL 粘贴到地址栏中。

  6. 可以在每个浏览器中同时看到动态更新的相同数据。

  7. 选择任一控件时,所有浏览器都会同时以相同的方式响应。

实时股票交易盘显示

Live Stock Ticker 显示是按 CSS 样式格式化为单行的元素中的<div>无序列表。 应用以与表相同的方式初始化和更新代码符号:通过替换模板字符串中的<li>占位符并将元素<ul>动态添加到 <li> 元素。 该应用包括滚动,方法是使用 jQuery animate 函数改变 内 <div>无序列表的左边距。

SignalR.Sample StockTicker.html

股票代码 HTML 代码:

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

SignalR.Sample StockTicker.css

股票代码 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;
    }

SignalR.Sample SignalR.StockTicker.js

使其滚动的 jQuery 代码:

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

客户端可以调用的服务器上的其他方法

若要为应用增加灵活性,应用可以调用其他方法。

SignalR.Sample StockTickerHub.cs

StockTickerHub 定义了客户端可以调用的四个附加方法:

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

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

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

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

应用调用 OpenMarketCloseMarketReset 以响应页面顶部的按钮。 它们演示了一个客户端触发立即传播到所有客户端的状态更改的模式。 其中每个方法调用 类中 StockTicker 导致市场状态更改的方法,然后广播新状态。

SignalR.Sample StockTicker.cs

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 属性的 MarketState 字段指定 volatile

private volatile MarketState _marketState;

BroadcastMarketStateChangeBroadcastMarketReset 方法类似于你已经看到的 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中的新函数根据市场状态启用和禁用按钮。 它们还会停止或启动 实时股票交易盘 水平滚动。 由于许多函数正在添加到 ticker.client中,应用使用 jQuery 扩展函数 来添加它们。

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

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

客户端建立连接后,需要执行一些额外的工作:

  • 找出市场是打开还是关闭,以调用适当的 marketOpenedmarketClosed 函数。

  • 将服务器方法调用附加到按钮。

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

在应用建立连接之前,服务器方法不会连接到按钮。 因此,代码无法在服务器方法可用之前调用它们。

其他资源

在本教程中,你已了解如何对 SignalR 应用程序进行编程,以便将消息从服务器广播到所有连接的客户端。 现在,你可以定期广播消息,并响应来自任何客户端的通知。 可以使用多线程单一实例的概念来维护多玩家在线游戏方案中的服务器状态。 有关示例,请参阅 基于 SignalR 的拍摄游戏

有关演示对等通信方案的教程,请参阅使用 SignalR 入门和使用 SignalR 进行实时更新

有关 SignalR 的详细信息,请参阅以下资源:

后续步骤

在本教程中,你将了解:

  • 已创建项目
  • 设置服务器代码
  • 已检查服务器代码
  • 设置客户端代码
  • 已检查客户端代码
  • 已测试应用程序
  • 已启用日志记录

请转到下一篇文章,了解如何创建使用 ASP.NET SignalR 2 的实时 Web 应用程序。