利用SignalR实现实时聊天

2018/10/10:博主第一次写原创博文而且还是关于C#的(博主是从前端转过来的),菜鸟一枚,如果有什么写的不对,理解错误,还望各位轻喷。,从SignalR开始!

       首先先介绍一下关于SignalR的一些基本概念,

       ASP.NET SignalR是为简化开发开发人员将实时web内容添加到应用程序过程而提供的类库。实时web功能指的是让服务器代码可以随时主动推送内容给客户端,而不是让服务器等待客户端的请求(才返回内容)。   所有"实时"种类的web功能都可以使用SignalR来添加到你的ASP.NET应用程序中。最常用的例子有聊天室,但我们能做的比这要多得多。考虑以下情况:用户需要不停的刷新网页来看最新的数据;或者在页面上通过实现长轮询来检索新数据(并显示),那你就可以考虑使用SignalR来实现了。比如:仪表板及监视型应用程序;协作型应用程序(如多人同时对文档进行编辑);作业进度更新及实时呈现表单等。   SignalR也适合新型的,需要从服务器上进行高频率更新的web应用程序,例如实时游戏。这里有一个好例子:ShoorR。   SignalR提供了一个简单的API用户创建服务器到客户端的远程过程调用(RPC),可以方便地从服务器端的.Net代码中对客户端浏览器及其他客户端平台中的的JS函数进行调用。SignalR还包括了用于管理连接(例如:连接和断开事件)及连接分组。

        SignalR可以自动对连接进行管理。并让你发送广播消息到所有已连接的客户端上,就像一个聊天室一样。当然除了群发外,你也可以发送到消息到特定的客户端。客户端和服务器的连接是持久的,不像传统的每次通信都需要重新建立连接的HTTP协议。   SignalR支持“服务器推送”功能,即服务器代码可以通过使用远程过程调用(RPC)来调用浏览器中的客户端代码,而不是当前在web上常用的请求-相应处理模型。   SignalR的应用可以使用服务总线,SQL SERVER或者Redis来扩展到数以千计的客户端上。   SignalR是开源的,可以通过GitHub访问。

    2、有人可能会问SignalR和WebSocket有什么区别

    ignalR使用WebSocket传输方式——在可能的情况下。并且会自动切换到旧的传输方式(如HTTP长连接)。你当然可以直接使用WebSocket来编写你的应用程序,但使用SignalR意味着你将有更多的额外功能而无需重新发明轮子。最重要的是,你可以将注意力关注在业务实现上,而无需考虑为旧的客户端单独创建兼容代码。SignalR还能够使你不必担心WebSocket更新,因为SignalR将会持续更新以支持变化的底层传输方式,跨不同版本的WebSocket来为应用程序提供一个一致的访问接口。
  当然,你可以创建只使用WebSocket传输的解决方案,SignalR提供了你可能需要自行编写代码的所有功能,比如回退到其他传输方式及针对更新的WebSocket实现来修改你的应用程序。博主的理解中就是SignalR是微软为了简化开发开发人员工作而造出的轮子,他不仅拥有WebSocket的功能,而且还有传统的HTTP长连接的方式。

     接下来就是安装SignalR,SignalR在nuget上可以下载安装(SignalR要求.net 4.5的框架),在vs的工具里能找到nuget管理:搜Microsoft.AspNet.SignalR安装就好了,安装后会自动生成一下文件夹。

服务端/接口Api端代码

  • 从Nuget上搜索SignalR并引入
  • 创建的脚本拷贝到客户端Lib中到时使用requirejs引用,并删除服务端生成的脚本
  • 在Startup.cs中注册SignalR中间件
   
1、注册视频聊天中间件:LiveVideoChat(app); 其中"/LiveVideoChat"前端脚本要调用的地址
2、中间件需要授权登录的情况在,需要配置【QueryStringOAuthBearerProvider】,以及在集线器(LiveVideoChat)标注【Authorize】
 
using System;
using System.Threading.Tasks;
using Ilikexx.Framework;
using Microsoft.AspNet.SignalR;
using Microsoft.Owin;
using Microsoft.Owin.Cors;
using Microsoft.Owin.Security.OAuth;
using Owin;
 
[assembly: OwinStartup(typeof(Brand.Api.Startup))]
 
namespace Brand.Api
{
    public partial class Startup
    {
        private readonly ILog logger = LogManager.GetLogger(typeof(Startup));
        /// <summary>
        /// 
        /// </summary>
        /// <param name="app"></param>
        public void Configuration(IAppBuilder app)
        {
            logger.Info("Startup开始运行");
            ConfigureAuth(app);
            LogManager.Flush();
 
            LiveVideoChat(app);
 
        }
        /// <summary>
        /// 注册视频聊天模块
        /// </summary>
        /// <param name="app"></param>
        private void LiveVideoChat(IAppBuilder app)
        {
            //LiveVideoChat 前端脚本要调用的地址
            app.Map("/LiveVideoChat", map =>
            
                map.UseCors(CorsOptions.AllowAll);
                map.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()
                {
                    Provider = new QueryStringOAuthBearerProvider()
                });
                var hubConfiguration = new HubConfiguration
                {
                    Resolver = GlobalHost.DependencyResolver,
                    EnableJavaScriptProxies = true
                };
                map.RunSignalR(hubConfiguration);
            });
        }
    }
}
/// <summary>
/// 验证token
/// </summary>
public class QueryStringOAuthBearerProvider : OAuthBearerAuthenticationProvider
{
    /// <summary>
    /// 从请求地址中获取token
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public override Task RequestToken(OAuthRequestTokenContext context)
    {
        var value = context.Request.Query.Get("access_token");
 
        if (!string.IsNullOrEmpty(value))
        {
            context.Token = value;
        }
 
        return Task.FromResult<object>(null);
    }
    /// <summary>
    /// 验证token的有效性
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public override Task ValidateIdentity(OAuthValidateIdentityContext context)
    {
        return base.ValidateIdentity(context);
    }
}
  • LiveVideoChat:聊天中间件(核心)

 

1、类标注Authorize标记是否需要token访问
2、类标注HubName给集线器起名,客户端在调用的时候会用到
3、方法标注HubMethodName供客户端脚本调用
4、熟悉分组广播,全部广播,单一广播
 
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Brand.Model;
using Brand.Service;
using Ilikexx.Framework;
using Ilikexx.Framework.Web.Mvc;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using Microsoft.AspNet.SignalR.Infrastructure;
using Newtonsoft.Json.Linq;
 
namespace Brand.Api.Controllers.SignalR
{
    /// <summary>
    /// 视频聊天模块
    /// </summary>
    [HubName("LiveVideoChatService")]
    [Authorize]
    public class LiveVideoChat : BaseHub
    {
 
        /// <summary>
        /// 在线用户类,按各个房间存储
        /// </summary>
        public static ConcurrentDictionary<string, List<Dictionary<string, User>>> OnLineUsers = new ConcurrentDictionary<string, List<Dictionary<string, User>>>();
 
        private UserService userService;
        /// <summary>
        /// 
        /// </summary>
        public LiveVideoChat()
        {
            userService = new UserService();
        }
         
        /// <summary>
        /// 获取调用者的用户信息
        /// </summary>
        public JObject CallerUserInfo
        {
            get
            {
                JObject data = new JObject();
                User user = null;
                List<Dictionary<string, User>> listUser;
                OnLineUsers.TryGetValue(RoomId, out listUser);
                if (listUser != null)
                {
                    Dictionary<string, User> mapUser = listUser.Find(x =>
                    {
                        return x.ContainsKey(Context.ConnectionId);
                    });
                    if (mapUser != null)
                    {
                        mapUser.TryGetValue(Context.ConnectionId, out user);
                        data.Add("Id", user.Id);
                        data.Add("NickName", user.NickName);
                        data.Add("HeadUrl", user.HeadUrl);
                    }
                }
 
                return data;
            }
        }
        /// <summary>
        /// 重写连接
        /// </summary>
        /// <returns></returns>
        public override Task OnConnected()
        {
 
            Connected();
            return base.OnConnected();
        }
        /// <summary>
        /// 重新链接
        /// </summary>
        /// <returns></returns>
        public override Task OnReconnected()
        {
            Connected();
            return base.OnReconnected();
        }
 
        /// <summary>
        /// 重写断开连接
        /// </summary>
        /// <param name="stopCalled"></param>
        /// <returns></returns>
        public override Task OnDisconnected(bool stopCalled)
        {
            User user = null;
            List<Dictionary<string, User>> listUser;
            OnLineUsers.TryGetValue(RoomId, out listUser);
            if (listUser != null)
            {
                Dictionary<string, User> mapUser = listUser.Find(x =>
                {
                    return x.ContainsKey(Context.ConnectionId);
                });
                if (mapUser != null)
                {
                    mapUser.TryGetValue(Context.ConnectionId, out user);
                    listUser.Remove(mapUser);
                    OnLineUsers.TryAdd(RoomId, listUser);
                }
            }
            //当前离线移除房间
            Groups.Remove(Context.ConnectionId, RoomId);
            SendToGroup(0);
            return base.OnDisconnected(stopCalled);
        }
 
        /// <summary>
        /// 连接上的处理
        /// </summary>
        private void Connected()
        {
            //房间号
            long Id = cvt.ToLong(RoomId);
 
            User user = userService.GetModel(UserId);
 
            List<Dictionary<string, User>> listUser;
            OnLineUsers.TryGetValue(RoomId, out listUser);
            if (listUser == null)
            {
                listUser = new List<Dictionary<string, User>>();
                Dictionary<string, User> mapUser = new Dictionary<string, User>();
                mapUser.Add(Context.ConnectionId, user);
                listUser.Add(mapUser);
 
                OnLineUsers.TryAdd(RoomId, listUser);
                Groups.Add(Context.ConnectionId, RoomId);
                SendToGroup(1);
            }
            else
            {
                Dictionary<string, User> mapUser = listUser.Find(x =>
                {
                    return x.ContainsKey(Context.ConnectionId);
                });
                //不等于null 说明是重新连接的(重新连接不等于要退出后才连接)
                if (mapUser == null)
                {
                    mapUser = new Dictionary<string, User>();
                    mapUser.Add(Context.ConnectionId, user);
                    listUser.Add(mapUser);
 
                    OnLineUsers.TryAdd(RoomId, listUser);
                    Groups.Add(Context.ConnectionId, RoomId);
                    SendToGroup(1);
                }
            }
 
 
        }
        /// <summary>
        /// 给房间内的所有用户发消息
        /// </summary>
        /// <param name="JoinOrExit">0=退出 1=加入</param>
        private void SendToGroup(int JoinOrExit)
        {
            int totalCount = 0;
            List<Dictionary<string, User>> listUser;
            OnLineUsers.TryGetValue(RoomId, out listUser);
            if (listUser != null)
            {
                totalCount = listUser.Count;
            }
            resultMessage.data = new
            {
                TotalCount = totalCount,
                User = CallerUserInfo
            };
            resultMessage.ret = 0;
            #region 特别注意不用 Clients.Group(RoomId) 可能是刚连接(连接后就可以使用)的时候,调用者加入到组会有延迟,所以分二条发送
            if (JoinOrExit == 1)
            {
                //给调用者(登录或退出者)发一条
                Clients.Caller.JoinRoom(resultMessage);
                //所在房间,要排除调用者
                Clients.Group(RoomId, Context.ConnectionId).JoinRoom(resultMessage);
            }
            else
            {
                //给调用者(登录或退出者)发一条
                Clients.Caller.ExitRoom(resultMessage);
                //所在房间,要排除调用者
                Clients.Group(RoomId, Context.ConnectionId).ExitRoom(resultMessage);
            }
            #endregion
        }
        #region 提供给JS事件调用的方法
        /// <summary>
        /// 发送【文本】消息给当前房间
        /// </summary>
        /// <param name="message"></param>
        [HubMethodName("SendMsgText")]
        public void SendMsgText(string message)
        {
            resultMessage.ret = 0;
            resultMessage.data = new
            {
                Content = message,
                User = CallerUserInfo
            };
            Clients.Group(RoomId, Context.ConnectionId).ReceiveMsgText(resultMessage);
        }
        /// <summary>
        /// 发送【图片】消息给当前房间
        /// </summary>
        /// <param name="message"></param>
        [HubMethodName("SendMsgImg")]
        public void SendMsgImg(string message)
        {
            resultMessage.ret = 0;
            resultMessage.data = new
            {
                Content = message,
                User = CallerUserInfo
            };
            Clients.Group(RoomId, Context.ConnectionId).ReceiveMsgImg(resultMessage);
        }
        #endregion
    }
}
 
  • BaseHub:继承 Hub,封装常用方法和属性

 

using Ilikexx.Framework;
using Ilikexx.Framework.Web.Mvc;
using Microsoft.AspNet.SignalR;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Text;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
 
namespace Brand.Api.Controllers
{
    /// <summary>
    /// 
    /// </summary>
    public partial class BaseHub : Hub
    {
        /// <summary>
        /// 
        /// </summary>
        public ResultMessage resultMessage;
        /// <summary>
        /// 
        /// </summary>
        public BaseHub()
        {
            resultMessage = new ResultMessage();
        }
 
        /// <summary>
        /// 获取房间号
        /// </summary>
        public string RoomId
        {
            get
            {
                return Context.QueryString["Id"];
            }
        }
        /// <summary>
        /// 转为JSON串
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        protected string toJsonString(object obj)
        {
            return JsonConvert.SerializeObject(obj, Newtonsoft.Json.Formatting.None);
        }
        /// <summary>
        /// 获取所请求的用户ID
        /// </summary>
        protected long UserId
        {
 
            get
            {
                var identity = System.Web.HttpContext.Current.User.Identity as ClaimsIdentity;
                //因为保存的是userid,当然也可以用其他方式
                return long.Parse(identity.Name);
            }
        }
    }
}
 

WEB端代码

客户端/移动端脚本

    • 使用Requirejs引入Signalr脚本
   
require(["jquery""ui""lib/jquery.signalR-2.2.2"], function ($, ui) {
});
  • 开始使用:

 

 
1、$.hubConnection创建集线器连接,地址LiveVideoChat要跟服务端的一致,若有参数可以在qs中添加(token,房间号等)
2、createHubProxy创建服务代理LiveVideoChatService也要与服务端集线器类起名(服务名)一致
3、接收服务器方法,其中JoinRoom为服务端委托调用名称,data为回调的数据(目前都统一跟接口返回格式一致为JSON串,{ret:0,data:{},msg:""})  
self.connHusService
                .on("JoinRoom", function (data) {
 
});
4、调用服务器方法,其中SendMsgText为服务端的方法, $msg.val()为发送的数据。要注意若有多个参数要与服务端一致
self.connHusService.invoke("SendMsgText", $msg.val())
                    .done(function () {
                        var txtResponse = $("#txtResponse")
                        txtResponse.append("OKOK");
                    })
                    .fail(function (e) {
                        alert(e);
                    });
 
 
/****************************************************************************************************
  注释:脚本模板示例
****************************************************************************************************/
require(["jquery""ui""lib/jquery.signalR-2.2.2"], function ($, ui) {
    var self = {
        $wrap: $("body"),
        tpl: '<div id="txtResponse"></div>
                <div style="position:absolute;bottom:50px;100%"><input type="text" name="msg" style="80%;border:1px solid"/><a  href="javasrcipt:void(0);" class="js_send" style="background:green;color:white;padding:10px">发送</a></div>',
        init: function () {
            /// <summary>
            /// 初始化
            /// </summary>
            var size = ui.utils.getViewPort();
            self.$wrap.append(ui.render(self.tpl, {
                play_ size.width,
                play_height: size.width / 4 * 3
            }));
            self.chatInit();
 
            //绑定事件
            self.bind();
        },
bind: function () {
            self.$wrap.on("click"".js_send", function () {
                var $msg = self.$wrap.find("[name=msg]");
                //客户端代理调用服务端方法
                self.connHusService.invoke("SendMsgText", $msg.val())
                    .done(function () {
                        var txtResponse = $("#txtResponse")
                        txtResponse.append("OKOK");
                    })
                    .fail(function (e) {
                        alert(e);
                    });
            });
 
        },
        chatInit: function () {
 
 
            //创建集线器连接
            self.connHus = $.hubConnection(Ilikexx.apiUrl + "LiveVideoChat");
            //添加请求参数
            self.connHus.qs = {
                access_token: Ilikexx.token,
                Id: 100
            }
            //开启日志记录
            //self.connHus.logging = true;
 
            // 获取代理
            self.connHusService = self.connHus.createHubProxy("LiveVideoChatService");
            // 设置state的值
            // self.connHusService.state.ClientType = "HubNonAutoProxy";
 
              
 
            // 客户端监听服务端发送的方法
            self.connHusService
                .on("JoinRoom", function (data) {
                    console.log(data)
                    var txtResponse = $("#txtResponse")
                    txtResponse.append(ui.render("<div>{{User.NickName}}进入了房间,总人数:{{TotalCount}}</div>", data.data));
                })
                .on("ExitRoom", function (data) {
                    console.log(data)
                    var txtResponse = $("#txtResponse")
                    txtResponse.append(ui.render("<div>{{User.NickName}}退出了房间,总人数:{{TotalCount}}</div>", data.data));
                })
                .on("ReceiveMsgText", function (data) {
                    console.log(data)
                    var txtResponse = $("#txtResponse")
                    txtResponse.append(ui.render('<div><img src="{{User.HeadUrl}}" />{{User.NickName}},说:{{Content}}</div>', data.data));
                })
                ;
 
            self.connHus.disconnected(function (e, conn) {
                console.log(conn)
                console.log('Wdisconnected。。。。。');
                //几秒进行重连
                // 开启连接
                self.connHus.start()
                    .done(function () {
                        console.log("Hus已连接服务器OK");
                    })
                    .fail(function () {
                        console.log("Hus已连接服务器失败");
                    });
            });
 
            // 开启连接
            self.connHus.start()
                .done(function () {
                    console.log("Hus已连接服务器OK");
                })
                .fail(function () {
                    console.log("Hus已连接服务器失败");
                });
        },
        render: function () {
            /// <summary>
            /// 渲染数据
            /// </summary>
 
        }
 
    };
    self.init();
});
 

--------------------- 作者:qq_964878912 来源:CSDN 原文:https://blog.csdn.net/qq_18798917/article/details/53897586 版权声明:本文为博主原创文章,转载请附上博文链接!

原文地址:https://www.cnblogs.com/lintaicheng/p/9849922.html