1. 程式人生 > >ASP.Net Core 3.1 使用實時應用SignalR入門

ASP.Net Core 3.1 使用實時應用SignalR入門

參考文章:微軟官方文件:https://docs.microsoft.com/zh-cn/aspnet/core/signalr/introduction?view=aspnetcore-3.1 和 https://www.cnblogs.com/cgzl/p/9509207.html 系列

ASP.NET Core SignalR 簡介

什麼是 SignalR?

ASP.NET Core SignalR 是一種開放原始碼庫,可簡化將實時 web 功能新增到應用程式的功能。 實時 web 功能使伺服器端程式碼可以立即將內容推送到客戶端。

適用於 SignalR :

  • 需要從伺服器進行高頻率更新的應用。 示例包括遊戲、社交網路、投票、拍賣、地圖和 GPS 應用。
  • 儀表板和監視應用。 示例包括公司儀表板、即時銷售更新或旅行警報。
  • 協作應用。 協作應用的示例包括白板應用和團隊會議軟體。
  • 需要通知的應用。 社交網路、電子郵件、聊天、遊戲、旅行警報和很多其他應用都需使用通知。

SignalR 提供一個 API,用於建立 (RPC) 的伺服器到客戶端 遠端過程呼叫 。 Rpc 通過伺服器端 .NET Core 程式碼從客戶端呼叫 JavaScript 函式。

下面是的某些功能 SignalR ASP.NET Core:

  • 自動處理連線管理。
  • 將訊息同時傳送到所有連線的客戶端。 例如,聊天室。
  • 向特定客戶端或客戶端組傳送訊息。
  • 可縮放以處理不斷增加的流量。

傳輸

SignalR 支援以下用於按正常回退) (處理實時通訊的技術:

  • WebSockets
  • Server-Sent 事件
  • 長輪詢

SignalR 自動選擇伺服器和客戶端功能內的最佳傳輸方法。

中心

SignalR 使用 集線器 在客戶端和伺服器之間進行通訊。

中心是一種高階管道,它允許客戶端和伺服器分別呼叫方法。 SignalR 自動處理跨計算機邊界的排程,使客戶端能夠在伺服器上呼叫方法,反之亦然。 可以將強型別引數傳遞給方法,從而啟用模型繫結。 SignalR 提供了兩個內建的集線器協議:基於 JSON 的文字協議和基於 MessagePack的二進位制協議。 與 JSON 相比,MessagePack 通常會建立較小的訊息。 較早的瀏覽器必須支援 XHR 級別 2 ,才能提供 MessagePack 協議支援。

中心通過傳送包含客戶端方法的名稱和引數的訊息來呼叫客戶端程式碼。 作為方法引數傳送的物件將使用配置的協議進行反序列化。 客戶端嘗試將名稱與客戶端程式碼中的方法匹配。 當客戶端找到匹配項時,它將呼叫方法並向其傳遞反序列化的引數資料。

 

下面介紹SignalR所用到的技術和原理

實時Web簡述

大家都見過和用過實時Web, 例如網頁版的即時通訊工具, 網頁直播, 網頁遊戲, 還有股票儀表板等等.

 

傳統的Web應用是這樣工作的:

瀏覽器傳送HTTP請求到ASP.NET Core Web伺服器, 如果一切順利的話, Web伺服器會處理請求並返回響應, 在Payload裡面會包含所請求的資料.

 

但是這種工作方式對實時Web是不靈的. 實時Web需要伺服器可以主動傳送訊息給客戶端(可以是瀏覽器):

Web伺服器可以主動通知客戶端資料的變化, 例如收到了新的對話訊息.

 

"底層"技術

而SignalR使用了三種"底層"技術來實現實時Web, 它們分別是Long Polling, Server Sent Events 和 Websocket.

首先, 得知道什麼是Ajax. 這個就不介紹了.

 

Long Polling

Polling

介紹Long Polling之前, 首先介紹一下Polling.

Polling是實現實時Web的一種笨方法, 它就是通過定期的向伺服器傳送請求, 來檢視伺服器的資料是否有變化.

如果伺服器資料沒有變化, 那麼就返回204 No Content; 如果有變化就把最新的資料傳送給客戶端:

下面是Polling的一個實現, 非常簡單:

1、建立一個web MVC程式,在新建立的程式新增一個控制器命名為:DemoTestController 程式碼如下

 1 using Microsoft.AspNetCore.Http;
 2 using Microsoft.AspNetCore.Mvc;
 3 using Microsoft.AspNetCore.SignalR;
 4 using Newtonsoft.Json;
 5 using System;
 6 using System.Collections.Generic;
 7 using System.Linq;
 8 using System.Net.WebSockets;
 9 using System.Text;
10 using System.Threading;
11 using System.Threading.Tasks;
12 
13 namespace SignaIRDemoTwo.Controllers
14 {
15     public class DemoTestController : Controller
16     {
17        
18         #region Polling
19 
20         /// <summary>
21         ///  Polling
22         /// </summary>
23         /// <returns></returns>
24         public IActionResult PollingTest()
25         {
26             return View();
27         }
28 
29 
30         /// <summary>
31         ///  Polling
32         /// </summary>
33         /// <param name="count"></param>
34         /// <returns></returns>
35         public IActionResult PollingTest_GetCount(int id)
36         {
37             var count = GetLastedCount();
38             if (id > 10)
39             {
40                 return Ok(new { id, count, finished = true });
41 
42             }
43             if (id > 6)
44             {
45                 return Ok(new { id, count });
46             }
47             return NotFound();
48         }
49 
50         #endregion
51 
52         private static int _count;
53         public int GetLastedCount()
54         {
55             _count++;
56             return _count;
57         }
58 
59     }
60 }
View Code

 

就看這個Controller的PollingTest_GetCount方法即可. 用到了GetLastedCount方法, 就是做了一個全域性的Count, 它的GetLatestCount會返回最新的Count.

Controller裡面的程式碼意思是: 如果Count > 6 就返回一個物件, 裡面包含count的值和傳進來的id; 如果 count > 10, 還要返回一個finished標誌.

PollingTest頁面的前端程式碼:

 1 <div>
 2     <button id="btnStart" type="button">開始Polling</button>
 3     <span id="result" style="color:red;font-weight:bolder;">
 4 
 5     </span>
 6 </div>
 7 <script src="~/lib/jquery/dist/jquery.js"></script>
 8 <script>
 9     var intervalId;
10     function poll(id) {
11         fetch('/DemoTest/PollingTest_GetCount?id='+id)
12             .then(function (response) {
13                 if (response.status === 200) {
14                     return response.json().then(j => {
15                         const resultDiv = document.getElementById("result");
16                         resultDiv.innerHTML = j.count;
17                         if (j.finished) {
18                             clearInterval(intervalId);
19                             resultDiv.innerHTML = j.count + ",已結束";
20                         }
21 
22                     })
23                 }
24                 ;
25             });
26     }
27 
28 
29     $(function () {
30 
31         $("#btnStart").click(function () {
32             intervalId = setInterval(poll, 3000, 8)
33         });
34     });
35 </script>
View Code

 

也是非常的簡單, 點選按鈕後定時傳送請求, 如果有結果就顯示最新count值; 如果有finished標誌, 就顯示最新值和已結束.

注意這裡使用的是fetch API.

 

執行專案, count > 6的時候:

count > 10的時候結束:

這就是Polling, 很簡單, 但是比較浪費資源.

SignalR沒有采用Polling這種技術.

 

Long Polling

Long Polling和Polling有類似的地方, 客戶端都是傳送請求到伺服器. 但是不同之處是: 如果伺服器沒有新資料要發給客戶端的話, 那麼伺服器會繼續保持連線, 直到有新的資料產生, 伺服器才把新的資料返回給客戶端.

如果請求發出後一段時間內沒有響應, 那麼請求就會超時. 這時, 客戶端會再次發出請求.

 

例子, 在DemoTestController的程式碼中新增一個名稱為LongPollingTest的 Action:

 1  #region Long Polling 
 2 
 3         public IActionResult LongPollingTest()
 4         {
 5             return View();
 6         }
 7 
 8         public IActionResult LongPollingTest_GetCount(int id)
 9         {
10             int count = GetLastedCount();
11             Thread.Sleep(1000);
12             if (count > 5)
13             {
14                 return Ok(new { id, count, finished = true });
15             }
16             else
17             {
18                 return Ok(new { id, count, finished = false });
19             }
20         }
21 
22         #endregion
View Code

LongPollingTest_GetCount方法是前端點選按鈕後請求的方法。

改動的目的就是在符合要求的資料出現之前, 保持連線開放.

LongPollingTest頁面前端也有一些改動:

 1 <div>
 2     <button id="btnStart" type="button">開始Polling</button>
 3     <span id="result" style="color:red;font-weight:bolder;">
 4 
 5     </span>
 6 </div>
 7 <script src="~/lib/jquery/dist/jquery.js"></script>
 8 <script>
 9     pollwithTimeout = (url, options, timeout = 9000) => {
10         return Promise.race([fetch(url, options), new Promise((_, reject) =>
11             setTimeout(() => reject(new Error('timeout')), timeout)
12         )]);
13     };
14     //pollWithTimeout方法使用了race, 如果請求後超過9秒沒有響應, 那麼就返回超時錯誤.
15 
16     //poll裡面, 如果請求返回的結果是200, 那麼就更新UI.但是如果沒有finished標誌, 就繼續發出請求.
17     function poll(id) {
18         pollwithTimeout('/DemoTest/LongPollingTest_GetCount?id='+id)
19             .then( response => {
20                 if (response.status === 200) {
21                     return response.json().then(j => {
22                         const resultDiv = document.getElementById("result");
23                         resultDiv.innerHTML = j.count;
24                         if (!j.finished) {
25                             poll(id);
26                         }
27 
28                     })
29                 }
30                 ;
31             }).catch(response=>poll(id));
32     };
33 
34 
35     $(function () {
36 
37         $("#btnStart").click(function () {
38             poll(123);
39         });
40     });
41 </script>
View Code

 

pollWithTimeout方法使用了race, 如果請求後超過9秒沒有響應, 那麼就返回超時錯誤.

poll裡面, 如果請求返回的結果是200, 那麼就更新UI. 但是如果沒有finished標誌, 就繼續發出請求.

 

執行後分別出現數字:1 2 3 4 5 6 在數字6那裡停止請求

可以看到只有一個請求, 請求的時間很長, 標識連線開放了很長時間.

這裡需要注意的一點是, 伺服器的超時時長和瀏覽器的超時時長可能不一樣.

 

前邊介紹的Polling和Long Polling都是HTTP請求, 這其實並不是很適合.

下面介紹稍微一個好點的技術: 

 

Server Sent Events (SSE)

使用SSE的話, Web伺服器可以在任何時間把資料傳送到瀏覽器, 可以稱之為推送. 而瀏覽器則會監聽進來的資訊, 這些資訊就像流資料一樣, 這個連線也會一直保持開放, 直到伺服器主動關閉它.

瀏覽器會使用一個叫做EventSource的物件用來處理傳過來的資訊.

 

 

 例子,在該控制器繼續新增一個名為SSETest 的action 這和之前的程式碼有很多地方不同, 用到了Reponse,C#程式碼如下:

 1  #region Server Sent Events (SSE)
 2 
 3         public IActionResult SSETest()
 4         {
 5             return View();
 6         }
 7 
 8 
 9         public async void SSEGet(int id)
10         {
11             Response.ContentType = "text/event-stream";
12             int count;
13             do 
14             {
15                 count = GetLastedCount();
16                 Thread.Sleep(1000);
17                 if (count % 3 == 0)
18                 {
19                     //注意SSE返回資料的只能是字串, 而且以data:開頭, 後邊要跟著換行符號, 否則EventSource會失敗.
20                     await HttpContext.Response.WriteAsync($"data:{count}\n\n");
21                     await HttpContext.Response.Body.FlushAsync();
22                 }
23 
24             } while(count<10);
25 
26             Response.Body.Close();
27         }
28 
29         #endregion
View Code

 

注意SSE返回資料的只能是字串, 而且以data:開頭, 後邊要跟著換行符號, 否則EventSource會失敗.

 

頁面客戶端:

 1 <div class="text-center">
 2     <h1 class="display-4">Test</h1>
 3     <div>
 4         <button id="btn" type="submit">開始Polling</button>
 5         結果<span id="result" style="color:red;font-weight:bolder"></span>
 6     </div>
 7 
 8 </div>
 9 <script>
10 
11     listen = (id) => {
12         const eventSource = new EventSource('/DemoTest/SSEGet?id=' + id);
13         eventSource.onmessage = (event) => {
14             const resultDiv = document.getElementById("result");
15             console.log(event.data);
16             resultDiv.innerHTML = event.data;
17         };
18         eventSource.onerror = function (e) {
19             console.log("EventSource failed",e);
20         }
21     };
22     document.getElementById("btn").addEventListener("click", e => {
23         e.preventDefault();
24         listen(123);//id 123
25     });
26 
27 </script>
View Code

 

這個就很簡單了, 使用EventSource的onmessage事件. 前一個請求等到響應回來後, 會再發出一個請求.

 

執行:

 

這個EventSource要比Polling和Long Polling好很多.

它有以下優點: 使用簡單(HTTP), 自動重連, 雖然不支援老瀏覽器但是很容易polyfill.

而缺點是: 很多瀏覽器都有最大併發連線數的限制, 只能傳送文字資訊, 單向通訊.

 

Web Socket

Web Socket是不同於HTTP的另一個TCP協議. 它使得瀏覽器和伺服器之間的互動式通訊變得可能. 使用WebSocket, 訊息可以從伺服器發往客戶端, 也可以從客戶端發往伺服器, 並且沒有HTTP那樣的延遲. 資訊流沒有完成的時候, TCP Socket通常是保持開啟的狀態.

使用線代瀏覽器時, SignalR大部分情況下都會使用Web Socket, 這也是最有效的傳輸方式. 

全雙工通訊: 客戶端和伺服器可以同時往對方傳送訊息.

並且不受SSE的那個瀏覽器連線數限制(6個), 大部分瀏覽器對Web Socket連線數的限制是50個.

訊息型別: 可以是文字和二進位制, Web Socket也支援流媒體(音訊和視訊).

其實正常的HTTP請求也使用了TCP Socket. Web Socket標準使用了握手機制把用於HTTP的Socket升級為使用WS協議的 WebSocket socket.

 

生命週期

Web Socket的生命週期是這樣的:

所有的一切都發生在TCP Socket裡面, 首先一個常規的HTTP請求會要求伺服器更新Socket並協商, 這個叫做HTTP握手. 然後訊息就可以在Socket裡來回傳送, 直到這個Socket被主動關閉. 在主動關閉的時候, 關閉的原因也會被通訊.

 

HTTP 握手

每一個Web Socket開始的時候都是一個簡單的HTTP Socket.

客戶端首先發送一個GET請求到伺服器, 來請求升級Socket. 

如果伺服器同意的話, 這個Socket從這時開始就變成了Web Socket.

 

這個請求的示例如下:

第一行表明這就是一個HTTP GET請求.

Upgrade 這個Header表示請求升級socket到Web Socket.

Sec-WebSocket-Key, 也很重要, 它用於防止快取問題, 具體請檢視官方文件.

 

伺服器理解並同意請求以後, 它的響應如下:

返回101狀態碼, 表示切換協議.

如果返回的不是101, 那麼瀏覽器就會知道伺服器沒有處理WebSocket的能力.

此外Header裡面還有Upgrade: websocket.

Sec-WebSocket-Accept是配合著Sec-WebSocket-Key來運作的, 具體請查閱官方文件.

 

訊息型別

Web Socket的訊息型別可以是文字, 二進位制. 也包括控制類的訊息: Ping/Pong, 和關閉.

每個訊息由一個或多個Frame組成:

所有的Frame都是二進位制的. 所以文字的話, 就會首先轉化成二進位制.

 

Frame 有若干個Header bits.

有的可以表示這個Frame是否是訊息的最後一個Frame;

有的可以表示訊息的型別.

有的可以表示訊息是否被掩蔽了. 客戶端到伺服器的訊息被掩蔽了, 為了防止快取投毒(使用惡意資料替換快取).

有的可以設定payload的長度, payload會佔據frame剩下的地方.

 

實際上用的時候, 你基本不會觀察到frame, 它是在後臺處理的, 你能看到的就是完整的訊息.

但是在瀏覽器除錯的時候, 你看到的是frame挨個傳遞進來而不是整個訊息.

 

看下例子:

首先ASP.NET Core專案裡已經內建了WebSocket, 但是需要配置和使用這個中介軟體, 在Startup:

這裡我們設定了每隔120秒就ping一下. 還設定用於接收和解析frame的快取大小. 其實這兩個值都是預設的值.

 

修改後的Controller:

 1   #region WebSocket
 2 
 3         public IActionResult WebSocketTest()
 4         {
 5             return View();
 6         }
 7 
 8 
 9         public async void WebSocketGet(int id)
10         {
11             if (HttpContext.WebSockets.IsWebSocketRequest)
12             {
13                 var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
14                 await SendEvents(webSocket, id);
15                 await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Done", CancellationToken.None);
16             }
17             else
18             {
19                 HttpContext.Response.StatusCode = 400;
20             }
21         }
22 
23 
24         private async Task SendEvents(WebSocket webSocket, int id)
25         {
26             int count;
27             do
28             {
29                 count = GetLastedCount();
30                 Thread.Sleep(1000);
31                 if (count % 3 != 0) continue;
32                 var obj = new { id, count };
33                 var jsonStr = JsonConvert.SerializeObject(obj);
34                 await webSocket.SendAsync(buffer: new ArraySegment<byte>(array:Encoding.UTF8.GetBytes(jsonStr),offset:0,count: jsonStr.Length),messageType:WebSocketMessageType.Text,endOfMessage:true,cancellationToken: CancellationToken.None);
35             } while (count < 10);
36         }
37 
38 
39         #endregion
View Code

 

這裡需要注入HttpContextAccessor. 然後判斷請求是否是WebSocket請求, 如果是的話, 客戶端會收到回覆, 這時Socket就升級完成了. 升級完返回一個webSocket物件, 然後我把events通過它傳送出去. 隨後我關閉了webSocket, 並指明瞭原因NormalClosure.

 

然後看看SendEvents方法:

這裡的重點就是webSocket物件的SendAsync方法. 我需要把資料轉化成buffer進行傳送. 資料型別是Text. 具體引數請檢視文件.

 

看一下頁面客戶端:

 1 <div class="text-center">
 2     <h1 class="display-4">Test</h1>
 3     <div>
 4         <button id="btn" type="submit">開始Polling</button>
 5         結果<span id="result" style="color:red;font-weight:bolder"></span>
 6     </div>
 7 
 8 </div>
 9 <script>
10 
11 
12     listen = (id) => {
13         const socket = new WebSocket('ws://localhost:52136/DemoTest/WebSocketGet?id=' + id);
14         socket.onmessage = event => {
15             var resultDiv = document.getElementById("result");
16             console.log(event.data);
17             resultDiv.innerHTML = JSON.parse(event.data).count;
18         }
19     };
20     document.getElementById("btn").addEventListener("click", e => {
21         e.preventDefault();
22         listen(123);//id 123
23     });
24 
25 </script>
View Code

 

也很簡單, 這裡有一個WebSocket物件, 注意這裡的url開頭是ws而不是http, 還有一個wss, 就先當與http裡的https.

然後eventhandler和SSE的差不多. 返回的json資料需要先parse, 然後再使用.

 介紹到這裡下面開始真正的寫SignalR的運用吧!

 建立一個TestHub, 繼承於Hub程式碼如下:

 1 using Microsoft.AspNetCore.Authorization;
 2 using Microsoft.AspNetCore.SignalR;
 3 using System;
 4 using System.Collections.Generic;
 5 using System.Linq;
 6 using System.Threading;
 7 using System.Threading.Tasks;
 8 
 9 namespace SignaIRDemoTwo
10 {
11     //[Authorize]
12     public class TestHub: Hub
13     {
14         private static int count;
15 
16         private int GetLastCount()
17         {
18            return count++;
19         }
20 
21         public async Task GetLastedCount(string random)
22         {
23             int count;
24             do
25             {
26                 count = GetLastCount();
27                 Thread.Sleep(1000);
28                 await Clients.All.SendAsync("ReceiveMessage", count);
29             } while (count < 10);
30             Thread.Sleep(2000);
31             await Clients.All.SendAsync("Finished");
32         }
33 
34         //從Hub的Context屬性, 可以獲得使用者的資訊.
35         public override async Task OnConnectedAsync()
36         {
37             //var userName = Context.User.Identity.Name;
38             //return base.OnConnectedAsync();
39             //ConnectionId就是連線到Hub的這個客戶端的唯一標識.
40             var connectionId = Context.ConnectionId;
41             await Clients.Clients(connectionId).SendAsync("someFunc", new { });
42             //await Clients.AllExcept(connectionId).SendAsync("someFunc");
43             //await Groups.AddToGroupAsync(connectionId, "MyGroup");
44             //await Groups.RemoveFromGroupAsync(connectionId, "MyGroup");
45             //await Clients.Group("MyGroup").SendAsync("someFunc");
46         }
47     }
48 }
View Code

Context

從Hub的Context屬性, 我們可以獲得使用者的資訊.

我們在CountHub裡override父類的一個方法OnConnectedAsync():

如果有新的連線建立了, 這個方法就會被執行.

 

在Hub類裡, 我們可以訪問到Context屬性. 從Context屬性那, 我們可以獲得一個常用的屬性叫做ConnectionId. 這個ConnectionId就是連線到Hub的這個客戶端的唯一標識.

使用ConnectionId, 我們就可以取得這個客戶端, 並呼叫其方法, 如圖中的Clients.Client(connectionId).xxx.

Hub的Clients屬性表示客戶端, 它有若干個方法可以選擇客戶端, 剛才的Client(connectionId)就是使用connectionId找到這一個客戶端. 而AllExcept(connectionId)就是除了這個connectionId的客戶端之外的所有客戶端. 更多方法請檢視文件.

SignalR還有Group分組的概念, 而且操作簡單, 這裡用到的是Hub的Groups屬性. 向一個Group名新增第一個connectionId的時候, 分組就被建立. 移除分組內最後一個客戶端的時候, 分組就被刪除了. 使用Clients.Group("組名")可以呼叫組內客戶端的方法.

配置SignalR

在Startup裡註冊SignalR:

 1      public void ConfigureServices(IServiceCollection services)
 2         {
 3             services.AddSignalR();
 4             //Asp.Net Core 3.1 修改Razor檢視即時重新整理的配置步驟
 5             //2. 安裝package,Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation。
 6             //3.Startup的ConfigureServices裡註冊服務:services.AddMvc().AddRazorRuntimeCompilation();
 7 
 8             services.AddControllersWithViews().AddRazorRuntimeCompilation();
 9            
10         }

下面的方法3.1中以棄用了

然後在管道里使用SignalR, 使用app.UseSignalR():  

 推薦下面的紅色加粗字型的內容

 1 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
 2         public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
 3         {
 4             if (env.IsDevelopment())
 5             {
 6                 app.UseDeveloperExceptionPage();
 7             }
 8             else
 9             {
10                 app.UseExceptionHandler("/Home/Error");
11             }
12             //配置和使用這個WebSocket中介軟體
13             var webSocketOptions = new WebSocketOptions()
14             {
15                 KeepAliveInterval = TimeSpan.FromSeconds(120),
16                 ReceiveBufferSize = 4 * 1024
17             };
18             app.UseWebSockets(webSocketOptions);
19             app.UseStaticFiles();
20             //app.UseSignalR(routes => routes.MapHub<TestHub>("/DemoTest"));
21             app.UseRouting();
22 
23             app.UseAuthorization();
24 
25             app.UseEndpoints(endpoints =>
26             {
27                 endpoints.MapControllerRoute(
28                     name: "default",
29                     pattern: "{controller=Home}/{action=Index}/{id?}");
30                 endpoints.MapHub<TestHub>("/DemoTest");
31             });
32         }

使用Hub

首先在之前建立DemoTestController, 並注入IHubContext<CountHub>:

 public class DemoTestController : Controller
    {
        private readonly IHubContext<TestHub> _testHub;
        public DemoTestController(IHubContext<TestHub> testHub) 
        {
            _testHub = testHub;
        }
}

接下來我們就可以使用IHubContext<CountHub>這個物件與客戶端進行實時通訊了.

 

下面建立一個SignalRPost Action, 客戶端點選按鈕之後來到這個Action, 在這裡我們使用hub為所有的客戶端傳送一個訊息:

c#程式碼如下

 1 #region SignalR
 2 
 3         public IActionResult SignalRTest()
 4         {
 5 
 6             return View();
 7         }
 8 
 9         [HttpPost]
10         public async Task<IActionResult> SignalRPost()
11         {
12             await _testHub.Clients.All.SendAsync("someFunc", new { random = "abcd" });
13 
14             Thread.Sleep(2000);
15             return Accepted(1); //202: 請求已被接受並處理,但還沒有處理完成
16         }
17 
18         #endregion
View Code

這裡, 我呼叫了所有客戶端上的someFunc這個方法, 引數是一個物件.

但是使用這種IHubContext<Hub>的注入方式, 我們無法在它那取得Caller(呼叫該方法的客戶端)這個屬性.

 

新增 SignalR 客戶端庫

ASP.NET Core 3.1 共享框架中包含 SignalR 伺服器庫。 JavaScript 客戶端庫不會自動包含在專案中。 對於此教程,使用庫管理器 (LibMan) 從 unpkg 獲取客戶端庫。 unpkg 是一個內容分發網路 (CDN),可分發在 npm(即 Node.js 包管理器)中找到的任何內容。

  • 在“解決方案資源管理器”中,右鍵單擊專案,然後選擇“新增”>“客戶端庫” 。

  • 在“新增客戶端庫”對話方塊中,對於“提供程式”,選擇“unpkg”。

  • 對於“庫”,輸入 @microsoft/signalr@latest

  • 選擇“選擇特定檔案”,展開“dist/browser”資料夾,然後選擇“signalr.js”和“signalr.min.js”。

  • 將“目標位置”設定為 wwwroot/js/signalr/,然後選擇“安裝”。

    LibMan 建立 wwwroot/js/signalr 資料夾並將所選檔案複製到該資料夾。

在 SignalRTest action裡新增對應的客戶端頁面

程式碼如下

 1 <button id="submit">提交</button>
 2 <div id="result" style="color:green;font-weight:bold;"></div>
 3 <script src="~/lib/microsoft/signalr/dist/browser/signalr.js"></script>
 4 
 5 <script>
 6         var connection = new signalR.HubConnectionBuilder().withUrl("/DemoTest").build();
 7 
 8     connection.on("someFunc", function (obj) {
 9         var resultDiv = document.getElementById("result");
10         resultDiv.innerHTML = "Someone called this ,parameters: " + obj.random;
11     });
12     connection.on("ReceiveMessage", function (update) {
13         var resultDiv = document.getElementById("result");
14         resultDiv.innerHTML = update;
15     });
16     connection.on("Finished", function () {
17         connection.stop();
18         var resultDiv = document.getElementById("result");
19         resultDiv.innerHTML = "Finished";
20     });
21     //connection.start().catch(err => console.error(err.toString()));
22     connection.start().then(function () {
23     }).catch(function (err) {
24         return console.error(err.toString());
25     });
26 
27 
28 
29     document.getElementById("submit").addEventListener("click", e => {
30         e.preventDefault();
31         fetch("/DemoTest/SignalRPost", { method: "post" })
32             .then(Response => Response.text())
33             .then(id => connection.invoke("GetLastedCount", id));//id 是1
34 
35         //connection.invoke("GetLastCountTest", "sss").catch(function (err) {
36         //    return console.error(err.toString());
37         //});
38 
39     })
40 </script>
View Code

 

先執行一下看看效果:

可以看到使用Clients.All, 所有的客戶端的方法都會被呼叫.

 

剛開啟頁面的時候, 我們就嘗試建立連線, 從F12可以看到一個叫做negotiate的請求被髮送了:

 

這個請求的body如下:

可以看到客戶端選擇了一個connectionId,  裡面還有瀏覽器支援的傳輸方式.

伺服器的響應:

響應也包含著connectionId, 以及伺服器支援的傳輸方式. 這裡三種都支援. 由於我沒有指定傳輸方式, 所以SignalR選擇了最好的方式: websocket.

 

而在我點選按鈕後, Web Socket連線才被初始化:

 

如果需要手動指定傳輸方式, 請在withUrl()方法的第二個引數指定傳輸方式: 

 

其它型別的客戶端

.NET 客戶端可以安裝 Microsoft.AspNetCore.SignalR.Client 這個包來支援SignalR.

具體用法請檢視官方文件, 語法和js的差不多.

完整程式碼連結 :https://github.com/hudean/signalrDemo