第三节:SignalR之PersistentConnection模型详解(步骤、用法、分组、跨域)
一. 承上声明
几点介绍:
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 }
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 }
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>
(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 }
服务器端代码
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 }
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>
代码分析:
通过客户端发送过来的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模型一起介绍
六. 总结
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 :
- 声 明1 : 本人才疏学浅,用郭德纲的话说“我是一个小学生”,如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,如需代码请在评论处留下你的邮箱
下一篇: MyBatis动态创建表