使用 SignalR 1.x 实现高频率实时功能

作者 :Patrick Fletcher

警告

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

本教程演示如何创建使用 ASP.NET SignalR 提供高频消息传送功能的 Web 应用程序。 在这种情况下,高频率消息传送是指以固定速率发送的更新;对于此应用程序,每秒最多 10 条消息。

本教程中创建的应用程序显示用户可以拖动的形状。 然后,形状在所有其他连接的浏览器中的位置将更新,以使用定时更新来匹配拖动的形状的位置。

本教程中介绍的概念在实时游戏和其他模拟应用程序中都有应用。

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

概述

本教程演示如何创建一个与其他浏览器实时共享对象状态的应用程序。 我们将创建的应用程序称为 MoveShape。 MoveShape 页面将显示用户可以拖动的 HTML Div 元素;当用户拖动 Div 时,其新位置将发送到服务器,服务器随后会告知所有其他连接的客户端更新形状的位置以匹配。

显示 MoveShape 应用程序页的屏幕截图。

本教程中创建的应用程序基于 Damian Edwards 的演示。 可在此处查看包含此演示的视频。

本教程首先演示了如何从拖动形状时触发的每个事件发送 SignalR 消息。 然后,每个连接的客户端将在每次收到消息时更新形状的本地版本的位置。

虽然应用程序将使用此方法运行,但这不是建议的编程模型,因为发送的消息数没有上限,因此客户端和服务器可能会不堪重负的消息,并且性能会降低。 客户端上显示的动画也会脱节,因为每个方法都会立即移动形状,而不是顺利地移动到每个新位置。 本教程的后续部分将演示如何创建计时器函数来限制客户端或服务器发送消息的最大速率,以及如何在位置之间平稳移动形状。 可以从 代码库下载本教程中创建的应用程序的最终版本。

本教程包含以下部分:

先决条件

本教程需要 Visual Studio 2012 或 Visual Studio 2010。 如果使用 Visual Studio 2010,则项目将使用 .NET Framework 4 而不是 .NET Framework 4.5。

如果使用 Visual Studio 2012,建议安装 ASP.NET 和 Web 工具 2012.2 更新。 此更新包含新功能,例如发布增强功能、新功能和新模板。

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

创建项目

在本部分中,我们将在 Visual Studio 中创建项目。

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

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

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

    创建新项目

添加 SignalR 和 JQuery.UI NuGet 包

可以通过安装 NuGet 包将 SignalR 功能添加到项目。 本教程还将使用 JQuery.UI 包来允许拖动形状并创建动画。

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

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

    Install-Package Microsoft.AspNet.SignalR -Version 1.1.3
    

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

  3. 在包管理器控制台中输入以下命令,以安装 JQuery 和 JQuery.UI 包。

    Install-Package jQuery.ui.combined
    

创建基本应用程序

在本部分中,我们将创建一个浏览器应用程序,用于在每次鼠标移动事件期间将形状的位置发送到服务器。 然后,服务器在收到此信息时将此信息广播到所有其他连接的客户端。 我们将在后面的部分中对此应用程序进行扩展。

  1. “解决方案资源管理器”中,右键单击项目,然后选择“添加”、“类...”。将类命名为 MoveShapeHub,然后单击“添加”。

  2. 将新的 MoveShapeHub 类中的代码替换为以下代码。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.AspNet.SignalR;
    using Microsoft.AspNet.SignalR.Hubs;
    using Newtonsoft.Json;
    
    namespace MoveShapeDemo
    {
        public class MoveShapeHub : Hub
        {
            public void UpdateModel(ShapeModel clientModel)
            {
                clientModel.LastUpdatedBy = Context.ConnectionId;
                // Update the shape model within our broadcaster
                Clients.AllExcept(clientModel.LastUpdatedBy).updateShape(clientModel);
            }
        }
        public class ShapeModel
        {
            // We declare Left and Top as lowercase with 
            // JsonProperty to sync the client and server models
            [JsonProperty("left")]
            public double Left { get; set; }
    
            [JsonProperty("top")]
            public double Top { get; set; }
    
            // We don't want the client to get the "LastUpdatedBy" property
            [JsonIgnore]
            public string LastUpdatedBy { get; set; }
        }
    }
    

    上述 MoveShapeHub 类是 SignalR 中心的实现。 与使用 SignalR 的入门教程一样,中心具有客户端将直接调用的方法。 在这种情况下,客户端会将包含形状的新 X 和 Y 坐标的对象发送到服务器,然后服务器将广播到所有其他连接的客户端。 SignalR 将使用 JSON 自动序列化此对象。

    将发送到客户端 (ShapeModel) 的对象包含用于存储形状位置的成员。 服务器上的 对象版本还包含一个成员,用于跟踪要存储的客户端数据,以便不会向给定客户端发送其自己的数据。 此成员使用 JsonIgnore 属性来防止它被序列化并发送到客户端。

  3. 接下来,我们将在应用程序启动时设置中心。 在“解决方案资源管理器”中,右键单击项目,然后单击“添加 | ”全局应用程序类。 接受默认名称 “全局 ”,然后单击“ 确定”。

    添加全局应用程序类

  4. 在 Global.asax.cs 类中提供的 using 语句后面添加以下using语句。

    using System.Web.Routing;
    
  5. 在 Global 类的 方法中添加以下代码 Application_Start 行,以注册 SignalR 的默认路由。

    RouteTable.Routes.MapHubs();
    

    global.asax 文件应如下所示:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.Security;
    using System.Web.SessionState;
    
    using System.Web.Routing;
    
    namespace MoveShapeDemo
    {
        public class Global : System.Web.HttpApplication
        {
            protected void Application_Start(object sender, EventArgs e)
            {
                RouteTable.Routes.MapHubs();
            }
        }
    }
    
  6. 接下来,我们将添加客户端。 在“解决方案资源管理器”中,右键单击项目,然后单击“添加 | ”新建项。 在 “添加新项 ”对话框中,选择“ Html 页面”。 为页面指定适当的名称 (,例如 Default.html) ,然后单击“ 添加”。

  7. “解决方案资源管理器”中,右键单击刚刚创建的页面,然后单击“设为起始页”。

  8. 将 HTML 页中的默认代码替换为以下代码片段。

    注意

    验证以下脚本引用是否与在 Scripts 文件夹中添加到项目的包匹配。 在 Visual Studio 2010 中,添加到项目的 JQuery 和 SignalR 版本可能与下面的版本号不匹配。

    <!DOCTYPE html>
    <html>
    <head>
        <title>SignalR MoveShape Demo</title>
        <style>
            #shape {
                width: 100px;
                height: 100px;
                background-color: #FF0000;
            }
        </style>
    </head>
    <body>
    <script src="Scripts/jquery-1.6.4.js"></script>
    <script src="Scripts/jquery-ui-1.10.2.js"></script>
    <script src="Scripts/jquery.signalR-1.0.1.js"></script>
    <script src="/signalr/hubs"></script>
    <script>
     $(function () {
                var moveShapeHub = $.connection.moveShapeHub,
                $shape = $("#shape"),
    
                shapeModel = {
                    left: 0,
                    top: 0
                };
    
                moveShapeHub.client.updateShape = function (model) {
                    shapeModel = model;
                    $shape.css({ left: model.left, top: model.top });
                };
    
                $.connection.hub.start().done(function () {
                    $shape.draggable({
                        drag: function () {
                            shapeModel = $shape.offset();
                            moveShapeHub.server.updateModel(shapeModel);
                        }
                    });
                });
            });
    </script>
    
        <div id="shape" />
    </body>
    </html>
    

    上述 HTML 和 JavaScript 代码创建一个名为 Shape 的红色 Div,使用 jQuery 库启用形状的拖动行为,并使用形状的 drag 事件将形状的位置发送到服务器。

  9. 按 F5 启动应用程序。 复制页面的 URL,并将其粘贴到第二个浏览器窗口中。 在其中一个浏览器窗口中拖动形状;另一个浏览器窗口中的形状应移动。

    显示在一个浏览器窗口中拖动的形状如何在另一个窗口中移动的屏幕截图。

添加客户端循环

由于在每次鼠标移动事件上发送形状的位置会产生不必要的网络流量,因此需要限制来自客户端的消息。 我们将使用 javascript setInterval 函数设置一个循环,以固定速率将新位置信息发送到服务器。 此循环是“游戏循环”的非常基本的表示形式,这是一个反复调用的函数,用于驱动游戏或其他模拟的所有功能。

  1. 更新 HTML 页中的客户端代码以匹配以下代码片段。

    <!DOCTYPE html>
    <html>
    <head>
        <title>SignalR MoveShape Demo</title>
        <style>
            #shape {
                width: 100px;
                height: 100px;
                background-color: #FF0000;
            }
        </style>
    
    </head>
    <body>
    <script src="Scripts/jquery-1.6.4.js"></script>
    <script src="Scripts/jquery-ui-1.10.2.js"></script>
    <script src="Scripts/jquery.signalR-1.0.1.js"></script>
    <script src="/signalr/hubs"></script>
    <script>
            $(function () {
                var moveShapeHub = $.connection.moveShapeHub,
                    $shape = $("#shape"),
                    // Send a maximum of 10 messages per second 
                    // (mouse movements trigger a lot of messages)
                    messageFrequency = 10, 
                    // Determine how often to send messages in
                    // time to abide by the messageFrequency
                    updateRate = 1000 / messageFrequency, 
                    shapeModel = {
                        left: 0,
                        top: 0
                    },
                    moved = false;
    
                moveShapeHub.client.updateShape = function (model) {
                    shapeModel = model;
                    $shape.css({ left: model.left, top: model.top });
                };
    
                $.connection.hub.start().done(function () {
                    $shape.draggable({
                        drag: function () {
                            shapeModel = $shape.offset();
                            moved = true;
                        }
                    });
    
                    // Start the client side server update interval
                    setInterval(updateServerModel, updateRate);
                });
    
                function updateServerModel() {
                    // Only update server if we have a new movement
                    if (moved) {
                        moveShapeHub.server.updateModel(shapeModel);
                        moved = false;
                    }
                }
            });
    </script>
    
        <div id="shape" />
    </body>
    </html>
    

    上述更新添加了 updateServerModel 函数,该函数以固定频率调用。 每当标志指示要发送新的位置数据时, moved 此函数就会将位置数据发送到服务器。

  2. 按 F5 启动应用程序。 复制页面的 URL,并将其粘贴到第二个浏览器窗口中。 在其中一个浏览器窗口中拖动形状;其他浏览器窗口中的形状应移动。 由于发送到服务器的消息数将受到限制,因此动画不会像上一部分那样流畅。

    显示添加客户端循环时,在一个浏览器窗口中拖动的形状如何在另一个窗口中移动的屏幕截图。

添加服务器循环

在当前应用程序中,从服务器发送到客户端的消息与接收消息一样频繁。 这与客户端上出现的问题类似:消息的发送频率可能高于所需的频率,因此连接可能会泛滥。 本部分介绍如何更新服务器以实现限制传出消息速率的计时器。

  1. 将 的内容 MoveShapeHub.cs 替换为以下代码片段。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    
    using System.Threading;
    using Microsoft.AspNet.SignalR;
    
    using Newtonsoft.Json;
    
    namespace MoveShapeDemo
    {
        public class Broadcaster
        {
            private readonly static Lazy<Broadcaster> _instance = 
                new Lazy<Broadcaster>(() => new Broadcaster());
            // We're going to broadcast to all clients a maximum of 25 times per second
            private readonly TimeSpan BroadcastInterval = 
                TimeSpan.FromMilliseconds(40); 
            private readonly IHubContext _hubContext;
            private Timer _broadcastLoop;
            private ShapeModel _model;
            private bool _modelUpdated;
    
            public Broadcaster()
            {
                // Save our hub context so we can easily use it 
                // to send to its connected clients
                _hubContext = GlobalHost.ConnectionManager.GetHubContext<MoveShapeHub>();
    
                _model = new ShapeModel();
                _modelUpdated = false;
    
                // Start the broadcast loop
                _broadcastLoop = new Timer(
                    BroadcastShape, 
                    null, 
                    BroadcastInterval, 
                    BroadcastInterval);
            }
    
            public void BroadcastShape(object state)
            {
                // No need to send anything if our model hasn't changed
                if (_modelUpdated)
                {
                    // This is how we can access the Clients property 
                    // in a static hub method or outside of the hub entirely
                    _hubContext.Clients.AllExcept(_model.LastUpdatedBy).updateShape(_model);
                    _modelUpdated = false;
                }
            }
    
            public void UpdateShape(ShapeModel clientModel)
            {
                _model = clientModel;
                _modelUpdated = true;
            }
    
            public static Broadcaster Instance
            {
                get
                {
                    return _instance.Value;
                }
            }
        }
        
        public class MoveShapeHub : Hub
        {
            // Is set via the constructor on each creation
            private Broadcaster _broadcaster;
    
            public MoveShapeHub()
                : this(Broadcaster.Instance)
            {
            }
    
            public MoveShapeHub(Broadcaster broadcaster)
            {
                _broadcaster = broadcaster;
            }
    
            public void UpdateModel(ShapeModel clientModel)
            {
                clientModel.LastUpdatedBy = Context.ConnectionId;
                // Update the shape model within our broadcaster
                _broadcaster.UpdateShape(clientModel);
            }
        }
        public class ShapeModel
        {
            // We declare Left and Top as lowercase with 
            // JsonProperty to sync the client and server models
            [JsonProperty("left")]
            public double Left { get; set; }
    
            [JsonProperty("top")]
            public double Top { get; set; }
    
            // We don't want the client to get the "LastUpdatedBy" property
            [JsonIgnore]
            public string LastUpdatedBy { get; set; }
        }
        
    }
    

    上述代码扩展客户端以添加 Broadcaster 类,从而使用 Timer .NET Framework 中的 类限制传出消息。

    由于中心本身是临时的, (每次需要) 创建它时, Broadcaster 都会创建为单一实例。 .NET 4) 中引入的延迟初始化 (用于延迟创建,直到需要,以确保在启动计时器之前完全创建第一个中心实例。

    然后,将客户端函数的 UpdateShape 调用移出中心的 UpdateModel 方法,这样,每当收到传入消息时,就不再立即调用它。 相反,发送到客户端的消息将以每秒 25 次调用的速率发送,由 _broadcastLoop 计时器从 类中 Broadcaster 管理。

    最后,类需要使用 GlobalHost获取对当前正在运行的中心 (_hubContext) 的引用,Broadcaster而不是直接从中心调用客户端方法。

  2. 按 F5 启动应用程序。 复制页面的 URL,并将其粘贴到第二个浏览器窗口中。 在其中一个浏览器窗口中拖动形状;其他浏览器窗口中的形状应移动。 浏览器与上一部分没有明显差异,但发送到客户端的消息数将受到限制。

    显示添加服务器循环时,在一个浏览器窗口中拖动的形状在另一个窗口中移动的屏幕截图。

在客户端上添加平滑动画

该应用程序几乎已完成,但我们可以在客户端上移动形状以响应服务器消息时,在客户端上移动形状方面再进行一项改进。 我们将使用 JQuery UI 库的 animate 函数在形状的当前位置和新位置之间平滑移动形状,而不是将形状的位置设置为服务器提供的新位置。

  1. 更新客户端的 updateShape 方法,如下所示突出显示的代码:

    <!DOCTYPE html>
    <html>
    <head>
        <title>SignalR MoveShape Demo</title>
        <style>
            #shape {
                width: 100px;
                height: 100px;
                background-color: #FF0000;
            }
        </style>
    
    </head>
    <body>
    <script src="Scripts/jquery-1.6.4.js"></script>
    <script src="Scripts/jquery-ui-1.10.2.js"></script>
    <script src="Scripts/jquery.signalR-1.0.1.js"></script>
    <script src="/signalr/hubs"></script>
    <script>
            $(function () {
                var moveShapeHub = $.connection.moveShapeHub,
                    $shape = $("#shape"),
                    // Send a maximum of 10 messages per second 
                    // (mouse movements trigger a lot of messages)
                    messageFrequency = 10, 
                    // Determine how often to send messages in
                    // time to abide by the messageFrequency
                    updateRate = 1000 / messageFrequency, 
                    shapeModel = {
                        left: 0,
                        top: 0
                    },
                    moved = false;
    
                 moveShapeHub.client.updateShape = function (model) {
                     shapeModel = model;
                     // Gradually move the shape towards the new location (interpolate)
                     // The updateRate is used as the duration because by the time 
                     // we get to the next location we want to be at the "last" location
                     // We also clear the animation queue so that we start a new 
                     // animation and don't lag behind.
                     $shape.animate(shapeModel, { duration: updateRate, queue: false });
                };
    
                $.connection.hub.start().done(function () {
                    $shape.draggable({
                        drag: function () {
                            shapeModel = $shape.offset();
                            moved = true;
                        }
                    });
    
                    // Start the client side server update interval
                    setInterval(updateServerModel, updateRate);
                });
    
                function updateServerModel() {
                    // Only update server if we have a new movement
                    if (moved) {
                        moveShapeHub.server.updateModel(shapeModel);
                        moved = false;
                    }
                }
            });
    </script>
    
        <div id="shape" />
    </body>
    </html>
    

    在动画间隔 (期间,上述代码将形状从旧位置移动到服务器提供的新位置,在本例中为 100 毫秒) 。 在新动画开始之前,将清除形状上运行的任何先前动画。

  2. 按 F5 启动应用程序。 复制页面的 URL,并将其粘贴到第二个浏览器窗口中。 在其中一个浏览器窗口中拖动形状;其他浏览器窗口中的形状应移动。 形状在其他窗口中的移动应不那么模糊,因为它的移动是随着时间推移而内插的,而不是为每个传入消息设置一次。

    显示当您在客户端上添加平滑动画时,在一个浏览器窗口中拖动的形状如何在另一个窗口中移动的屏幕截图。

进一步步骤

在本教程中,你已了解如何对在客户端和服务器之间发送高频消息的 SignalR 应用程序进行编程。 此通信范例可用于开发在线游戏和其他模拟,例如 使用 SignalR 创建的拍摄游戏

可从 代码库下载本教程中创建的完整应用程序。

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