教程:使用 SignalR 2 创建高频实时应用

本教程演示如何创建使用 ASP.NET SignalR 2 提供高频消息传递功能的 Web 应用程序。 在这种情况下,“高频消息传送”意味着服务器以固定速率发送更新。 每秒最多发送 10 条消息。

创建的应用程序显示用户可以拖动的形状。 服务器使用计时更新更新在所有连接的浏览器中更新形状的位置,以匹配拖动的形状的位置。

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

在本教程中,你将了解:

  • 设置项目
  • 创建基本应用程序
  • 应用启动时映射到中心
  • 添加客户端
  • 运行应用
  • 添加客户端循环
  • 添加服务器循环
  • 添加平滑动画

警告

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

先决条件

设置项目

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

本部分介绍如何使用 Visual Studio 2017 创建空 ASP.NET Web 应用程序并添加 SignalR 和 jQuery.UI 库。

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

    创建 Web

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

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

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

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

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

  6. 选择“工具”>“NuGet 包管理器”>“包管理器控制台”。

  7. 包管理器控制台中,运行以下命令:

    Install-Package jQuery.UI.Combined
    

    命令安装 jQuery UI 库。 使用它对形状进行动画处理。

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

    脚本库参考

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

创建基本应用程序

在本部分中,将创建浏览器应用程序。 每次鼠标移动事件期间,应用都会将形状的位置发送到服务器。 服务器将此信息实时广播到所有其他连接的客户端。 有关此应用程序的详细信息,请参阅后面的部分。

  1. 打开 MoveShapeHub.cs 文件。

  2. MoveShapeHub.cs 文件中的代码替换为以下代码:

    using Microsoft.AspNet.SignalR;
    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; }
        }
    }
    
  3. 保存文件。

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

应用将 ShapeModel 对象发送到客户端。 它具有用于存储形状位置的成员。 服务器上的 对象版本还有一个成员,用于跟踪要存储的客户端数据。 此对象阻止服务器将客户端的数据发送回自身。 此成员使用 JsonIgnore 特性来阻止应用程序序列化数据并将其发送回客户端。

应用启动时映射到中心

接下来,在应用程序启动时设置到中心的映射。 在 SignalR 2 中,添加 OWIN 启动类将创建映射。

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

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

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

  4. Startup.cs 文件中的默认代码替换为以下代码:

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

当应用执行 Configuration 方法时,OWIN 启动类将调用MapSignalR。 应用使用 OwinStartup 程序集属性将 类添加到 OWIN 的启动进程。

添加客户端

为客户端添加 HTML 页。

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

  2. 将页面命名 为“默认值 ”,然后选择“ 确定”。

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

  4. Default.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.10.2.min.js"></script>
    <script src="Scripts/jquery-ui-1.10.4.min.js"></script>
    <script src="Scripts/jquery.signalR-2.1.0.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>
    
  5. “解决方案资源管理器”中,展开“脚本”。

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

    重要

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

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

此 HTML 和 JavaScript 代码创建名为 的shape红色div。 它使用 jQuery 库启用形状的拖动行为, drag 并使用 事件将形状的位置发送到服务器。

运行应用

你可以运行应用以使其正常工作。 在浏览器窗口周围拖动形状时,形状也会在其他浏览器中移动。

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

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

    此时会打开一个浏览器窗口,右上角有红色形状。

  2. 复制页面的 URL。

  3. 打开另一个浏览器,并将 URL 粘贴到地址栏中。

  4. 在其中一个浏览器窗口中拖动形状。 另一个浏览器窗口中的形状紧随其后。

虽然应用程序使用此方法运行,但它不是建议的编程模型。 发送的消息数没有上限。 因此,客户端和服务器因消息而不知所措,性能会降低。 此外,应用还会在客户端上显示不连续的动画。 之所以出现这种混蛋动画,是因为形状会按每个方法即时移动。 最好是形状平稳地移动到每个新位置。 接下来,你将了解如何解决这些问题。

添加客户端循环

在每个鼠标移动事件上发送形状的位置会产生不必要的网络流量。 应用需要限制来自客户端的消息。

使用 javascript setInterval 函数设置一个循环,该循环以固定速率将新位置信息发送到服务器。 此循环是“游戏循环”的基本表示形式。它是一个反复调用的函数,用于驱动游戏的所有功能。

  1. Default.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.10.2.min.js"></script>
    <script src="Scripts/jquery-ui-1.10.4.min.js"></script>
    <script src="Scripts/jquery.signalR-2.1.0.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. 选择“播放”按钮以启动应用程序

  3. 复制页面的 URL。

  4. 打开另一个浏览器,并将 URL 粘贴到地址栏中。

  5. 将形状拖动到其中一个浏览器窗口中。 另一个浏览器窗口中的形状如下所示。

由于应用会限制发送到服务器的消息数,因此动画最初不会像平滑一样显示。

添加服务器循环

在当前应用程序中,从服务器发送到客户端的消息发送的频率与接收消息的频率一样高。 此网络流量会出现与客户端上类似的问题。

应用可以发送比所需更频繁的消息。 连接可能会因此而泛滥。 本部分介绍如何更新服务器以添加限制传出消息速率的计时器。

  1. 用以下代码替换 MoveShapeHub.cs 的内容:

    using System;
    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; }
        }
        
    }
    
  2. 选择“播放”按钮以启动应用程序。

  3. 复制页面的 URL。

  4. 打开另一个浏览器,并将 URL 粘贴到地址栏中。

  5. 将形状拖动到其中一个浏览器窗口中。

此代码扩展客户端以添加 Broadcaster 类。 新类使用 Timer .NET Framework 中的 类限制传出消息。

最好了解中心本身是暂时的。 每次需要时都会创建它。 因此,应用将 Broadcaster 创建为单一实例。 它使用延迟初始化来延迟 的创建, Broadcaster直到需要它。 这可以保证应用在启动计时器之前完全创建第一个中心实例。

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

最后,类需要获取对当前操作_hubContext中心的引用,Broadcaster而不是直接从中心调用客户端方法。 它获取具有 的 GlobalHost引用。

添加平滑动画

应用程序已接近尾声,但我们可以再进行一项改进。 应用在客户端上移动形状以响应服务器消息。 使用 JQuery UI 库 animate 的 函数,而不是将形状的位置设置为服务器提供的新位置。 它可以在当前位置和新位置之间平稳地移动形状。

  1. 更新 Default.html 文件中客户端的 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.10.2.min.js"></script>
    <script src="Scripts/jquery-ui-1.10.4.min.js"></script>
    <script src="Scripts/jquery.signalR-2.1.0.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>
    
  2. 选择“播放”按钮以启动应用程序。

  3. 复制页面的 URL。

  4. 打开另一个浏览器,并将 URL 粘贴到地址栏中。

  5. 将形状拖动到其中一个浏览器窗口中。

形状在其他窗口中的移动看起来不那么抖动。 应用会随着时间推移内插其移动,而不是为每个传入消息设置一次。

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

获取代码

下载已完成项目

其他资源

刚刚了解的通信范例对于开发在线游戏和其他模拟非常有用,例如 使用 SignalR 创建的 ShootR 游戏

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

后续步骤

在本教程中,你将了解:

  • 设置项目
  • 已创建基本应用程序
  • 应用启动时映射到中心
  • 添加了客户端
  • 运行应用
  • 添加了客户端循环
  • 添加了服务器循环
  • 添加了平滑动画

请继续学习下一篇文章,了解如何创建使用 ASP.NET SignalR 2 提供服务器广播功能的 Web 应用程序。