緣起
哈嘍大家周一好呀,感覺好久沒有寫文章了,上周出差了一次,感覺還是比坐辦公室好的多,平時在讀一本書《時生》,感興趣的可以看看😂......
這幾天翻看 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或更高版本。
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
------♥------♥------♥----------