Asp.NET MVC 使用 SignalR 实现推送功能一(Hubs 在线聊天室)

简介

      ASP .NET SignalR 是一个ASP .NET 下的类库,可以在ASP .NET 的Web项目中实现实时通信。什么是实时通信的Web呢?就是让客户端(Web页面)和服务器端可以互相通知消息及调用方法,当然这是实时操作的。
WebSockets是HTML5提供的新的API,可以在Web网页与服务器端间建立Socket连接,当WebSockets可用时(即浏览器支持Html5)SignalR使用WebSockets,当不支持时SignalR将使用其它技术来保证达到相同效果。
SignalR当然也提供了非常简单易用的高阶API,使服务器端可以单个或批量调用客户端上的JavaScript函数,并且非常 方便地进行连接管理,例如客户端连接到服务器端,或断开连接,客户端分组,以及客户端授权,使用SignalR都非常 容易实现。
SignalR 将与客户端进行实时通信带给了ASP .NET 。当然这样既好用,而且也有足够的扩展性。以前用户需要刷新页面或使用Ajax轮询才能实现的实时显示数据,现在只要使用SignalR,就可以简单实现了。
最重要的是您无需重新建立项目,使用现有ASP .NET项目即可无缝使用SignalR。
 
  以上是来自百度百科的解释,个人觉得通俗来讲就是WebSockets是一种握手协议,当用户于服务器建立连接(握手成功)时,双方就建立了一个连接通道,互相传递时时数据。在这之前,我们一般来说实现这个功能的方式就是Ajax轮询,通过Ajax循环获取数据,这当然是非常消耗资源的,并且给服务器带来一定的压力。而WebSockets虽然可达到全双工通信,但依然需要发出请求,不过这种请求的Header是很小的-大概只有 2 Bytes。这里我们重点要知道的一点就是,WebSockets这个通道是双工通道,简单理解就是客户端(javascript、jquery)的方法不但可以调取服务器的功能程序方法,并且服务器的功能程序也可以调取全部(广播)或某个(单播)或某一类(组播)客户端(javascript、jquery)的方法。而SignalR则是微软给我们集成的一个WebSockets API,原理跟WebSockets是一致的,只是当WebSockets可用时(即浏览器支持Html5)SignalR使用WebSockets,当不支持时SignalR将使用其它技术来保证达到相同效果。

使用

很多新的技术没有必要非得理解,只需要知道你的应用环境可以用这项技术很简便的去实现,用的多了,用的久了,自然而然就会慢慢的理解这项技术的原理了。

今天我们一步一步来介绍一下如何使用SignalR,这一篇文章介绍的是使用Hubs SignalR集线器去实现,下一篇我们将介绍使用SignalR永久连接类去实现,我们做个简单的聊天室。

先给大家贴一下Demo的效果图:

一、准备:

SignalR 运行在 .NET 4.5 平台上,所以需要安装 .NET 4.5。为了方便演示,本示例使用 ASP.NET MVC 在 Win 7 系统来实现。这需要安装 ASP.NET MVC 3 或 ASP.NET MVC 4

二:Hubs代码示例:

1、首先我们创建一个MVC项目工程名字叫做SignalR_Chat

2、然后打开 工具 - NuGet程序包管理器 - 程序包管理器控制台

3、安装SignalR

输出NuGet命令:Install-Package Microsoft.AspNet.SignalR

安装成功后我们发现我们的bin里已经添加了我们需要的组件,并且在Scripts文件夹下添加了SignalR的Jquery引用

4、我们新建一个文件夹叫Hubs,然后添加SignalR集线器类ChatHub.cs

在上面的代码中:

(1)HubName 这个特性是为了让客户端知道如何建立与服务器端对应服务的代理对象,如果没有设定该属性,则以服务器端的服务类名字作为 HubName 的缺省值;

(2)Chat 继承自 Hub,从下面 Hub 的接口图可以看出:Hub 支持向发起请求者(Caller),所有客户端(Clients),特定组(Group) 推送消息。

5、添加OWIN Startup Class

修改 Configuration方法

using Microsoft.Owin;
using Owin;
using SignalR_Chat.Connections;

[assembly: OwinStartupAttribute(typeof(SignalR_Chat.Startup))]
namespace SignalR_Chat
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.MapSignalR();
       //这个是下一篇永久连接类的 我们先不用
            //app.MapSignalR<MyConnection>("/echo");
        }
    }
}

 

6、我们实现一个聊天室,代码我是分步贴出来的,后面我会附上完整的代码和Demo。

首先,我们新建一个类 标记用户和在线状态

    public class OnlineUserInfo
        {
            //用户ID
            public string UserId { get; set; }
            //用户连接ID
            public string ConnectionId { get; set; }
            //用户昵称
            public string UserNickName { get; set; }
            //用户头像
            public string UserFaceImg { get; set; }
            //用户状态
            public string UserStates { get; set; }
        }

然后,我们在ChatHub中实例化一下这个类

       /// <summary>
        /// 这个是通过Hub集线器 大家可以参考 Connections下的 MyConnection 永久连接
        /// </summary>
        [HubName("chat")]
        public class ChatHub : Hub
        {
            static List<OnlineUserInfo> UserList = new List<OnlineUserInfo>();
        }

我们写一个用户连接时注册一个群组,用户后面的组播

            /// <summary>
            /// 注册群组 注册用户信息
            /// </summary>
            /// <param name="groupid">群组ID</param>
            /// <param name="usernickname">用户昵称</param>
            /// <param name="userfaceimg">用户头像</param>
            /// <param name="userid">用户在网站中的唯一标识ID</param>
            public void Group(string groupid, string usernickname, string userfaceimg, string userid)
            {
                //添加用户到群组 Groups.Add(用户连接ID,群组)
                Groups.Add(Context.ConnectionId, groupid);

                //如果说是一个简单的聊天室 下面这段代码是没有什么作用的 因为Context.ConnectionId是唯一的用户于服务器之间的连接
                //这里我传递进来了 用户的昵称和头像 还有网站中用户的ID 所以我要把用户的信息添加到我们上面建立的那个列表类中

                //如果用户不存在在线列表中
                if (UserList.Where(p => p.UserId == userid).FirstOrDefault() == null)
                {
                    //我们在列表中 添加这个用户 并且标记用户在线 UserStates = "True"
                    UserList.Add(new OnlineUserInfo() { UserId = userid, ConnectionId = Context.ConnectionId, UserNickName = usernickname, UserFaceImg = userfaceimg, UserStates = "True" });
                }
                    //如果用户已经存在于在线列表中
                else
                {
                    //我们更新用户列表中用户的信息 (这里更新的信息主要是用户的连接ID  ConnectionId = Context.ConnectionId)
                    var UserInfo = UserList.Where(p => p.UserId == userid).FirstOrDefault();
                    UserList.Remove(UserInfo);
                    UserList.Add(new OnlineUserInfo() { UserId = userid, ConnectionId = Context.ConnectionId, UserNickName = usernickname, UserFaceImg = userfaceimg, UserStates = "True" });
                }
                //这个方法是调用客户端LoginUser方法 并且传递当前用户列表 客户端会刷新当前用户列表 调用的是全部的已连接的用户 Clients.All
                Clients.All.LoginUser(Common.JsonConverter.Serialize(UserList));
                //这个方法是调用客户端的 addNewMessageToPage方法 目的是实现 当一个用户上线是 提醒所有的用户 某个用户上线了 提醒的是所有的已连接用户 所以也是Clients.All
                Clients.All.addNewMessageToPage("<dl  class=\"messageTip clearfix\"><dt></dt><dd>系统消息:" + DateTime.Now.ToString("HH:mm:ss") + "&nbsp;" + usernickname + "&nbsp;上线了<dd></dl>");
            }

为了方便大家理解,我这里就先把客户端的LoginUser和addNewMessageToPage方法贴出来,让大家好理解服务器是怎样调用客户端的js方法的

    //接收服务器信息
    chat.client.addNewMessageToPage = function (message) {
        //#chatContent就是一个div层 我们把服务器返回的信息追加到这个层上 跟QQ聊天相反,新的信息我们追加顶部
        $('#chatContent').prepend(message);
    };
    //服务器端调用的LoginUser方法,根据返回的用户列表 输出用户列表到页面上
    chat.client.LoginUser = function (UserList) {
        //在下一篇介绍的持久连接类中 是可以直接返回Json的 这里不知道怎么回事 接收的Json总是被接收成字符串 所以这里我们解析一下
        var data = eval("(" + UserList + ")");
        var html = "";
        for(var i=0;i<data.length;i++)
        {
            //这里我们做了一个判断 就是 解析用户列表Json时 如果用户的ID 就是当前用户的ID 那么就不添加 这跟QQ不一样啊 QQ中好友列表中是有自己的
            if (data[i].UserId != $("#Juser-userid").val()) {
                //如果用户的在线状态是在线呢 我们就添加onclick方法 实现 点击用户的用户 可以私聊 如果不在线 就不添加了 因为我们这个是没有存数据库的 所以没有做离线消息
                if (data[i].UserStates == "True") {
                    html += "<dl onclick=\"javascript:sendPerMessage('" + data[i].ConnectionId + "','" + data[i].UserNickName + "')\" class=\"clearfix tab-item-1\"><dt><img src=\"" + data[i].UserFaceImg + "\"></dt><dd>" + data[i].UserNickName + "</dd></dl>";
                }
                else
                {
                    html += "<dl onclick=\"javascript:void(0)\" class=\"clearfix tab-item-1 liveout\"><dt><img src=\"" + data[i].UserFaceImg + "\"></dt><dd>" + data[i].UserNickName + "</dd></dl>";
                }
            }
        }
        //更新页面用户列表
        $("#OnlineUsers").html(html);
    };

这里注意的是服务器调用的客户端方法跟客户端写的JS方法大小写是一样的,后面我们介绍客户端调用服务器方法的时候会将 客户端调用服务器方法的时候 服务器方法的首字母是小写的,这里提醒一下。

下面是我们的发送消息的方法,当我发送消息的时候 传递我的头像和昵称给服务器 让别人显示消息的时候能显示出谁发送的(这个跟QQ消息类似啊 方便大家理解)

一个是群组消息当然也可以是全部消息,另一个是私聊,就是指定发送个某一个用户

/// <summary>
            /// 发送消息 自定义判断是发送给全部用户还是某一个组(类似于群聊啦)
            /// </summary>
            /// <param name="groupid">接收的组</param>
            /// <param name="userfaceimg">发送用户的头像</param>
            /// <param name="usernickname">发送用户的昵称</param>
            /// <param name="message">发送的消息</param>
            public void Send(string groupid, string userfaceimg, string usernickname, string message)
            {
                if (groupid == "All")//全部用户(广播)
                {
                    //调用所有客户端的addNewMessageToPage方法 推送一条消息
                    Clients.All.addNewMessageToPage("<dl class=\"clearfix\"><dt><img src=\"" + userfaceimg + "\" /></dt><dd><i></i><div class=\"J_Users\">" + usernickname + "</div><div class=\"J_Content\">" + message + "</div></dd></dl>");
                }
                else//指定组(组播)
                {
                    //调用指定客户端的addNewMessageToPage方法 推送一条消息(所有属于组groupid的已连接用户)
                    Clients.Group(groupid).addNewMessageToPage("<dl class=\"clearfix\"><dt><img src=\"" + userfaceimg + "\" /></dt><dd><i></i><div class=\"J_Users\">" + usernickname + "</div><div class=\"J_Content\">" + message + "</div></dd></dl>");
                }
            }

            /// <summary>
            /// 发送给指定用户(单播)
            /// </summary>
            /// <param name="clientId">接收用户的连接ID</param>
            /// <param name="userfaceimg">发送用户的头像</param>
            /// <param name="usernickname">发送用户的昵称</param>
            /// <param name="message">发送的消息</param>
            public void SendSingle(string clientId, string userfaceimg, string usernickname, string message)
            {
                //首先我们获取一下接收用户的信息
                var UserInfo = UserList.Where(p => p.ConnectionId == clientId).FirstOrDefault();
                //如果用户不存在或用户的在线状态为False 那么提醒一下 发送用户 对方不在线
                if (UserInfo == null || UserInfo.UserStates == "False")
                {
                    Clients.Client(Context.ConnectionId).addNewMessageToPage("<dl  class=\"messageTip clearfix\"><dt></dt><dd>系统消息:当前用户不在线<dd></dl>");
                }
                else
                {
                    //如果用户存在并且在线呢 就把消息推送给接收的用户 并且加上当前用户信息 以及添加一个onclick事件 让接收的用户 可以直接点击消息的用户 回复 私聊信息 (不然还要在用户列表中找到谁给我发的消息 点击回复 这不科学...)
                    Clients.Client(clientId).addNewMessageToPage("<dl class=\"clearfix\"><dt onclick=\"javascript:sendPerMessage('" + Context.ConnectionId + "','" + usernickname + "')\"><img src=\"" + userfaceimg + "\" /></dt><dd class=\"per\"><s></s><div onclick=\"javascript:sendPerMessage('" + Context.ConnectionId + "','" + usernickname + "')\" class=\"J_Users\">" + usernickname + "<span>私聊</span></div><div class=\"J_Content\">" + message + "</div></dd></dl>");
                    //这句是发送给发送用户的 总不能我发送个私聊 对方收到了信息 我这里什么都不显示是吧 我也显示我发送的私聊信息 因为发送发就是我自己 所以不加onclick事件了 不允许自己跟自己聊天哦
                    Clients.Client(Context.ConnectionId).addNewMessageToPage("<dl class=\"clearfix\"><dt><img src=\"" + userfaceimg + "\" /></dt><dd class=\"per\"><s></s><div class=\"J_Users\">" + usernickname + "<span>私聊</span></div><div class=\"J_Content\">" + message + "</div></dd></dl>");
                }
            }

这里我贴一下前台代码

    $.connection.hub.start().done(function () {
        //用户连接时 注册一下群组和个人信息哦 这个的服务器代码 我们上面贴出来了
        //这个Demo是为了让大家理解SigalR所以没有做多复杂的流程 个人信息 我是直接传递的 
        //$("#groupid").val()这个是要注册的群组,可以自己定义 组播的时候 只要是在这一个组里的都会收到 
        //$("#Juser-login").val()这个是发送方也就是我的昵称
        //$("#Juser-faceimg").val()这个是我的头像
        //$("#Juser-userid").val()这个是我在网站中的唯一标识ID,用户连接的ID(Context.ConnectionId)也是唯一的,那么为什么还要我在这个网站中的ID呢?
        //解释一下子:单页面的聊天室是没有多大必要的,但是比如我们这个功能是放到公用里的,就像网站的在线客服一样,你总不能每个页面都写一套吧 既然是引用的这一个页面 
        //那么用户打开其他页面的时候 这个Context.ConnectionId是会变的,那我怎么知道这又是谁呢 我们就用用户在网站中的唯一标识ID作为参照,当新的连接进来时 我们看下是不是ID一样 
        //一样的话我们就更新用户列表中这个唯一标识ID用户的Context.ConnectionId和在线状态 不一样的话就添加新用户
        chat.server.group($("#groupid").val(), $("#Juser-login").val(), $("#Juser-faceimg").val(), $("#Juser-userid").val());
        $('.sendBtn').click(function () {
            //这里做一下判断 如果没有输入消息就发送 那么提示一下
            if ($('#MessageBox').val().length <= 0)
            {
                $('#chatContent').prepend("<dl  class=\"messageTip clearfix\"><dt></dt><dd>系统消息:请输入信息<dd></dl>");
            }
            else
            {
                //sendToConnectId 是我们自定义的一个字段 如果你点击了某一个用户 那么就把他的ConnectionId赋给sendToConnectId 我们知道是私聊
                //当然,用户点击退出私聊的时候 这个字段会被赋为空 表示是群聊 这个大家在Demo中一看就明白了
                if (sendToConnectId != "" && sendToConnectId.length > 0) {
                    //调用服务器私聊方法 !!!注意啊!!!服务器的私聊方法是 public void SendSingle(string clientId, string userfaceimg, string usernickname, string message)
                    //这里是chat.server.sendSingle 首字母小写啊 客户端调用的服务器方法首字母要小写  服务器调用的客户端方法 大小写一致 
                    chat.server.sendSingle(sendToConnectId, $("#Juser-faceimg").val(), $("#Juser-login").val(), $('#MessageBox').val());
                    $('#MessageBox').val("").focus();
                }
                else {
                    //这里是群聊 我们演示的没有做群组聊天 所以这里传递的是"All"表示 全部,会发送给全部用户
                    //说明一下方便理解:比如我们有这么一个情景,这个聊天是一个讨论,对某一篇文章或产品的讨论,那么是不是应该只在这篇文章或这个产品页面的用户才能收发属于这篇文章或产品消息呢,在其他页面
                    //的用户不应该能收发这里的消息呀 那么我们上面的代码chat.server.group中传递的groupid就应该是某篇文章或产品的标识,把他们划分到一个组里比如chat.server.group("A123")一个自定义字符串加上文章或产品ID,
                    //或直接用文章或产品的IDchat.server.group("123") 这里就chat.server.send("123", ...);就实现了 只有在这个页面中的用户才能收到此消息 就跟QQ的群是一样的
                    chat.server.send("All", $("#Juser-faceimg").val(), $("#Juser-login").val(), $('#MessageBox').val());
                    $('#MessageBox').val("").focus();
                }
            }
        });
    });

使用者离线或重新连接 重写Hub的方法

 //使用者离线
            public override Task OnDisconnected(bool stopCalled)
            {
                var UserInfo = UserList.Where(p => p.ConnectionId == Context.ConnectionId).FirstOrDefault();
                var userid = UserInfo.UserId;
                var usernickname = UserInfo.UserNickName;
                var userfaceimg = UserInfo.UserFaceImg;
                UserList.Remove(UserInfo);
                UserList.Add(new OnlineUserInfo() { UserId = userid, ConnectionId = Context.ConnectionId, UserNickName = usernickname, UserFaceImg = userfaceimg, UserStates = "False" });

                Clients.All.LoginUser(Common.JsonConverter.Serialize(UserList));
                Clients.All.addNewMessageToPage("<dl  class=\"messageTip clearfix\"><dt></dt><dd>系统消息:" + DateTime.Now.ToString("HH:mm:ss") + "&nbsp;" + usernickname + "&nbsp;离线了<dd></dl>");

                return base.OnDisconnected(true);
            }

            //使用者重新连接
            public override Task OnReconnected()
            {
                var UserInfo = UserList.Where(p => p.ConnectionId == Context.ConnectionId).FirstOrDefault();
                if (UserInfo != null)
                {
                    var userid = UserInfo.UserId;
                    var usernickname = UserInfo.UserNickName;
                    var userfaceimg = UserInfo.UserFaceImg;
                    UserList.Remove(UserInfo);
                    UserList.Add(new OnlineUserInfo() { UserId = userid, ConnectionId = Context.ConnectionId, UserNickName = usernickname, UserFaceImg = userfaceimg, UserStates = "True" });
                    Clients.All.LoginUser(Common.JsonConverter.Serialize(UserList));
                }
                return base.OnReconnected();
            }

还有一些辅助的JS方法,在这里我就不一一贴出来了,我把demo地址留给大家 ,大家可以搭建起来研究一下。

这篇文章仅仅是个人的一些理解和实现,可能中间会出现一些不合理的地方或是错误,请大家指正,我们共同学习研究。

Demo是用VS 2013写的 

下载:百度网盘 

补充:Demo是我写这个博客之前写的 没有用到HubName 这个特性 所以Demo跑起来会有错误 大家删除这个特性就没有错误了 在Hubs文件夹下的ChatHub.cs

原创文章 转载请尊重劳动成果 http://yuangang.cnblogs.com

posted @ 2016-05-03 11:48  果冻布丁喜之郎  阅读(18040)  评论(99编辑  收藏  举报