雖然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 });
};