• 第二节:SignalR之PersistentConnection模型详解(步骤、用法、分组、跨域、第三方调用)


    一. 承上声明

      在上一个章节里,啰里啰嗦写了一堆关于介绍SignalR的“废话”,从这一篇开始往后正式撸代码,这期间不少人(包括 张善友大哥)建议我直接用.Net Core下的SignalR,关于此简单说一下,虽然我们要跟上时代步伐,但目前绝多数.Net项目都是基于 .Net FrameWork下的而非 .Net Core, 并且做事要有始有终,既然打算写这个系列,就不能半途而废,这个.Net FrameWork下的SignalR系列务必要写完。
      还有一点,不怕笑话,.Net Core虽然我也有研究,但并没有多么深入,暂时就不出来献丑了,后面等熟悉了,再来补充.Net Core下的SignalR的用法。
      这一节的主要内容: PersistentConnection模型 从零开始搭建的步骤、Web浏览器端和C#服务器端核心方法的使用介绍、分组的概念、开启跨域的两种方式。
      这一节的不足:没有体现SignalR的生命周期、没有断线重连的合理处理、没有心跳检测。

    几点介绍:   

      1. PersistentConnection(永久连接)相对于Hubs模式,更加偏向底层,它的编程模式与WebSocket的写法很类似,固定方法发送和接受,不能向Hub模式那样 客户端和服务端相互调用各自定义的方法。

      2. 该模型主要用于:单个发件人、分组、广播消息的简单终结点。

    二. 从零开始搭建

    1. 新建MVC5项目,通过Nuget安装:Microsoft.AspNet.SignalR程序集,安装成功后如下图:

     

    2. 新建一个永久连接模型类(MyPresitentConnection1),该类继承了PersistentConnection,并且override几个必要方法。

      

    3. 新建一个OWIN Startup Class(Startup),并在Configuration方法中指定使用的通讯模型的URl,  如: app.MapSignalR<MyPresitentConnection1>("/myPreConnection1");  

      PS: 程序启动时候首先会找到该类,然后运行里面的Configuration方法,从而url和通讯模型的匹配将生效。

          

    4. 在前端页面中书写SignalR的代码,与服务器端MyPresitentConnection1类进行连接,实现相应的通讯业务。

         

    三. 核心方法介绍

    1. 服务器端代码

    (1). OWIN Startup Class即Startup中要配置url和通讯模型向匹配,这里的url在web前端页面的js中要使用,代码如下:

    复制代码
    1   public class Startup
    2     {
    3         public void Configuration(IAppBuilder app)
    4         {
    5             // 有关如何配置应用程序的详细信息,请访问 https://go.microsoft.com/fwlink/?LinkID=316888
    6             //1. 基本用法的配置
    7             app.MapSignalR<MyPresitentConnection1>("/myPreConnection1");
    8         }
    9     }
    复制代码

    (2). 永久连接模型类MyPresitentConnection1继承了PersistentConnection,并且可以override几个方法。

    A. PersistentConnection中可以override的几个主要方法有:

      ①. OnConnected :连接成功后调用

      ②. OnReceived:接收到请求的时候调用

      ③. OnDisconnected:连接中断的时候调用

      ④. OnReconnected:连接超时重新连接的时候调用

    B. 核心业务主要使用PersistentConnection类中的Connection属性,有两个核心方法

      ①. 1对1发送消息: public static Task Send(string connectionId, object value);

      ②. 1对多发送消息: public static Task Send(IList<string> connectionIds, object value);

      ③. 广播(群发,可以去掉不发送的人): public static Task Broadcast(object value, params string[] excludeConnectionIds);

    PS:发现每个override里都有一个参数connectionId,它代表,每个客户端连接服务器成功后都会产生一个标记,这个标记是GUID产生的,它是唯一的, 不会重复, 在业务中可以通过该标记connectionId来区分客户端。

      下面我的代码中书写的业务为:

      ①. OnConnected方法即连接成功后调用的方法,调用Send方法告诉自己登录成功(当然你也可以根据实际业务告诉指定的人)。

      ②. OnReceived方法即接受请求的方法,调用Send方法向指定人一对一发送消息。

      ③. OnDisconnected方法即连接中断的方法,调用Broadcast方法向所有人发送消息,某某已经退出。

      ④. OnReconnected方法即超时重新连接方法,执行重连业务。

    分享代码:

     1  public class TempData
     2     {
     3         /// <summary>
     4         /// 接收人的connectionId
     5         /// </summary>
     6         public string receiveId { get; set; }
     7         
     8         /// <summary>
     9         /// 发送内容
    10         /// </summary>
    11         public string msg { get; set; }
    12     }
    View Code
     1  public class MyPresitentConnection1 : PersistentConnection
     2     {
     3         //下面的两个方法OnConnected 和 OnReceived默认带的
     4 
     5         /// <summary>
     6         /// 连接成功后的方法
     7         /// </summary>
     8         /// <param name="request"></param>
     9         /// <param name="connectionId"></param>
    10         /// <returns></returns>
    11         protected override Task OnConnected(IRequest request, string connectionId)
    12         {
    13             //Send方法,向指定人发送消息
    14             return Connection.Send(connectionId, $"用户:{connectionId}登录成功");
    15         }
    16 
    17         /// <summary>
    18         /// 接收请求的方法
    19         /// </summary>
    20         /// <param name="request"></param>
    21         /// <param name="connectionId"></param>
    22         /// <param name="data"></param>
    23         /// <returns></returns>
    24         protected override Task OnReceived(IRequest request, string connectionId, string data)
    25         {
    26             //一对一发送消息
    27             //data是一个json对象 { receiveId: $("#j_receiveId").val(), msg: $("#j_content").val() }
    28             var model = JsonConvert.DeserializeObject<TempData>(data);
    29 
    30             return Connection.Send(model.receiveId, model.msg);
    31         }
    32 
    33         /// <summary>
    34         /// 连接中断调用方法
    35         /// </summary>
    36         /// <param name="request"></param>
    37         /// <param name="connectionId"></param>
    38         /// <param name="stopCalled"></param>
    39         /// <returns></returns>
    40         protected override Task OnDisconnected(IRequest request, string connectionId, bool stopCalled)
    41         {
    42             //告诉所有人该用户退出了(包括自己,也可以配置排除一些用户)
    43             Connection.Broadcast( $"有用户{connectionId}已经退出");
    44             return base.OnDisconnected(request, connectionId, stopCalled);
    45         }
    46 
    47         /// <summary>
    48         /// 当连接在超时后重新连接时调用该方法
    49         /// </summary>
    50         /// <param name="request"></param>
    51         /// <param name="connectionId"></param>
    52         /// <returns></returns>
    53         protected override Task OnReconnected(IRequest request, string connectionId)
    54         {
    55             return base.OnReconnected(request, connectionId);
    56         }
    57     }
    View Code

    2. 前端Html页面

    (1). 引入JS库,这里包括JQuery库和SignalR库(JQuery最低版本为1.6.4)。

     

    (2). 配置路径:$.connection("/myPreConnection1");需要与Startup中的对应

    (3). 常用的几个方法有:

      ① start:开启连接

      ② received:接受服务器发送来的消息

      ③ disconnected:连接中断时调用

      ④ error:连接发生错误的时嗲用

      ④ stop:断开连接

      ⑤ send:发送消息

    另外还有:connectionSlow、stateChanged、reconnecting、reconnected等等

    (4). 当前连接状态有4种

      connecting: 0(正在连接),     connected: 1(正常连接,连接成功中),   reconnecting: 2(正在重连),      disconnected: 4 (掉线了)

    PS: 以上代码和WebSocket确实很像,下图为WebSocket相关方法。

     

    (5). 下面我的代码中的业务

    分享代码:

     1 @{
     2     Layout = null;
     3 }
     4 
     5 <!DOCTYPE html>
     6 
     7 <html>
     8 <head>
     9     @*
    10          Web客户端用法说明
    11          1. 配置路径:$.connection("/myPreConnection1");需要与Startup中的对应
    12          2. 常用的几个方法有:
    13             ① start:开启连接
    14             ② received:接受服务器发送来的消息
    15             ③ disconnected:连接中断时调用
    16             ④ error:连接发生错误的时嗲用
    17             ④ stop:断开连接
    18             ⑤ send:发送消息
    19          另外还有:connectionSlow、stateChanged、reconnecting、reconnected等等
    20         3. 当前连接状态有4种
    21          connecting: 0(正在连接),   connected: 1(正常连接),  reconnecting: 2(正在重连),    disconnected: 4 (掉线了)
    22     *@
    23     <meta name="viewport" content="width=device-width" />
    24     <title>Index</title>
    25     <script src="~/Scripts/jquery-3.3.1.min.js"></script>
    26     <script src="~/Scripts/jquery.signalR-2.3.0.js"></script>
    27     <script type="text/javascript">
    28         $(function () {
    29             var conn = $.connection("/myPreConnection1");
    30             //一. 监控
    31             //1. 接受服务器发来的消息
    32             conn.received(function (data) {
    33                 $("#j_Msg").append("<li>" + data + "</li>");
    34             });
    35             //2. 连接断开的方法
    36             conn.disconnected(function () {
    37                 $("#j_notice").html("连接中断");
    38             });
    39             //3. 连接发生错误时候触发
    40             conn.error(function (data) {
    41                 $("#j_notice").html(data);
    42             });
    43             //二. 主动事件
    44             //1.建立连接
    45             $("#j_connect").click(function () {
    46                 conn.start(function () {
    47                     $("#j_notice").html("连接成功");
    48                 });
    49             });
    50             //2.断开连接
    51             $("#j_close").click(function () {
    52                 conn.stop();
    53             });
    54             //3.发送消息
    55             $("#j_send").click(function () {
    56                 //发送消息之前要判断连接状态,conn.state有4中状态
    57                 //connecting: 0(正在连接),   connected: 1(正常连接),  reconnecting: 2(正在重连),    disconnected: 4 (掉线了)
    58                 console.log(conn.state);
    59                 if (conn.state == 1) {
    60                     conn.send({ receiveId: $("#j_receiveId").val(), msg: $("#j_content").val() });
    61 
    62                 } else if (conn.state == 0) {
    63                     $("#j_notice").html("正在连接中,请稍等");
    64                 } else if (conn.state == 2) {
    65                     $("#j_notice").html("正在重连,请稍等");
    66                 } else if (conn.state == 4) {
    67                     $("#j_notice").html("掉线了,请重新连接");
    68                 }
    69 
    70             });
    71 
    72         });
    73     </script>
    74 </head>
    75 <body>
    76     <div>
    77         <div><span>提示:</span><span id="j_notice"></span></div>
    78         <div style="margin-top:20px">
    79             <button id="j_connect">建立连接</button>
    80             <button id="j_close">关闭连接</button>
    81         </div>
    82         <div style="margin-top:20px">
    83             <input type="text" value="" placeholder="请输入接收人的标记" id="j_receiveId" />
    84             <input type="text" value="" placeholder="请输入发送内容" id="j_content" />
    85             <button id="j_send">发送消息</button>
    86         </div>
    87         <div>
    88             <ul id="j_Msg"></ul>
    89         </div>
    90     </div>
    91 </body>
    92 </html>
    View Code

    (6). 运行效果

    四. 分组的概念

    1. PersistentConnection类中提供了一个 IConnectionGroupManager Groups的概念,即可以将不同用户分到不同组里,就好比QQ的中的讨论组, 在这个组里发信息,该组里的所有人都能看到,但别的组是看不到的。并提供了两个方法分别是

      ①. 加入组:Task Add(string connectionId, string groupName)

      ②. 移除组:Task Remove(string connectionId, string groupName)

    IConnectionGroupManager下提供两个针对组进行发送消息的方法

      ①. 针对单个组(可以去掉不发送的人):Task Send(string groupName, object value, params string[] excludeConnectionIds);

      ②. 针对多个组(可以去掉不发送的人):Task Send(IList<string> groupNames, object value, params string[] excludeConnectionIds);

    注:一个客户端可以同时加入多个组的,就好比qq,一个用户你可以同时在多个讨论组里讨论,相互不影响。

    2. 需求背景:

      有两个房间,分别是room1和room2,将2个人加入到room1里,2两个人加入到room2里,1个既加入room1且加入room2,测试向指定组发送消息和普通的群发消息。

    测试页面如下图:

    3. 先贴代码后分析

    实体类代码

     1   public class RoomData
     2     {
     3         /// <summary>
     4         /// 房间名称
     5         /// </summary>
     6         public string roomName { get; set; }
     7 
     8         /// <summary>
     9         /// 发送的消息
    10         /// </summary>
    11         public string msg { get; set; }
    12 
    13         /// <summary>
    14         /// 用来区分是进入房间,还是普通的发送消息
    15         /// "enter":表示进入房间
    16         /// "sendRoom":表示向某个组发送信息
    17         /// "":表示普通的消息发送,不区分组的概念
    18         /// </summary>
    19         public string action { get; set; }
    20     }
    View Code

    服务器端代码

     1  public class MyPresitentConnection2 : PersistentConnection
     2     {
     3         protected override Task OnConnected(IRequest request, string connectionId)
     4         {
     5             //提示自己进入成功
     6             return Connection.Send(connectionId, "Welcome!");
     7         }
     8 
     9         protected override Task OnReceived(IRequest request, string connectionId, string data)
    10         {
    11             //data是一个json对象 { roomName: "room2", action: "enter", msg: "" }
    12             var model = JsonConvert.DeserializeObject<RoomData>(data);
    13             if (model.action == "enter")
    14             {
    15                 //表示建立组关系
    16                 this.Groups.Add(connectionId, model.roomName);
    17                 //提示自己进入房间成功
    18                 Connection.Send(connectionId, $"进入{model.roomName}房间成功");
    19                 //向该组中除了当前人外,均发送欢迎消息
    20                 return this.Groups.Send(model.roomName, $"欢迎{connectionId}进入{model.roomName}房间", connectionId);
    21             }
    22             else if (model.action == "sendRoom")
    23             {
    24                 //表示普通的按组发送信息(除了自己以外)
    25                 return this.Groups.Send(model.roomName, string.Format("用户 {0} 发来消息: {1}", connectionId, model.msg), connectionId);
    26             }
    27             else
    28             {
    29                 //表示普通的群发,不分组
    30                 return Connection.Broadcast(string.Format("用户 {0} 发来消息: {1}", connectionId, model.msg), connectionId);
    31             }
    32         }
    33     }
    View Code

    Html代码

     1 @{
     2     Layout = null;
     3 }
     4 
     5 <!DOCTYPE html>
     6 
     7 <html>
     8 <head>
     9     <meta name="viewport" content="width=device-width" />
    10     <title>Index</title>
    11     <script src="~/Scripts/jquery-3.3.1.min.js"></script>
    12     <script src="~/Scripts/jquery.signalR-2.3.0.js"></script>
    13     <script type="text/javascript">
    14         $(function () {
    15             var conn = $.connection("/myPreConnection2");
    16             //一. 监控
    17             //1. 接受服务器发来的消息
    18             conn.received(function (data) {
    19                 $("#j_Msg").append("<li>" + data + "</li>");
    20             });
    21             //2. 连接断开的方法
    22             conn.disconnected(function () {
    23                 $("#j_notice").html("连接中断");
    24             });
    25             //二. 主动事件
    26             //1.建立连接
    27             $("#j_connect").click(function () {
    28                 conn.start().done(function () {
    29                     $("#j_notice").html("连接成功");
    30                 });
    31             });
    32             //2.断开连接
    33             $("#j_close").click(function () {
    34                 conn.stop();
    35             });
    36             //3.进入room1
    37             $("#j_room1").click(function () {
    38                 conn.send({ roomName: "room1", action: "enter",msg:"" });
    39             });
    40             //4.进入room2
    41             $("#j_room2").click(function () {
    42                 conn.send({ roomName: "room2", action: "enter", msg: "" });
    43             });
    44             //5. 给room1中的用户发送消息
    45             $("#j_sendRoom1").click(function () {
    46                 conn.send({ roomName: "room1", action: "sendRoom", msg: $('#j_content').val() });
    47             });
    48             //6. 给room2中的用户发送消息
    49             $("#j_sendRoom2").click(function () {
    50                 conn.send({ roomName: "room2", action: "sendRoom", msg: $('#j_content').val() });
    51             });
    52             //7. 普通群发消息
    53             $("#j_sendAll").click(function () {
    54                 conn.send({ roomName: "", action: "", msg: $('#j_content').val() });
    55             });
    56 
    57         });
    58     </script>
    59 </head>
    60 <body>
    61     <div>
    62         <div><span>提示:</span><span id="j_notice"></span></div>
    63         <div style="margin-top:20px">
    64             <button id="j_connect">建立连接</button>
    65             <button id="j_close">关闭连接</button>
    66         </div>
    67         <div style="margin-top:20px">
    68             <button id="j_room1">进入room1</button>
    69             <button id="j_room2">进入room2</button>
    70         </div>
    71         <div style="margin-top:20px">
    72             <input type="text" value="" placeholder="请输入发送内容" id="j_content" />
    73             <button id="j_sendRoom1">给room1发送消息</button>
    74             <button id="j_sendRoom2">给room2发送消息</button>
    75             <button id="j_sendAll">普通群发</button>
    76         </div>
    77         <div>
    78             <ul id="j_Msg"></ul>
    79         </div>
    80     </div>
    81 </body>
    82 </html>
    View Code

    代码分析:

      通过客户端发送过来的action字段来区分几种情况。

        ① 当为“enter”时,表示建立组关系,并提示自己进入房间成功,通知其他人欢迎信息。

        ② 当为“sendRoom”时,表示向指定组发送消息

        ③ 当为空时,表示普通的向所有人发送消息,不区分组的概念

    4. 效果展示(实在是难截图啊)

     

    5. 开始吐槽

      本来框架默认提供一个组的概念,方便了我们对一些业务的开发,是一好事,但是竟然不能获取每个组内的connectionId列表,这。。。太坑了,不伦不类的,还得自己记录一下哪个组中有哪些connectionId,坑啊,微软baba真不知道你是怎么想的。

    五. 跨域请求

    1. SignalR跨域请求的默认是关闭的,我们可以自行开启,SignalR支持的跨域请求有两种:

      ①:JSONP的模式,仅支持Get请求,需要服务器端配合,传输数据大小有限制

      ②:Cors模式,支持Post、Get等请求,需要在浏览器中加 【Access-Control-Allow-Origin:*】类似的配置

    2. 开启跨域请求的方式,详见下面代码:

    复制代码
     1  public class Startup
     2     {
     3         public void Configuration(IAppBuilder app)
     4         {
     5             //1. JSONP的模式
     6             //app.MapSignalR<MyPresitentConnection1>("/myPreConnection1", new Microsoft.AspNet.SignalR.ConnectionConfiguration()
     7             //{
     8             //    EnableJSONP = true
     9             //});
    10 
    11             //2. Cors的模式(需要Nuget安装:Microsoft.Owin.Cors程序集)
    12             //app.Map("/myPreConnection1", (map) =>
    13             //{
    14             //    map.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
    15             //});
    16 
    17             //3. JSONP和Cors同时开启
    18             //app.Map("/myPreConnection1", (map) =>
    19             //{
    20             //    //1. 开启 jsonp
    21             //    map.RunSignalR<MyPresitentConnection1>(new Microsoft.AspNet.SignalR.HubConfiguration() { EnableJSONP = true });
    22             //    //2. 开启cors
    23             //    map.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
    24             //});
    25 
    26         }
    27     }
    复制代码

    3. 跨域请求的作用是什么,在后面章节和Hubs模型一起介绍

    六. 第三方调用

      以上所有的代码与通信相关的代码都写在永久连接类中,但在开发中经常会遇到,我要在控制器中的某个方法中调用相应的方法,发给用户信息,这个时候怎么办呢?

      可以通过:GlobalHost.ConnectionManager.GetConnectionContext<MyPresitentConnection1>();来获取永久连接类,然后调用相应的方法。

      代码如下:

    复制代码
     1          /// <summary>
     2          /// 向所有人发送消息
     3         /// </summary>
     4         /// <param name="msg">发送的信息</param>
     5         public string MySendAll(string msg)
     6         {
     7             string myConnectionId = Session["connectionId"].ToString();
     8             //PersistentConnection模式
     9             var perConnection = GlobalHost.ConnectionManager.GetConnectionContext<MyPresitentConnection1>();
    10             perConnection.Connection.Broadcast(msg);
    11             return "ok";
    12         }
    复制代码

      关于具体结合业务的样例在下一节的Hub的例子详细编写,原理都一样。

    七. 总结

      以上主要介绍了PersistentConnection模型的一些常规用法,仅能起到一个简单的引导作用,在项目中,还需要结合实际业务场景做好相应的限制和一些极端情况的处理,该模型介绍到此为止,我个人不是很喜欢它,在项目中也很少采用这种模式。
      推荐使用SignalR的中心模型(Hubs),Hubs这种模式才是SignalR的灵魂所在(个人观点),后面的几节详细来介绍Hubs模型的使用,感兴趣的朋友可以关注下一节:“Hubs模型的灵活之处”(本周更新),欢迎朋友们在下方留言讨论和指错,如有不足,请勿谩骂,谢谢。
      

     ***********转摘:https://www.cnblogs.com/yaopengfei/p/9284101.html

  • 相关阅读:
    集群架构搭建
    THUWC2019 游记
    【集训队互测2015】未来程序·改
    [NOIP2014普及组T1]珠心算测验
    [CF912D]Fishes
    [POJ2409]Let it Bead
    golang 统计系统测试覆盖率
    tcpdump常用方法
    数学闯关引发的思考
    linux lsof常用方法
  • 原文地址:https://www.cnblogs.com/linybo/p/13925849.html
Copyright © 2020-2023  润新知