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

一. 承上声明javascript

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

几点介绍:   html

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

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

二. 从零开始搭建jquery

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

 

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

  

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

  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模型的灵活之处”(本周更新),欢迎朋友们在下方留言讨论和指错,若有不足,请勿谩骂,谢谢。
  

 

 

 

!

  • 做       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 本人才疏学浅,用郭德纲的话说“我是一个小学生”,若有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文连接或在文章开头加上本人博客地址,如需代码请在评论处留下你的邮箱
相关文章
相关标签/搜索