使用SignalR实现即时通讯功能

教程简介

SignalR的好处是可以让多个客户端之间进行互动,比如这篇教程就展示了当你在页面上拖动矩形方块的同时,其它打开这个页面的用户也将会看到你拖动的轨迹以及最终的结果,当然他们也可以通过拖动该方块来改变你的网页上该方块的位置。如果对其进行扩展一下,说不定还能搞出来一个供多人完成的涂鸦墙。该教程出自High-Frequency Realtime with SignalR 2,有兴趣的可以看原教程。

运行结果

创建项目

1.打开Visual Studio,并创建一个新的ASP.NET Web Application项目,选择Empty模板,然后点击OK,创建项目。

2.依次单击Tools | NuGet Package Manager | Package Manager Console,打开NuGet控制台。

3.输入命令Install-Package Microsoft.AspNet.SignalR安装SignalR组件。

4.输入命令Install-Package jQuery.UI.Combined安装jQuery UI,并升级jQuery文件。

5.打开Solution Explorer可以看到对应的jQueryjQueryUISignalR,如下图所示。

引用的组件

添加Hub

1.右击项目文件,依次点击Add | New Item | Web | SignalR | SignalR Hub Class(v2),然后创建一个名为MoveShapeHub.cs的Hub文件。

2.在MoveShapeHub类中输入下面这段代码。

using Newtonsoft.Json;
using Microsoft.AspNet.SignalR;

namespace MoveShapeDemo
{
    public class MoveShapeHub : Hub
    {
        public void UpdateModel(ShapeModel clientModel)
        {
            clientModel.LastUpdateBy = Context.ConnectionId;

            // Clients.Others.updateShape(clientModel);
            Clients.AllExcept(clientModel.LastUpdateBy).updateShape(clientModel);
        }
    }

    public class ShapeModel
    {
        /// <summary>
        /// 使用JsonProperty指定的名称来序列化该属性
        /// </summary>
        [JsonProperty("left")]
        public double Left { get; set; }

        /// <summary>
        /// 同上
        /// </summary>
        [JsonProperty("top")]
        public double Top { get; set; }

        /// <summary>
        /// 使用JsonIgnore表示序列化时忽略该属性
        /// </summary>
        [JsonIgnore]
        public string LastUpdateBy { get; set; }
    }
}

添加Hub的启动文件

1.使用SignalR必需要添加启动文件(而这个启动文件则是基于OWIN的),在Solution Explorer里,右键项目,然后依次点击Add | New Item | Web | General | OWIN Startup Class,创建一个名为Startup.cs的启动文件。

2.在Startup类中输入下面这段代码。

using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(MoveShapeDemo.Startup))]

namespace MoveShapeDemo
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.MapSignalR();
        }
    }
}

添加客户端文件

1.服务端的工作完成后,再来编写客户端的代码,右键项目,并在根目录新建一个名为index.html的网页文件。

2.将下面这段代码拷备到新建的网页文件中。

<!DOCTYPE html>
<html>
<head>
    <title>SignalR MoveShape Demo</title>
    <style>
        #shape {
            width: 100px;
            height: 100px;
            background-color: #FF0000;
        }
    </style>
    <script src="Scripts/jquery-1.12.4.js"></script>
    <script src="Scripts/jquery-ui-1.12.1.js"></script>
    <script src="Scripts/jquery.signalR-2.2.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>
</head>
<body>
    <div id="shape" />
</body>
</html>

3.准备注绪后,按F5,并使用多个浏览器打开,试着在任何一个浏览器页面上拖动这个方块,就可以看到不管你在哪一个浏览器页面托动这个方块,所有打开的页面上这个方块都会跟着移动。不过这个测试案例并不是很好,因为只要你移动方块,它就会与服务器进行交互,这样就会造成网络带宽不必要的浪费,所以要改进一下。

改进一:客户端定时发送移动的新坐标

为了不使客户端移动的每一像素都发送给服务器,我们决定采用setInterval函数来让客户端定时把最新的坐标信息发送给服务器,并且加上一个开关,只有在移动后才会发送最新的坐标数据。

将静态页的代码改为下面这个样子(只给出脚本部分)

<script>
    var moveShapeHub,
        shapeModel,
        moved;

    $(function () {
        moveShapeHub = $.connection.moveShapeHub;
        moved = false;
        shapeModel = {
            left: 0,
            top: 0
        };

        var $shape = $("#shape"),
            messageFrequency = 10,                  // 限定每秒最多发送10个消息
            updateRate = 1000 / messageFrequency;   // 设置每个消息发送的间隔时间

        // 声明的回调方法
        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;
                }
            });

            // 使用定时器定时向服务器发送新坐标
            setInterval(updateServerModel, updateRate);
        });
    });

    function updateServerModel() {
        if (moved) {
            moveShapeHub.server.updateModel(shapeModel);
            moved = false;
        }
    }
</script>

重新运行程序,再次拖动矩形时,客户端就会以每秒最多10次的频率来与服务器传递数据,虽然节省了带宽,不过其它客户端的矩形在被移动时就会像PPT一样,没有那么顺滑了,不过这都不叫事儿。

改进二:服务器定时将数据广播给其它客户端

在这个例子中,服务器端只要一接收到数据就将它广播给其它的客户端,而这就会存在与上面客户端类似的问题,所以我们这次也要对服务器端的代码进行改造,让它定时向客户端广播数据。

MoveShapeHub.cs里的代码改为下面的代码。

using Microsoft.AspNet.SignalR;
using Newtonsoft.Json;
using System;
using System.Threading;

namespace MoveShapeDemo
{
    public class Broadcaster
    {
        /// <summary>
        /// 延时加载的功能
        /// </summary>
        private readonly static Lazy<Broadcaster> _instance = new Lazy<Broadcaster>(() => new Broadcaster());
        private readonly TimeSpan BroadcastInterval = TimeSpan.FromMilliseconds(40);
        private readonly IHubContext _hubContext;
        private Timer _broadcastLoop;
        private ShapeModel _model;
        private bool _modelUpdated;

        private Broadcaster()
        {
            // 获得对指定Hub连接的引用
            _hubContext = GlobalHost.ConnectionManager.GetHubContext<MoveShapeHub>();
            _model = new ShapeModel();
            _modelUpdated = false;
            _broadcastLoop = new Timer(BroadcastShape, null, BroadcastInterval, BroadcastInterval);
        }

        public static Broadcaster Instance
        {
            get { return _instance.Value; }
        }

        /// <summary>
        /// 将最新坐标广播给其它客户端(被定时器循环调用)
        /// </summary>
        /// <param name="state"></param>
        private void BroadcastShape(object state)
        {
            if (_modelUpdated)
            {
                _hubContext.Clients.AllExcept(_model.LastUpdateBy).updateShape(_model);
                _modelUpdated = false;
            }
        }

        public void UpdateShape(ShapeModel clientModel)
        {
            _model = clientModel;
            _modelUpdated = true;
        }
    }

    /// <summary>
    /// Hub的生命周期比较短(只有在需要的时候才会创建)
    /// </summary>
    public class MoveShapeHub : Hub
    {
        private Broadcaster _broadcaster;

        public MoveShapeHub() :
            this(Broadcaster.Instance)
        { }

        public MoveShapeHub(Broadcaster broadcaster)
        {
            _broadcaster = broadcaster;
        }

        public void UpdateModel(ShapeModel clientModel)
        {
            clientModel.LastUpdateBy = Context.ConnectionId;

            // 将原来的广播消息给其它客户端的代码交由Broadcaster来管理,
            // 这样就能够避免只要有数据传入就立即广播给其它客户端的情况
            // 出现。取而代之的是使用Timer类的定时广播功能,从而达到从
            // 服务器这端节省资源的目的。
            _broadcaster.UpdateShape(clientModel);
        }
    }

    public class ShapeModel
    {
        /// <summary>
        /// 使用JsonProperty指定的名称来序列化该属性
        /// </summary>
        [JsonProperty("left")]
        public double Left { get; set; }

        /// <summary>
        /// 同上
        /// </summary>
        [JsonProperty("top")]
        public double Top { get; set; }

        /// <summary>
        /// 使用JsonIgnore表示序列化时忽略该属性
        /// </summary>
        [JsonIgnore]
        public string LastUpdateBy { get; set; }
    }

重新运行项目,其实运行效果和之前并没有什么两样,只是服务器端不再会接到客户端发来的数据后就立即广播给其它客户端,而是会做记录,等到计器器下一次起动广播时再把这个坐标广播给其它客户端,不过被广播的用户依然会有看PPT的感觉。

改进三:为客户端添加动画效果

现在我们还要做最后一点改动,那就是解决被移动的矩形看起来像PPT一样的问题,只里要将客户端声明的回调方法(updateShape)做适当的改动即可,代码如下所示。

// 声明的回调方法
moveShapeHub.client.updateShape = function (model) {
	shapeModel = model;

    // 使用动画效果使得矩形被移动时看起来更平滑,
	// duration:动画的持续时间
	// queue:是否在效果队列中放置动画
	$shape.animate(shapeModel, { duration: updateRate, queue: false });
};
posted @ 2016-12-15 13:32  isrocking  阅读(4499)  评论(0编辑  收藏  举报