本文就簡單使用 往前端頁面推送消息
SignalR 是什么
SignalR是一個.NET Core/.NET Framework的開源實時框架. SignalR的可使用Web Socket, Server Sent Events 和 Long Polling作為底層傳輸方式.
SignalR基於這三種技術構建, 抽象於它們之上, 它讓你更好的關注業務問題而不是底層傳輸技術問題.
SignalR這個框架分服務器端和客戶端, 服務器端支持ASP.NET Core 和 ASP.NET; 而客戶端除了支持瀏覽器里的javascript以外, 也支持其它類型的客戶端, 例如桌面應用.
對於.NET開發者的福音,.NET平台為我們提供了一種簡潔高效智能的實時信息交互技術->SignalR,它集成了上述數種技術,並能根據配置自動或手動選擇其最佳應用
可以用SignalR做什么?
-
SignalR可用於將任何類型的"實時"web 功能添加到 ASP.NET 應用程序。 比如最常用的即時消息、聊天。 只要用戶刷新 web 頁面以查看新數據或頁面實現長輪詢若要檢索新數據,可以考慮對它使用 SignalR。 包括儀表板和監視應用程序,協作應用程序 (如同時進行編輯的文檔),作業的進度更新到並實時窗體。
-
SignalR還可以用於需要高頻率從服務器中更新的全新類型weB應用程序,例如在線聊天、實時游戲、天氣、股票信息更新等實時應用程序。
-
SignalR 提供一個簡單的 API,用於創建從服務器端.NET 代碼中調用 JavaScript 函數在客戶端瀏覽器 (和其他客戶端平台) 的服務器到客戶端的遠程過程調用 (RPC)。 SignalR 還包括連接管理的 API (例如,連接和斷開連接事件),並對連接進行分組。
回落機制
SignalR使用的三種底層傳輸技術分別是Web Socket, Server Sent Events 和 Long Polling.
其中Web Socket僅支持比較現代的瀏覽器, Web服務器也不能太老.
而Server Sent Events 情況可能好一點, 但是也存在同樣的問題.
所以SignalR采用了回落機制, SignalR有能力去協商支持的傳輸類型.
Web Socket是最好的最有效的傳輸方式, 如果瀏覽器或Web服務器不支持它的話, 就會降級使用SSE, 實在不行就用Long Polling.
一旦建立連接, SignalR就會開始發送keep alive消息, 來檢查連接是否還正常. 如果有問題, 就會拋出異常.
因為SignalR是抽象於三種傳輸方式的上層, 所以無論底層采用的哪種方式, SignalR的用法都是一樣的.
SignalR默認采用這種回落機制來進行傳輸和連接.
但是也可以禁用回落機制, 只采用其中一種傳輸方式.
RPC
RPC (Remote Procedure Call). 它的優點就是可以像調用本地方法一樣調用遠程服務.
SignalR采用RPC范式來進行客戶端與服務器端之間的通信.
SignalR利用底層傳輸來讓服務器可以調用客戶端的方法, 反之亦然, 這些方法可以帶參數, 參數也可以是復雜對象, SignalR負責序列化和反序列化.
Hub
Hub是SignalR的一個組件, 它運行在ASP.NET Core應用里. 所以它是服務器端的一個類.
Hub使用RPC接受從客戶端發來的消息, 也能把消息發送給客戶端. 所以它就是一個通信用的Hub.
在ASP.NET Core里, 自己創建的Hub類需要繼承於基類Hub.
在Hub類里面, 我們就可以調用所有客戶端上的方法了. 同樣客戶端也可以調用Hub類里的方法.
這種Hub+RPC的方式還是非常適合實時場景的.
之前說過方法調用的時候可以傳遞復雜參數, SignalR可以將參數序列化和反序列化. 這些參數被序列化的格式叫做Hub 協議, 所以Hub協議就是一種用來序列化和反序列化的格式.
Hub協議的默認協議是JSON, 還支持另外一個協議是MessagePack. MessagePack是二進制格式的, 它比JSON更緊湊, 而且處理起來更簡單快速, 因為它是二進制的.
此外, SignalR也可以擴展使用其它協議..
橫向擴展
隨着系統的運行, 有時您可能需要進行橫向擴展. 就是應用運行在多個服務器上.
這時負載均衡器會保證每個進來的請求按照一定的邏輯分配到可能是不同的服務器上.
在使用Web Socket的時候, 沒什么問題, 因為一旦Web Socket的連接建立, 就像在瀏覽器和那個服務器之間打開了隧道一樣, 服務器是不會切換的.
但是如果使用Long Polling, 就可能有問題了, 因為使用Long Polling的情況下, 每次發送消息都是不同的請求, 而每次請求可能會到達不同的服務器. 不同的服務器可能不知道前一個服務器通信的內容, 這就會造成問題.
針對這個問題, 我們需要使用Sticky Sessions (粘性會話).
Sticky Sessions 貌似有很多中實現方式, 但是主要是下面要介紹的這種方式.
作為第一次請求的響應的一部分, 負載均衡器會在瀏覽器里面設置一個Cookie, 來表示使用過這個服務器. 在后續的請求里, 負載均衡器讀取Cookie, 然后把請求分配給同一個服務器.
在ASP.NET Core 中使用SignalR
建立一個ServerHub, 繼承於Hub:
public class ServerHub : Hub { /// <summary> /// 已連接的用戶信息 /// </summary> public static List<UserModel> OnlineUser { get; set; } = new List<UserModel>(); private readonly ILogger<ServerHub> _logger; private ISysUser _userService = null; private readonly IHttpContextAccessor _accessor; public ServerHub(ISysUser user, ILogger<ServerHub> logger, IHttpContextAccessor accessor) { _userService = user; _logger = logger; _accessor = accessor; } /// <summary> /// 當連接成功時執行 /// </summary> /// <returns></returns> public override Task OnConnectedAsync() { string connId = Context.ConnectionId; _logger.LogWarning("SignalR已連接"); //驗證Token var token= _accessor.HttpContext.Request.Query["access_token"]; var user = JwtHelper.SerializeJwt(token); _logger.LogWarning("SignalR已連接,用戶名:" + user.UserName); //連接用戶 這里可以存在Redis var model= new UserModel { ConnectionId = connId, Token = token, UserName = user.UserName }; OnlineUser.Add(model); //給當前的連接分組 可以進行同一組接收消息 也可以用Token獲取機構權限 //await Groups.AddToGroupAsync(Context.ConnectionId, "測試組"); //給當前連接返回消息 .Clients可以發多個連接ID Clients.Client(connId).SendAsync("ConnectResponse", new ApiResult<UserModel>() { state=200, data = model, msg= user.UserName+"連接成功" }); return base.OnConnectedAsync(); } /// <summary> /// 當連接斷開時的處理 /// </summary> public override Task OnDisconnectedAsync(Exception exception) { string connId = Context.ConnectionId; var model = OnlineUser.Find(u => u.ConnectionId == connId); int count = OnlineUser.RemoveAll(u => u.ConnectionId == connId); if (model != null) { //給當前分組發送消息 在一個分組就能接收到消息 //Clients.Group(model.GroupName).SendAsync("GetUsersResponse", result); //給當前連接返回消息 .Clients可以發多個連接ID Clients.Client(connId).SendAsync("DisconnectResponse", new ApiResult<bool>() { state = 1000, data = true, msg = "斷開連接" }); } return base.OnDisconnectedAsync(exception); } /// <summary> /// 接受用戶的數進行推送 /// </summary> /// <returns></returns> public async Task SendMessage(string user,string msg) { ApiResult<UserModel> result = new ApiResult<UserModel>(); result.data = new UserModel { ConnectionId = Context.ConnectionId, Token = "", UserName = user }; result.state = 200; result.msg = msg; //推送給所有連接ID的第一條數據 await Clients.Clients(OnlineUser.Select(q=>q.ConnectionId).ToList()).SendAsync("SendMessage", result); } }
在Startup里注冊SignalR:
注意 如果報跨域錯誤
endpoints.MapHub<ServerHub>("/serverHub").RequireCors(t => t.WithOrigins(new string[] { "http://localhost:8080" }).AllowAnyMethod().AllowAnyHeader().AllowCredentials());
我這里定時像前台推送數據 實際項目可以用MQ
/// <summary> /// 利用Quartz定時推送假數據 一般是接受MQ推送的消息 或者從數據庫 resis /// </summary> [DisallowConcurrentExecution] public class SignalRJob : IJob { private readonly ILogger<SignalRJob> _logger; private readonly IHubContext<ServerHub> _hubContext; public SignalRJob(ILogger<SignalRJob> logger, IHubContext<ServerHub> hubContext) { _logger = logger; _hubContext = hubContext; } public async Task Execute(IJobExecutionContext context) { ApiResult<bool> result = new ApiResult<bool>(); if (ServerHub.OnlineUser.Count == 0) { result.data = false; result.state = 1000; result.msg = "沒有連接用戶"; } else { result.data = true; result.state = 200; result.msg = "推送第一條數據"; } //推送給所有連接ID的第一條數據 await _hubContext.Clients.Clients(ServerHub.OnlineUser.Select(q => q.ConnectionId).ToList()).SendAsync("SendMessageResponse", result); } }
前端:
JS端
"use strict"; var connection = new signalR.HubConnectionBuilder() .withUrl("https://localhost:44317/chatHub") .withAutomaticReconnect() //斷線自動重連 .build(); connection.start(); //自動重連成功后的處理 connection.onreconnected(connectionId => { alert(connectionId); }); //---消息--- document.getElementById("sendButton").addEventListener("click", function (event) { var user = document.getElementById("userInput").value; var message = document.getElementById("messageInput").value; connection.invoke("SendMessage", user, message).catch(function (err) { return console.error(err.toString()); }); event.preventDefault(); }); connection.on("SendMessageResponse", function (res) { if (res && res.status == 0) { var li = document.createElement("li"); li.textContent = res.message; document.getElementById("messagesList").appendChild(li); } else { alert(res.message); } }); //---消息--- //---登錄--- document.getElementById("btnLogin").addEventListener("click", function (event) { var user = document.getElementById("userInput").value; var message = document.getElementById("messageInput").value; connection.invoke("Login", user, message).catch(function (err) { return console.error(err.toString()); }); event.preventDefault(); }); connection.on("LoginResponse", function (res) { if (res && res.status == 0) { sessionStorage.setItem('curuser', res.data); alert(res.message); getUsers(); } else { alert('登錄失敗!'); } }); //---登錄--- //獲取在線用戶 function getUsers() { connection.invoke("GetUsers").catch(function (err) { return console.error(err.toString()); }); connection.on("GetUsersResponse", function (res) { if (res && res.status == 0) { var _lis = '<li>在線用戶:</li>'; for (var i = 0; i < res.onlineUser.length; i++) { _lis += `<li>${res.onlineUser[i].userName}</li>`; } document.getElementById("usersList").innerHTML = _lis; } }); }
VUE端
首先安裝依賴包
npm install @microsoft/signalr
然后新建一個vue頁面
<template> <div class="hello"> <div id="message" v-html="remsg"></div> <input type="text" placeholder="請輸入用戶名" v-model="user" /> <input type="text" placeholder="請輸入內容" v-model="msg"> <button @click="handle">發送消息</button> </div> </template> <script> import * as signalR from "@microsoft/signalr"; let token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VyTmFtZSI6ImFkbWluIiwiSUQiOiIyIiwiZXhwIjoxNTk5NjM3NjIxLCJpc3MiOiJuZXRsb2NrIiwiYXVkIjoibmV0bG9ja3MifQ.9T1zw2LaCx4enZLj5RCfxhJ85a169NPMqmW0n5OlzgI"; let hubUrl = "http://localhost:3906/serverHub"; //.net core 版本中默認不會自動重連,需手動調用 withAutomaticReconnect const connection = new signalR.HubConnectionBuilder() .withAutomaticReconnect()//斷線自動重連 .withUrl(hubUrl,{ accessTokenFactory: () => token })//傳遞參數Query["access_token"] .build(); //啟動 connection.start().catch(err => { console.log(err); }); //自動重連成功后的處理 connection.onreconnected(connectionId => { console.log(connectionId); }); export default { name: "First", mounted() { var _this = this; //調用后端方法 SendMessageResponse 接收定時數據 connection.on("SendMessageResponse", function(data) { if(data.state==200) _this.remsg = _this.remsg + "<br>" + "定時數據:" + data.msg; }); //調用后端方法 SendMessage 接受自己人發送消息 connection.on("SendMessage", function(data) { if(data.state==200) _this.remsg = _this.remsg + "<br>" + data.data.userName + ":" + data.msg; }); //調用后端方法 ConnectResponse 接收連接成功 connection.on("ConnectResponse", function(data) { if(data.state==200) _this.remsg = _this.remsg + "<br>" + "連接:" + data.msg; }); }, data() { return { user: "", msg: "", remsg: "" }; }, methods: { handle: function() { if(this.msg.trim()==""){ alert("不能發送空白消息"); return; } //調用后端方法 SendMessage 傳入參數 connection.invoke("SendMessage", this.user, this.msg); this.msg = ""; } } }; </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> #message { overflow-y:auto; text-align: left; border: #42b983 solid 1px; height: 500px; } </style>
配置路由
最后的效果圖
參考地址 https://www.cnblogs.com/shousiji/p/12737925.html
我的代碼 https://files.cnblogs.com/files/netlock/SignalRDemo.rar