從壹開始 [Admin] 之四 || NetCore + SignalR 實現日志消息推送


緣起

哈嘍大家周一好呀,感覺好久沒有寫文章了,上周出差了一次,感覺還是比坐辦公室好的多,平時在讀一本書《時生》,感興趣的可以看看😂......

這幾天翻看 NetCore 相關知識擴展的時候,發現了久違的一個知識點 —— SignalR ,為啥說久違呢,因為去年的時候,我在公司的項目里就想用了,后來組員說他學學看,也沒有了下文,我也就耽擱了,昨天突然看到這個了,想着正好看看吧,盡量落地到 NetCore 項目上,當時我很自信的以為這個技術很老了,應該用的人很多,可是天不遂人願,在.Net MVC中使用的很多有六成,在NetCore 的小demo有兩成,NetCore + Vue 一起使用的就是寥寥無幾了,而且更多的是仿照官網的,好吧,我就簡單寫一個吧,希望對大家有所幫助,盡量將這個技術落地。

 

你一定很好奇,為啥要學 SignalR ,或者說它有啥作用,那我先說幾個場景,你就知道了:

1、用戶登錄處理相關場景; //平時我們是ajax請求,等待后端處理,然后返回 true ,根據返回結果相應處理;

2、后端(c#)強制用戶退出登錄(js); //這個后端還真沒有做過,沒啥思路

3、用戶支付訂單,等待成功后跳轉; //我以前用的是 ajax 輪詢,額。。。

4、給用戶發消息,或網頁內簡單的聊天;

5、秒殺用戶名單,在頁面實時進行滑動展示;

大家從這幾個栗子中,可以看到一個共性:

就是想用后端來操作前端,也就是說可以通過 服務端代碼,來控制前端 js 事件,來實現響應式的實時場景過程,用戶完全不用做任何操作,或者做少量的操作就能實現更多的效果。

大家可以先自己用自己平時的想法和經驗來實現上邊的場景,當然並不是一定使用 SignalR ,Scoket 現在也是很火,本文就是簡單說說 SignalR 的基礎用法,想了想,怎么才能讓這個技術落地,最后決定將全部的操作日志通過 SignalR 的形式在Admin后台展示出來吧,以后也試試強制登錄的功能,請看:

動圖:

 

 

 

一、什么是SignalR?

1、基本概念

本文重點說如何使用,但是為了文章的完整性,還是粘貼了一些概念講解,參考《ASP.NET Core SignalR 簡介》,更多的知識點請自行研究吧,網上這種概念類文章很多:

 

ASP.NET Core SignalR 是一個開源代碼庫,它簡化了向應用添加實時 Web 功能的過程。 實時 Web 功能使服務器端代碼能夠即時將內容推送到客戶端。

SignalR的可使用Web Socket, Server Sent Events 和 Long Polling作為底層傳輸方式。

SignalR 的適用對象:

  • 需要來自服務器的高頻率更新的應用。 例如:游戲、社交網絡、投票、拍賣、地圖和 GPS 應用。
  • 儀表板和監視應用。 示例包括公司儀表板、銷售狀態即時更新或行程警示。
  • 協作應用。 協作應用的示例包括白板應用和團隊會議軟件。
  • 需要通知的應用。 社交網絡、電子郵件、聊天、游戲、行程警示以及許多其他應用都使用通知。

SignalR 提供了一個用於創建服務器到客戶端 《遠程過程調用(RPC的 API。 RPC 通過服務器端 .NET Core 代碼調用客戶端上的 JavaScript 函數。

以下是 ASP.NET Core SignalR 的一些功能:

  • 自動管理連接。
  • 同時向所有連接的客戶端發送消息。 例如,聊天室。
  • 將消息發送到特定的客戶端或客戶端組。
  • 擴展以處理增加的流量。

 

 

2、支持平台

 

服務端:ASP.NET Core SignalR 適用於 ASP.NET Core 支持的任何服務器平台。

JS 客戶端:需要支持NodeJS 8+、或者常見主流瀏覽器都支持。

Java 客戶端:支持Java 8或更高版本。

Net 客戶端:可以在 ASP.NET Core 支持的任何平台上運行。  例如, Xamarin 開發人員可以使用 SignalR用於構建 Android 應用程序使用 Xamarin.Android 8.4.0.1 或更高版本和 iOS 應用程序使用 Xamarin.iOS 11.14.0.4 或更高版本。如果服務器運行 IIS,Websocket 傳輸要求安裝 IIS 8.0 或更高版本在 Windows Server 2012 或更高版本。  其他傳輸在所有平台上都受支持。
 

3、回落機制

參考文章《SignalR簡介及使用

SignalR使用的三種底層傳輸技術分別是Web Socket, Server Sent Events 和 Long Polling;

其中Web Socket僅支持比較現代的瀏覽器,Web服務器也不能太老;

而Server Sent Events 情況可能好一點, 但是也存在同樣的問題。

 

Web Socket是最好的最有效的傳輸方式, 如果瀏覽器或Web服務器不支持它的話, 就會降級使用SSE, 實在不行就用Long Polling;

 

一旦建立連接, SignalR就會開始發送keep alive消息, 來檢查連接是否還正常, 如果有問題, 就會拋出異常;

因為SignalR是抽象於三種傳輸方式的上層, 所以無論底層采用的哪種方式, SignalR的用法都是一樣的;

 

SignalR默認采用這種回落機制來進行傳輸和連接。但是也可以禁用回落機制, 只采用其中一種傳輸方式。

 

4、Hub組件

Hub是SignalR的一個組件, 它運行在ASP.NET Core應用里. 所以它是服務器端的一個類;

Hub使用RPC接受從客戶端發來的消息, 也能把消息發送給客戶端, 所以它就是一個通信用的Hub;

 

在ASP.NET Core里, 自己創建的Hub類需要繼承於基類Hub;

在Hub類里面, 我們就可以調用所有客戶端上的方法了, 同樣客戶端也可以調用Hub類里的方法;

之前說過方法調用的時候可以傳遞復雜參數, SignalR可以將參數序列化和反序列化, 這些參數被序列化的格式叫做Hub 協議,所以Hub協議就是一種用來序列化和反序列化的格式;

Hub協議的默認協議是JSON, 還支持另外一個協議是MessagePack, MessagePack是二進制格式的, 它比JSON更緊湊, 而且處理起來更簡單快速, 因為它是二進制的;

此外, SignalR也可以擴展使用其它協議。

 

好啦,復雜而又枯燥的概念說完了,接下來咱們開始動手寫代碼了!(額概念還是要看的😊)

 

二、搭建 SignalR 服務中心

既然要實現實時交互,肯定得有服務端,那我們就直接在 Blog.Core 項目上,進行處理吧

1、引用 SignalR 包

為了以后好拓展,我就把 SignalR 中心,也可以是通訊管道定義到了 Common 層,當然可以自定義任意層。

//請看清,還有一個 Net 版本的,但是也能用,還是用 core 版本的吧
Install-Package Microsoft.AspNetCore.SignalR

 

 

2、聲明 Hub 管道——集線器

在 Blog.Core.Common 層,新建一個 Hubs 文件夾,然后添加一個 ChatHub 類:

 public class ChatHub : Hub
 {
     public async Task SendMessage(string user, string message)
     {
         await Clients.All.SendAsync("ReceiveMessage", user, message);
     }

     //定於一個通訊管道,用來管理我們和客戶端的連接
     //1、客戶端調用 GetLatestCount,就像訂閱
     public async Task GetLatestCount(string random)
     {
         //2、服務端主動向客戶端發送數據,名字千萬不能錯
         await Clients.All.SendAsync("ReceiveUpdate", LogLock.GetLogData());
         
         //3、客戶端再通過 ReceiveUpdate ,來接收

     }
 }

基本的概念已經在上邊了,大家結合之前的概念,應該能看懂,看不懂也沒事兒,等看了下邊的 Vue 代碼,就理解了。

旁白:

這里說一下,上文中的GetLogData,這個方法,是直接把我們之前的日志從log文件給提取出來了,包括AOP日志,異常日志,Sql日志,因為格式規則變了,如果你本地已經存在之前的錯誤日志了,請刪了文件重新生成,否則格式不正確會報錯。

 

 

3、配置服務 與 中間件

還是老規則,在netcore 中,基本只要涉及到Http請求相關的,一定要配置中間件,任何需要在宿主中用的服務,都需要注入:

//這個配置就太簡單了,不細說了,大家一看就知道往哪里放
services.AddSignalR();


 app.UseMvc();

//我這個放到了 Mvc 管道下邊,注意順序
 app.UseSignalR(routes =>
 {
     //這里要說下,為啥地址要寫 /api/xxx 
     //因為我前后端分離了,而且使用的是代理模式,所以如果你不用/api/xxx的這個規則的話,會出現跨域問題,畢竟這個不是我的controller的路由,而且自己定義的路由
     routes.MapHub<ChatHub>("/api/chatHub");
 });

 

4、跨域

這一塊大家肯定都已經配置好了,要不然不會前后端分離的,至於用前端 proxy 代理,還是后端 CORS 配置,看自己喜好吧。

我online項目統一使用的都是nginx,全部配置是這樣的:http://apk.neters.club/.doc/guide/function-sheet.html#四、nginx一覽表

 

這個時候,我們的后端通道就打通了,如何驗證呢,我們在路由器直接輸入上邊的自定義路由地址即可:

 

 因為每次請求需要一個 ID 號的,直接訪問肯定不行,那這個 ID 我們怎么來拿呢,別着急,下文會說到,重點來了。

 

三、在 Vue 中配置客戶端連接

 服務端可以了,那就改配置客戶端了,其實客戶端特別簡單,就好像我們使用一個js庫插件一樣,比如大家一定用過地圖api庫 ,直接引用js,然后new map對象即可使用了,沒錯,SignalR和它一毛一樣。

1、安裝庫依賴包

 現在 vue 也和 core 很像了,用一個東西,都需要安裝包,配置服務,再調用這三部曲了。

//直接在項目中執行
npm install @aspnet/signalr

 

我為了更好的讓大家理解這個通訊的過程,每個標題后邊,都破折號了我對這個過程的理解,大家一看就懂了。

 

2、添加引用——買個手機

在Admin 項目里,我增加了一個展示日志的頁面,大家自己看看就都懂了,然后之前是需要每次刷新的,但是這次改造成可以自推送的。

上邊也說到了,這個 SignalR 我們只需要像 map 地圖那樣,引用就行了,很簡單:

 

網上很坑的是,很多教程里竟然要在 main.js 中引用,最后導致還出現了依賴 Jquery 的各種bug,大家如果無聊可以試試。

 

3、開始連接到中心——連上網絡

 直接上代碼:

 created: function () {
     //1、首先我們實例化一個連接器
     this.connection = new signalR.HubConnectionBuilder()
         //然后配置通道路由
         .withUrl('/api/chatHub')
         //日志信息
         .configureLogging(signalR.LogLevel.Information)
         //創建
         .build();
 },

 是不是很簡單,只需要我們在頁面初始化的時候,創建連接即可,只不過這里有一些小問題,

 

 咱們的項目中,已經配置好了跨域,並且所有接口也已經成功調用了,但是唯獨 Hub 卻不行,情況如下:

還記得上邊咱們在 startup.cs 中配置的 hub 路由么,如果我們不使用 /api/chatHub 會是怎么樣呢?這里我也簡單把錯誤的給寫出來,留作參考:

1、相對路徑,沒用代理規則:

withUrl('/axxxxx/chatHub')

 

是因為我們用的相對路徑,而且也沒與代理,系統會認為我們訪問的是一個頁面路由,所以 404;

 

2、絕對路徑,沒用代理規則

withUrl('http://localhost:8081/axxxxx/chatHub')

 

這樣就會出現代理的問題。

當然!如果你使用的是后端 CORS 機制跨域,不會這個問題的,其他的各種情況自己把握就好。

是不是我們這么配置好了就沒事了呢,別着急,還有要給bug,

 

3、服務器Nginx代理

如果你在服務器里用的是 Nginx 做代理的話,可能會遇到這個問題:

大家可以看看,這個錯誤,和上邊兩個都不一樣,是已經連上了,但是去不能開啟數據 的交互 transport !神奇,簡單的看了看開源的 websockets 上提的 issues,是這么解決的《wss: Error during WebSocket handshake: Unexpected response code: 200 #979

 

好啦!這次應該沒啥問題了,繼續往下走。

 

4、客戶端調用集線器——建立連接,呼叫對方

那我們現在已經連接成功了,剩下的就是調用集線器了,也就是上邊我們定義的 Hub 通道,用來接收日志:

 // 開始通訊,並成功呼叫服務器
 thisvue.connection.start().then(() => {
     thisvue.connection.invoke('GetLatestCount', 1).catch(function (err) {
         return console.error(err);
     });
 });

 

 

5、從集線器調用客戶端方法——接收回應

 上邊我們是從客戶端去訂閱了一個 通道 連接,也就是說,我需要這個約定,那約定成功后,就需要接收來自服務器的通訊返回結果了,

mounted() {
    thisVue.connection.on('ReceiveUpdate', function (update) { console.info('update success!') thisVue.tableData = update;//將返回的數據,實時的賦值給當前頁面的 data 中; }) },

這個時候我們刷新頁面,已經能看到消息了,然后我們在看看接口請求:

是不是很熟悉!沒錯,這個就是我們上邊說到的那個 ID ,不記得的往上看,這個是自動生成的,而且不隨着消息推送而變化,只有每次請求重新連接的時候,才會變化。 

好啦,這樣我們就成功了在頁面上展示出了我們的數據,BUT!別慌,好像還沒有完成,因為我們現在僅僅是展示了出來,還沒有實現推送啊!別着急,既然一次能顯示,那多次也能顯示。

 

6、每次更新日志,推送到客戶端——實時短信

這個就很簡單了,我們只需要在每次日志產生的時候,來推送出來即可,舉個全局異常的栗子吧:

先注入我們的通道上下文:

 private readonly IHubContext<ChatHub> _hubContext;

 public GlobalExceptionsFilter(IHostingEnvironment env, ILoggerHelper loggerHelper,
IHubContext<ChatHub> hubContext)
 {
     _env = env;
     _loggerHelper = loggerHelper;
     _hubContext = hubContext;
 }

然后直接使用:

//采用log4net 進行錯誤日志記錄
_loggerHelper.Error(json.Message, WriteLog(json.Message, context.Exception));

_hubContext.Clients.All.SendAsync("ReceiveUpdate", LogLock.GetLogData()).Wait();

 

這樣,每次我們操作的時候,就會觸發生成日志的功能,同時再觸發推送功能,就這樣,我們把消息及時的推送了出去,達到了目的,實現了文章開頭的功能。

如果想中斷連接,只需要頁面關閉的時候,執行 connection.stop() 即可。

 

7、對不同對象進行推送

話不多說,直接看代碼即可(群友代碼,有疑問問群里):

 /// <summary>
 /// 全員推送
 /// </summary>
 /// <returns></returns>
 [HttpPost]
 public async Task<IActionResult> PushMessageAsync([FromBody]object data)
 {
     await _hubContext.Clients.All.SendAsync(MessageDefault.ReceiveMessage, data);
     return Ok();
 }

 /// <summary>
 /// 對某人推送
 /// </summary>
 /// <returns></returns>
 [HttpPost]
 public async Task<IActionResult> PushAnyOneAsync([FromBody]MessagePushDTO model)
 {
     if (model == null)
     {
         return Forbid();
     }
     var user = SignalRMessageGroups.UserGroups.FirstOrDefault(m => m.UserId == model.UserId && m.GroupName == model.GroupName);
     if (user != null)
     {
         await _hubContext.Clients.Client(user.ConnectionId).SendAsync(MessageDefault.ReceiveAnyOne, model.MsgJson);
     }
     return Ok();
 }

 /// <summary>
 /// 對某組進行推送
 /// </summary>
 /// <returns></returns>
 [HttpPost]
 public async Task<IActionResult> PushGroupAsync([FromBody]MessageGroupPushDTO model)
 {
     if (model == null)
     {
         return Forbid();
     }
     var list = SignalRMessageGroups.UserGroups.Where(m => m.GroupName == model.GroupName);
     foreach (var item in list)
     {
         await _hubContext.Clients.Client(item.ConnectionId).SendAsync(model.GroupName, model.MsgJson);
     }
     return Ok();
 }

 

8、SignalR 斷點續連

當我們使用 SignalR 的時候,或多或少的都會遇到過斷線了,需要重連,一般我們是通過 try catch 的方法,但是這樣不能,因為這個不是異常,所以 catch 無法捕捉到,官方給我們已經提供了一個方法了:

官網地址:https://docs.microsoft.com/zh-cn/aspnet/core/signalr/javascript-client?view=aspnetcore-3.1

 

 

 

 

 

四、模擬登錄

 在文章開頭,我說了幾個場景,其他的不好實現,先來個模擬登錄吧,就是把用戶名密碼傳到后台,然后后台將結果推送回來。

具體的流程就不說了,和上邊的是一樣的,只是很簡單的一個動作,接收下數據即可,

 

 

五、結語

今天很簡單的實現了兩個小功能,一個是模擬登錄,一個是實時推送消息,大家學會了么,這里有幾個問題,大家可以思考思考:

1、SignalR到底能在平時開發中,使用哪些地方?

2、服務中心是如何將消息發出去的?

3、客戶端是如何來訂閱某一個通道集線器的?

4、SignalR的底層原理是什么?

5、如何關閉連接?

 

六、Github && Gitee

 

NetCore https://github.com/anjoy8/Blog.Core

       Vue https://github.com/anjoy8/Blog.Admin

 

 ------♥------♥------♥----------


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM