zoukankan      html  css  js  c++  java
  • 第二节: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

  • 相关阅读:
    【原】Coursera—Andrew Ng机器学习—课程笔记 Lecture 15—Anomaly Detection异常检测
    【原】Coursera—Andrew Ng机器学习—课程笔记 Lecture 14—Dimensionality Reduction 降维
    【原】Coursera—Andrew Ng机器学习—课程笔记 Lecture 13—Clustering 聚类
    【原】Coursera—Andrew Ng机器学习—课程笔记 Lecture 12—Support Vector Machines 支持向量机
    【原】机器学习公开课 目录(课程笔记、测验习题答案、编程作业源码)...持续更新...
    【原】Coursera—Andrew Ng机器学习—Week 11 习题—Photo OCR
    【原】Coursera—Andrew Ng机器学习—Week 10 习题—大规模机器学习
    【原】Coursera—Andrew Ng机器学习—Week 9 习题—异常检测
    【原】Coursera—Andrew Ng机器学习—Week 8 习题—聚类 和 降维
    【原】Coursera—Andrew Ng机器学习—Week 7 习题—支持向量机SVM
  • 原文地址:https://www.cnblogs.com/linybo/p/13925849.html
Copyright © 2011-2022 走看看