天天看點

SignalR學習筆記(二)高并發應用

雖然SignalR借助Websocket提供了很強大的實時通訊能力,但是在有些實時通訊非常頻繁的場景之下,如果使用不當,還是會導緻伺服器,甚至用戶端浏覽器崩潰。

以下是一個實時拖拽方塊項目的優化過程

項目的需求如下

  1. 在網頁中顯示一個紅色的可拖拽方塊
  2. 一個使用者拖拽該方塊,該方塊在其他使用者用戶端浏覽器中的位置也會相應改變
SignalR學習筆記(二)高并發應用

建立項目

使用VS建立一個空的Web項目

SignalR學習筆記(二)高并發應用
SignalR學習筆記(二)高并發應用

引入SignalR庫及jQuery UI庫

打開Package Manage Console面闆

SignalR學習筆記(二)高并發應用

運作一下2個指令

Install-package Microsoft.AspNet.SignalR

Install-package jQuery.UI.Combined

安裝完成之後,解決方案結構如下

SignalR學習筆記(二)高并發應用

添加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個使用者同時通路的情況

SignalR學習筆記(二)高并發應用

效率問題

下面我們在Drag事件裡面添加日志代碼

Console.log($shape.offset())

 然後重新整理頁面,打開Chrome的開發者工具的console面闆,然後移動方塊,你會發現每做一次微小的移動,代碼都會執行一次。

SignalR學習筆記(二)高并發應用

也就是說移動一個微小的距離,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 });

};