SignalR学习笔记(二)高并发应用
虽然SignalR借助Websocket提供了很强大的实时通讯能力,但是在有些实时通讯非常频繁的场景之下,如果使用不当,还是会导致服务器,甚至客户端浏览器崩溃。
以下是一个实时拖拽方块项目的优化过程
项目的需求如下
- 在网页中显示一个红色的可拖拽方块
- 一个用户拖拽该方块,该方块在其他用户客户端浏览器中的位置也会相应改变

创建项目
使用VS创建一个空的Web项目


引入SignalR库及jQuery UI库
打开Package Manage Console面板

运行一下2个命令
Install-package Microsoft.AspNet.SignalR
Install-package jQuery.UI.Combined
安装完成之后,解决方案结构如下

添加Owin启动类,启用SignalR
和学习笔记(一)中的步骤一样,添加一个Owin Startup Class, 命名为Startup.cs, 并在Configuration启用SignalR
using Microsoft.Owin;
using Owin;
 
[assembly: OwinStartup(typeof(MoveShape.Startup))]
 
namespace MoveShape
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.MapSignalR();
        }
    }
}
 
添加Position类
这里我们需要一个新建一个类来传递方法的位置信息
using Newtonsoft.Json; 
namespace MoveShape
{
    public class Position
    {
        [JsonProperty("left")]
        public double Left { get; set; }
 
        [JsonProperty("top")]
        public double Top { get; set; }
 
        [JsonProperty("lastUpdatedBy")]
        public string LastUpdatedBy { get; set; }
    }
}
添加MoveShapeHub
我们需要创建一个Hub来传递当前方块的位置信息
using Microsoft.AspNet.SignalR;
 
namespace MoveShape
{
    public class MoveShapeHub : Hub
    {
        public void MovePosition(Position model)
        {
            model.LastUpdatedBy = Context.ConnectionId;
            Clients.AllExcept(Context.ConnectionId).updatePosition(model);
        }
    }
}
 
当前用户在移动方块,除了当前用户之外的其他用户都需要更新方块位置,所以这里使用了
Clients.AllExcept方法,将当前用户从排除列表里面去除掉。
在SignalR学习笔记(一)中有说道,当用户客户端与Hub连接成功之后,Hub会分配一个全局唯一的ConnectionId给当前用户客户端,所以Context中的ConnectionId即表示当前用户。
Clients对象提供的所有筛选客户端方法如下
- Client.All – 向所有和Hub连接成功的客户端发送消息
- Client.AllExcept – 向除了指定客户端外的用户客户端发送消息
- Client.Client – 向指定的一个客户端发送消息
- Client.Clients – 向指定的多个客户端发送消息
添加前台页面
前台页面是用jQuery UI的Draggable功能实现拖拽,在Drag事件里可以获取到当前方块的位置,所以在这里我们可以将位置发送到MoveShapeHub中
<!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.12.4.min.js"></script>
    <script src="Scripts/jquery-ui-1.12.1.min.js"></script>
    <script src="Scripts/jquery.signalR-2.2.0.js"></script>
    <script src="/signalr/hubs"></script>
    <script>
        $(function () {
 
            //创建Hub代理
            var moveShapeHub = $.connection.moveShapeHub,
            $shape = $("#shape"),
            shapeModel = {
                left: 0,
                top: 0
            };
 
            //客户端接受到位置变动消息,执行的方法
            moveShapeHub.client.updatePosition = function (model) {
                shapeModel = model;
                $shape.css({ left: model.left, top: model.top });
            };
 
            $.connection.hub.start().done(function () {
                $shape.draggable({
                    drag: function () {
                        shapeModel = $shape.offset();
 
                        //当发生拖拽的之后,把方块当前位置发送到Hub
                        moveShapeHub.server.movePosition(shapeModel);
                    }
                });
            });
        });
    </script>
 
    <div id="shape" />
</body>
</html>
 
当前效果
分别在2个浏览器中启动MoveShape.html, 模拟2个用户同时访问的情况
 
效率问题
下面我们在Drag事件里面添加日志代码
Console.log($shape.offset())
然后刷新页面,打开Chrome的开发者工具的console面板,然后移动方块,你会发现每做一次微小的移动,代码都会执行一次。
 
也就是说移动一个微小的距离,SignalR的Hub中的MovePosition方法都会执行一边,所有观看这个页面的用户都会执行一次UpdatePosition方法来同步位置,在用户比较少的情况下可能问题还不大,但是一旦用户数量增多,这个就是一个极大的性能黑洞。
如何改善效率
对于如何改善效率,我们可以分别从客户端和服务器端入手
客户端
在客户端,我们可以添加一个定时器,每隔一个时间间隔,向服务器更新一次方块的位置,这样更新位置的请求数量就大幅减少了
<!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.12.4.min.js"></script>
    <script src="Scripts/jquery-ui-1.12.1.min.js"></script>
    <script src="Scripts/jquery.signalR-2.2.2.js"></script>
    <script src="/signalr/hubs"></script>
    <script>
        $(function () {
 
            //创建Hub代理
            var moveShapeHub = $.connection.moveShapeHub,
            $shape = $("#shape"),
 
            //每200毫秒,向服务器同步一次位置
            interval = 200,
 
            //方块是否在移动
            moved = false,
            shapeModel = {
                left: 0,
                top: 0
            };
 
            //客户端接受到位置变动消息,执行的方法
            moveShapeHub.client.updatePosition = 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;
                    }
                });
 
                //添加定时器, 每个200毫秒, 向服务器同步一次位置
                setInterval(updateServerModel, interval);
            });
 
            function updateServerModel() {
                if (moved) {
                    console.log($shape.offset());
 
                    moveShapeHub.server.movePosition(shapeModel);
 
                    //同步完毕之后, 设置moved标志为false
                    moved = false;
                }
            }
        });
    </script>
 
    <div id="shape" />
</body>
</html>
 
服务器端
服务器端,可以采取和客户端差不多的思路,加入一个定时器,减少同步方块位置的次数。
using Microsoft.AspNet.SignalR;
using System;
using System.Threading;
 
namespace MoveShape
{
    public class Broadcaster
    {
        private readonly static Lazy<Broadcaster> _instance =
            new Lazy<Broadcaster>(() => new Broadcaster());
       
        //每隔40毫秒,执行一次同步操作
        private readonly TimeSpan BroadcastInterval =
            TimeSpan.FromMilliseconds(40);
        private readonly IHubContext _hubContext;
        private Timer _broadcastLoop;
        private Position _model;
        private bool _modelUpdated;
        public Broadcaster()
        {
            _hubContext = GlobalHost.ConnectionManager.GetHubContext<MoveShapeHub>();
            _model = new Position();
            _modelUpdated = false;
 
            //添加定时器,每隔一个时间间隔,执行一次同步位置方法
            _broadcastLoop = new Timer(
                BroadcastShape,
                null,
                BroadcastInterval,
                BroadcastInterval);
        }
        public void BroadcastShape(object state)
        {
            if (_modelUpdated)
            {
                _hubContext.Clients.AllExcept(_model.LastUpdatedBy).updatePosition(_model);
                _modelUpdated = false;
            }
        }
        public void UpdatePosition(Position position)
        {
            _model = position;
            _modelUpdated = true;
        }
        public static Broadcaster Instance
        {
            get
            {
                return _instance.Value;
            }
        }
    }
 
    public class MoveShapeHub : Hub
    {
        private Broadcaster _broadcaster;
        public MoveShapeHub()
            : this(Broadcaster.Instance)
        {
        }
        public MoveShapeHub(Broadcaster broadcaster)
        {
            _broadcaster = broadcaster;
        }
        public void UpdateModel(Position position)
        {
            position.LastUpdatedBy = Context.ConnectionId;
            // Update the shape model within our broadcaster
            _broadcaster.UpdatePosition(position);
        }
    }
}
 
位置更新不连续
由于加入定时器,导致方块位置更新不连续,界面上看起来方块的移动是断断续续的。
这里的解决方案是,在客户端可以使用jQuery的animate方法,填补方块移动不连续的部分
moveShapeHub.client.updatePosition = function (model) {
    shapeModel = model;
    $shape.animate(shapeModel, { duration: 200, queue: false });
};

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号