教程:使用 ASP.NET SignalR 1.x 实现服务器广播

作者 :Patrick FletcherTom Dykstra

警告

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

本教程演示如何创建使用 ASP.NET SignalR 提供服务器广播功能的 Web 应用程序。 服务器广播意味着发送到客户端的通信由服务器启动。 此方案需要与对等方案(例如聊天应用程序)不同的编程方法,其中发送到客户端的通信由一个或多个客户端发起。

本教程中创建的应用程序模拟股票代码,这是服务器广播功能的典型方案。

欢迎对教程发表评论。 如果你有与本教程不直接相关的问题,可以将其发布到 ASP.NET SignalR 论坛StackOverflow.com

概述

Microsoft.AspNet.SignalR.Sample NuGet 包在 Visual Studio 项目中安装示例模拟股票代码应用程序。 在本教程的第一部分中,你将从头开始创建该应用程序的简化版本。 在本教程的其余部分,你将安装 NuGet 包并查看它创建的其他功能和代码。

股票代码应用程序是一种实时应用程序的代表,你希望定期将通知从服务器“推送”或广播到所有连接的客户端。

将在本教程的第一部分中生成的应用程序显示一个包含库存数据的网格。

StockTicker 初始版本

服务器定期随机更新股票价格,并将更新推送到所有连接的客户端。 在浏览器中, “更改 ”和列中的数字和 % 符号会动态更改,以响应来自服务器的通知。 如果向同一 URL 打开其他浏览器,它们都会同时显示相同的数据和相同的数据更改。

本教程包含以下部分:

注意

如果不想完成生成应用程序的步骤,可以在新的 空 ASP.NET Web 应用程序 项目中安装 SignalR.Sample 包,并通读这些步骤以获取代码说明。 本教程的第一部分介绍 SignalR.Sample 代码的子集,第二部分介绍 SignalR.Sample 包中附加功能的主要功能。

先决条件

在开始之前,请确保已在计算机上安装 Visual Studio 2012 或 2010 SP1。 如果没有 Visual Studio,请参阅 ASP.NET 下载 获取免费的 Visual Studio 2012 Express for Web。

如果有 Visual Studio 2010,请确保已安装 NuGet

创建项目

  1. “文件 ”菜单中,单击“ 新建项目”。

  2. 在“新建项目”对话框中,展开“模板”下的“C#”,然后选择“Web”。

  3. 选择 “ASP.NET 空 Web 应用程序 ”模板,将项目命名为 SignalR.StockTicker,然后单击“ 确定”。

    “新建项目”对话框

添加 SignalR NuGet 包

添加 SignalR 和 JQuery NuGet 包

可以通过安装 NuGet 包将 SignalR 功能添加到项目。

  1. 单击“ 工具” |NuGet 包管理器 |包管理器控制台

  2. 在包管理器中输入以下命令。

    Install-Package Microsoft.AspNet.SignalR -Version 1.1.3
    

    SignalR 包将许多其他 NuGet 包作为依赖项安装。 安装完成后,你拥有在 ASP.NET 应用程序中使用 SignalR 所需的所有服务器和客户端组件。

设置服务器代码

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

创建 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 == 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);
                }
            }
        }
    }
    

    创建股票时将设置的两个属性是符号 (例如 MSFT for Microsoft) 和 Price。 其他属性取决于设置 Price 的方式和时间。 首次设置 Price 时,该值将传播到 DayOpen。 以后设置 Price 时,将根据 Price 和 DayOpen 之间的差值计算 Change 和 PercentChange 属性值。

创建 StockTicker 和 StockTickerHub 类

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

从 StockTicker 广播

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

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

  2. 如果 Visual Studio 2012 具有 ASP.NET 和 Web 工具 2012.2 更新,请单击“Visual C#”下的“Web”,然后选择“SignalR 中心类”项模板。 否则,请选择 “类 ”模板。

  3. 将新类命名为 StockTickerHub.cs,然后单击“ 添加”。

    添加 StockTickerHub.cs

  4. 将模板代码替换为以下代码:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    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 服务调用)来获取数据,则需将 指定 Task<IEnumerable<Stock>> 为返回值以启用异步处理。 有关详细信息,请参阅 ASP.NET SignalR 中心 API 指南 - 服务器 - 何时异步执行

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

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

  5. 在项目文件夹中创建新的类文件,将其命名为 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 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 > .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 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;
    }
    

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

    _updatingStockPrices标志标记为 可变 ,以确保对它的访问是线程安全的。

    private volatile bool _updatingStockPrices = false;
    

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

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

    由于价格更改源自 StockTicker 对象,因此需要在所有连接的客户端上调用 updateStockPrice 方法的对象。 在中心类中,你有一个用于调用客户端方法的 API,但 StockTicker 不派生自 Hub 类,也没有对任何 Hub 对象的引用。 因此,为了广播到连接的客户端,StockTicker 类必须获取 StockTickerHub 类的 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);
    }
    

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

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

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

注册 SignalR 路由

服务器需要知道要截获和定向到 SignalR 的 URL。 为此,需要向 Global.asax 文件添加一些代码。

  1. “解决方案资源管理器”中,右键单击该项目,然后单击“添加新项”。

  2. 选择 “全局应用程序类” 项模板,然后单击“ 添加”。

    添加 global.asax

  3. 将 SignalR 路由注册代码添加到 Application_Start 方法:

    protected void Application_Start(object sender, EventArgs e)
    {
        RouteTable.Routes.MapHubs();
    }
    

    默认情况下,所有 SignalR 流量的基 URL 为“/signalr”,“/signalr/hubs”用于检索动态生成的 JavaScript 文件,该文件定义应用程序中所有中心的代理。 MapHubs 方法包含重载,可用于在 HubConfiguration 类的实例中指定不同的基 URL 和某些 SignalR 选项。

  4. 在文件顶部添加 using 语句:

    using System.Web.Routing;
    
  5. 保存并关闭 Global.asax 文件,然后生成项目。

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

设置客户端代码

  1. 在项目文件夹中创建新的 HTML 文件,并将其 命名StockTicker.html

  2. 将模板代码替换为以下代码:

    <!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.8.2.min.js" ></script>
        <!--Reference the SignalR library. -->
        <script src="/Scripts/jquery.signalR-1.0.1.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 列的单个单元格的数据行。 数据行显示“正在加载...”和 仅在应用程序启动时暂时显示。 JavaScript 代码将删除该行,并在其位置行中添加从服务器检索的库存数据。

    脚本标记指定 jQuery 脚本文件、SignalR 核心脚本文件、SignalR 代理脚本文件和稍后将创建的 StockTicker 脚本文件。 SignalR 代理脚本文件(指定“/signalr/hubs”URL)是动态生成的,并为 Hub 类上的方法定义代理方法(在本例中为 StockTickerHub.GetAllStocks)。 如果需要,可以使用 SignalR 实用工具 手动生成此 JavaScript 文件,并在 MapHubs 方法调用中禁用动态文件创建。

  3. 重要

    确保 StockTicker.html 中的 JavaScript 文件引用正确。 也就是说,请确保脚本标记中的 jQuery 版本 (示例) 中的 1.8.2 版本与项目的 Scripts 文件夹中的 jQuery 版本相同,并确保脚本标记中的 SignalR 版本与项目的 Scripts 文件夹中的 SignalR 版本相同。 如有必要,更改脚本标记中的文件名。

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

  5. 在项目文件夹中创建新的 JavaScript 文件,并将其 命名为StockTicker.js

  6. 将模板代码替换为以下代码:

    // 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 类的代理的引用,并将其放入股票代码变量中。 代理名称是由 [HubName] 属性设置的名称:

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

    定义所有变量和函数后,文件中的最后一行代码通过调用 SignalR start 函数初始化 SignalR 连接。 start 函数异步执行并返回 jQuery Deferred 对象,这意味着可以调用 done 函数来指定要在异步操作完成时调用的函数。。

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

    init 函数调用服务器上的 getAllStocks 函数,并使用服务器返回的信息更新库存表。 请注意,默认情况下,必须在客户端上使用 camel 大小写,尽管方法名称在服务器上采用 pascal 大小写。 camel 大小写规则仅适用于方法,不适用于对象。 例如,你指的是股票。符号和库存。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 属性修饰 Hub 方法,就像使用 HubName 属性修饰 Hub 类本身一样。

    在 init 方法中,为从服务器接收的每个股票对象创建表行的 HTML,方法是调用 formatStock 来设置股票对象的属性的格式,然后调用在 StockTicker.js) 顶部定义的 supplant (,以将 rowTemplate 变量中的占位符替换为常用对象属性值。 然后将生成的 HTML 追加到库存表中。

    可以通过将 init 作为回调函数传入来调用 init,该回调函数在异步启动函数完成后执行。 如果在调用 start 后将 init 作为单独的 JavaScript 语句调用,则函数将失败,因为它会立即执行,而不会等待 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. 按 F5 以调试模式运行应用程序。

    库存表最初显示“正在加载...”行,然后在短暂延迟后显示初始股票数据,然后股票价格开始变化。

    加载

    初始库存表

    从服务器接收更改的库存表

  2. 从浏览器地址栏中复制 URL,并将其粘贴到一个或多个新的浏览器窗口 () 。

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

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

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

  4. 关闭浏览器。

启用日志记录

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

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

  1. 打开 StockTicker.js 并添加一行代码,以在文件末尾初始化连接的代码之前立即启用日志记录:

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

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

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

    IE 10 IIS 8 控制台

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

    IE 10 控制台、IIS 7.5

    在 Firefox 中,安装 Firebug 加载项以获取控制台窗口。 如果在 Windows 8 (IIS 8) 上运行 Firefox 19,则传输方法是 WebSocket。

    Firefox 19 IIS 8 Websocket

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

    Firefox 19 IIS 7.5 控制台

安装并查看完整的 StockTicker 示例

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

安装 SignalR.Sample NuGet 包

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

  2. 在“管理 NuGet 包”对话框中,单击“联机”,在“联机搜索”框中输入 SignalR.Sample,然后在 SignalR.Sample 包中单击“安装”。

    安装 SignalR.Sample 包

  3. Global.asax 文件中,注释掉 RouteTable.Routes.MapHubs () ;前面在 Application_Start 方法中添加的行。

    不再需要 Global.asax 中的代码,因为 SignalR.Sample 包在 App_Start/RegisterHubs.cs 文件中注册 SignalR 路由:

    [assembly: WebActivator.PreApplicationStartMethod(typeof(SignalR.StockTicker.RegisterHubs), "Start")]
    
    namespace SignalR.StockTicker
    {
        public static class RegisterHubs
        {
            public static void Start()
            {
                // Register the default hubs route: ~/signalr/hubs
                RouteTable.Routes.MapHubs();
            }
        }
    }
    

    程序集属性引用的 WebActivator 类包含在 WebActivatorEx NuGet 包中,该包作为 SignalR.Sample 包的依赖项安装。

  4. 解决方案资源管理器中,展开通过安装 SignalR.Sample 包创建的 SignalR.Sample 文件夹。

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

    注意

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

运行应用程序

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

    除了前面看到的网格之外,完整的股票股票代码应用程序还显示一个水平滚动窗口,该窗口显示相同的股票数据。 首次运行应用程序时,“市场”将“关闭”,并且会看到一个静态网格和一个不滚动的时钟窗口。

    StockTicker 屏幕启动

    单击“ 打开市场”时, “实时股票交易 ”框开始水平滚动,服务器开始定期随机广播股票价格变化。 每次股票价格更改时, “实时股票表” 网格和 “实时股票交易” 框都会更新。 当股票的价格变化为正值时,该股票将以绿色背景显示,当更改为负值时,该股票将以红色背景显示。

    StockTicker 应用,市场开放

    关闭市场 ”按钮停止更改并停止滚动,“ 重置” 按钮会将所有股票数据重置为价格更改开始前的初始状态。 如果打开更多浏览器窗口并转到同一 URL,则会在每个浏览器中同时看到动态更新的相同数据。 单击其中一个按钮时,所有浏览器都会同时以相同的方式响应。

实时股票交易盘显示

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

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

使其滚动的 jQuery 代码:

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 枚举值的 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中的新函数基于市场状态启用和禁用按钮,并停止或启动时钟窗口水平滚动。 由于正在向 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();
    }
});

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

客户端建立连接后,还有一些额外的工作要做:找出市场是打开还是关闭,以便调用相应的 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();
        });
    });

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

后续步骤

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

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

若要了解更多高级 SignalR 开发概念,请访问以下站点以获取 SignalR 源代码和资源: