第三節: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 : 原創博客請在轉載時保留原文連接或在文章開頭加上本人博客地址,如需代碼請在評論處留下你的郵箱
相關文章
相關標籤/搜索