基於ASP.NET Core SignalR 可以實現使用者端和伺服器之間進行即時通訊。本篇隨筆介紹一些SignalR的基礎知識,以及結合對SqlSugar的開發框架的支援,實現SignalR的多端處理整合,從而實現Winform使用者端,基於Vue3+ElementPlus的BS端整合,後面也可以實現對行動端的SignalR的整合通訊。
適合 SignalR 的應用場景:
SignalR 自動選擇伺服器和使用者端能力範圍內的最佳傳輸方法,如WebSockets、Server-Sent Events、長輪詢。Hub 是一種高階管道,允許使用者端和伺服器相互呼叫方法。 SignalR 自動處理跨計算機邊界的排程,並允許使用者端呼叫伺服器上的方法,反之亦然。SignalR 提供兩個內建中心協定:基於 JSON 的文字協定和基於 MessagePack 的二進位制協定。
使用者端負責通過 HubConnection
物件建立到伺服器終結點的連線。 Hub 連線在每個目標平臺中表示:
當中心連線範例成功啟動後,訊息可以自由地雙向流動。 使用者可以自由地將通知傳送到伺服器,以及從伺服器接收通知。 使用者端是任何已連線的應用程式,例如(但不限於)Web 瀏覽器、移動應用或桌面應用。
在.net core的Web API上,我們首先需要註冊SignalR的服務,然後建立對應的Hub進行使用。一般可以在啟動類中新增如下程式碼即可。
builder.Services.AddSignalR();// 即時通訊 app.UseEndpoints(endpoints => { // 註冊集線器 endpoints.MapHub<OnlineUserHub>("/hubs/onlineUser"); });
定義集線器只需要繼承 Hub
或 Hub<TStrongType>
泛型基礎類別即可。
public class ChatHub : Hub { public async Task SendMessage(string user, string message) => await Clients.All.SendAsync("ReceiveMessage", user, message); }
泛型強型別方法是使用 Hub<T>的強型別Hub類。在以下範例中 ChatHub
,使用者端方法已提取到名為 的 IChatClient
介面中:
public interface IChatClient { Task ReceiveMessage(string user, string message); }
此介面可用於將前面的 ChatHub
範例重構為強型別:
public class ChatHub : Hub<IChatClient> { public async Task SendMessage(string user, string message) => await Clients.All.ReceiveMessage(user, message); public async Task SendMessageToCaller(string user, string message) => await Clients.Caller.ReceiveMessage(user, message); public async Task SendMessageToGroup(string user, string message) => await Clients.Group("SignalR Users").ReceiveMessage(user, message); }
這樣Clients的物件都具備了介面定義的 ReceiveMessage方法呼叫,實際這個就是使用者端的方法。
使用 Hub<IChatClient>
可以對使用者端方法進行編譯時檢查。 這可以防止使用字串引起的問題,因為 Hub<T>
只能提供對 介面中定義的方法的存取許可權。 使用強型別 Hub<T>
會禁止使用 SendAsync
。
Hub伺服器端中心
public interface IClient { Task<string> GetMessage(); } public class ChatHub : Hub<IClient> { public async Task<string> WaitForMessage(string connectionId) { string message = await Clients.Client(connectionId).GetMessage(); return message; } }
使用者端在其 .On(...)
處理程式中返回結果,如下所示:
hubConnection.On("GetMessage", async () => { Console.WriteLine("Enter message:"); var message = await Console.In.ReadLineAsync(); return message; });
hubConnection.on("GetMessage", async () => { let promise = new Promise((resolve, reject) => { setTimeout(() => { resolve("message"); }, 100); }); return promise; });
hubConnection.onWithResult("GetMessage", () -> { return Single.just("message"); });
在框架中整合SignalR的Hub的時候,我們定義一個介面IOnlineUserHub,以便強型別對使用者端介面方法的呼叫,減少錯誤。
然後在定義一個Hub的物件類,如下所示 。
public class OnlineUserHub : Hub<IOnlineUserHub> { private readonly IOnlineUserService _onlineUserService; private readonly IHubContext<OnlineUserHub, OnlineUserHub> _chatHubContext; public OnlineUserHub(IOnlineUserService onlineUserService, IHubContext<OnlineUserHub, IOnlineUserHub> onlineUserHubContext) { _onlineUserService = onlineUserService; _chatHubContext = onlineUserHubContext; } }
物件Hub<T>本身可以通過注入一個 IHubContext<OnlineUserHub, OnlineUserHub> 介面來獲得對它的呼叫,如上面建構函式所示。該Hub一般還需要重寫連線和斷開的處理操作,如下程式碼所示。
如對於使用者的SignalR連線發起,我們需要判斷使用者的令牌及相關身份資訊,如果成功,則通過給使用者端提供線上使用者列表。
/// <summary> /// 連線後處理 /// </summary> /// <returns></returns> public override async Task OnConnectedAsync() { var httpContext = Context.GetHttpContext(); var token = httpContext!.Request.Query["access_token"]; if (string.IsNullOrWhiteSpace(token)) return; ................ //向用戶端提供線上使用者資訊 await _chatHubContext.Clients.Groups(groupName).OnlineUserList(new OnlineUserList { ConnectionId = user.ConnectionId, RealName = user.RealName + $"({client.UA.Family})", //加上實際終端 Online = true, UserList = userList.Items.ToList() }); //更新線上使用者快取 await RedisHelper.SetAsync(CacheConst.KeyOnlineUser, userList.Items.ToList()); }
類 Hub 包含一個 Context 屬性,該屬性包含以下屬性以及有關連線的資訊:
屬性 | 說明 |
---|---|
ConnectionId | 獲取連線的唯一 ID(由 SignalR 分配)。 每個連線都有一個連線 ID。 |
UserIdentifier | 獲取使用者識別符號。 預設情況下,SignalR 使用與連線關聯的 ClaimsPrincipal 中的 ClaimTypes.NameIdentifier 作為使用者識別符號。 |
User | 獲取與當前使用者關聯的 ClaimsPrincipal。 |
Items | 獲取可用於在此連線範圍內共用資料的鍵/值集合。 資料可以儲存在此集合中,會在不同的中心方法呼叫間為連線持久儲存。 |
Features | 獲取連線上可用的功能的集合。 目前,在大多數情況下不需要此集合,因此未對其進行詳細記錄。 |
ConnectionAborted | 獲取一個 CancellationToken,它會在連線中止時發出通知。 |
Hub.Context 還包含以下方法:
方法 | 說明 |
---|---|
GetHttpContext | 返回 HttpContext 連線的 ;如果連線未與 HTTP 請求關聯, null 則返回 。 對於 HTTP 連線,請使用此方法獲取 HTTP 檔頭和查詢字串等資訊。 |
Abort | 中止連線。 |
類 Hub 包含一個 Clients 屬性,該屬性包含以下用於伺服器和使用者端之間通訊的屬性:
Hub.Clients 還包含以下方法:
方法 | 說明 |
---|---|
AllExcept | 對所有連線的使用者端呼叫方法(指定連線除外) |
Client | 對連線的一個特定使用者端呼叫方法 |
Clients | 對連線的多個特定使用者端呼叫方法 |
Group | 對指定組中的所有連線呼叫方法 |
GroupExcept | 對指定組中的所有連線呼叫方法(指定連線除外) |
Groups | 對多個連線組呼叫方法 |
OthersInGroup | 對一個連線組呼叫方法(不包括呼叫了中心方法的使用者端) |
User | 對與一個特定使用者關聯的所有連線呼叫方法 |
Users | 對與多個指定使用者關聯的所有連線呼叫方法 |
這樣我們Hub裡面定義的方法,就可以利用這些物件來處理了。
/// <summary> /// 前端呼叫傳送方法(傳送資訊給所有人) /// </summary> /// <param name="message"></param> /// <returns></returns> public async Task ClientsSendMessagetoAll(MessageInput message) { await _chatHubContext.Clients.All.ReceiveMessage(message); } /// <summary> /// 前端呼叫傳送方法(傳送訊息給除了傳送人的其他人) /// </summary> /// <param name="message"></param> /// <returns></returns> public async Task ClientsSendMessagetoOther(MessageInput message) { var onlineuserlist = RedisHelper.Get<List<OnlineUserInfo>>(CacheConst.KeyOnlineUser); var user = onlineuserlist.Where(x => x.UserId == message.UserId).ToList(); if (user != null) { await _chatHubContext.Clients.AllExcept(user[0].ConnectionId).ReceiveMessage(message); } }
基於IHubContext的介面,我們也可以定義一個常規的介面函數,用於在各個服務類中呼叫Hub處理常式
/// <summary> /// 封裝的SignalR的常規處理實現 /// </summary> public class HubContextService : BaseService, IHubContextService
這樣在伺服器端,註冊服務後,可以使用這個自定義服務類的處理邏輯。
//使用HubContextService服務介面 builder.Services.AddSingleton<IHubContextService, HubContextService>();
可以供一些特殊的控制器來使用Hub服務介面,如登入後臺的時候,實現強制多端下線的處理方式。
/// <summary> /// 登入獲取令牌授權的處理 /// </summary> [Route("api/[controller]")] [ApiController] public class LoginController : ControllerBase { private readonly IHubContextService _hubContextService;
/// <summary> /// 登入授權處理 /// </summary> /// <returns></returns> [AllowAnonymous] [HttpPost] [Route("authenticate")] public async Task<AuthenticateResultDto> Authenticate(LoginDto dto) { var authResult = new AuthenticateResultDto(); ................ var loginResult = await this._userService.VerifyUser(dto.LoginName, dto.Password, ip); if (loginResult != null && loginResult.UserInfo != null) { var userInfo = loginResult.UserInfo; ............... //單使用者登入 await this._hubContextService.SignleLogin(userInfo.Id.ToString()); } else { authResult.Error = loginResult?.ErrorMessage; } return authResult; }
.net使用者端在對接Hub中心伺服器端的時候,需要新增Microsoft.AspNetCore.SignalR.Client的參照。
Install-Package Microsoft.AspNetCore.SignalR.Client
若要建立連線,請建立 HubConnectionBuilder
並呼叫 Build
。 在建立連線期間,可以設定中心 URL、協定、傳輸型別、紀錄檔級別、檔頭和其他選項。 可通過將任何 HubConnectionBuilder
方法插入 Build
中來設定任何必需選項。 使用 StartAsync
啟動連線。
using System; using System.Threading.Tasks; using System.Windows; using Microsoft.AspNetCore.SignalR.Client; namespace SignalRChatClient { public partial class MainWindow : Window { HubConnection connection; public MainWindow() { InitializeComponent(); connection = new HubConnectionBuilder() .WithUrl("http://localhost:53353/ChatHub") .Build(); connection.Closed += async (error) => { await Task.Delay(new Random().Next(0,5) * 1000); await connection.StartAsync(); }; } private async void connectButton_Click(object sender, RoutedEventArgs e) { connection.On<string, string>("ReceiveMessage", (user, message) => { this.Dispatcher.Invoke(() => { var newMessage = $"{user}: {message}"; messagesList.Items.Add(newMessage); }); }); try { await connection.StartAsync(); messagesList.Items.Add("Connection started"); connectButton.IsEnabled = false; sendButton.IsEnabled = true; } catch (Exception ex) { messagesList.Items.Add(ex.Message); } } private async void sendButton_Click(object sender, RoutedEventArgs e) { try { await connection.InvokeAsync("SendMessage", userTextBox.Text, messageTextBox.Text); } catch (Exception ex) { messagesList.Items.Add(ex.Message); } } } }
可以將 HubConnection 設定為對 HubConnectionBuilder 使用 WithAutomaticReconnect
方法來自動重新連線。 預設情況下,它不會自動重新連線。
HubConnection connection= new HubConnectionBuilder() .WithUrl(new Uri("http://127.0.0.1:5000/chathub")) .WithAutomaticReconnect() .Build();
在沒有任何引數的情況下,WithAutomaticReconnect()
將使用者端設定為在每次嘗試重新連線之前分別等待 0、2、10 和 30 秒,在四次嘗試失敗後停止。
為了測試Winform使用者端對伺服器端的連線,我們可以新建一個小案例Demo,來測試資訊處理的效果。
建立一個測試的表單如下所示(實際測試效果)。
建立連線Hub中心的程式碼如下所示。
/// <summary> /// 初始化服務連線 /// </summary> private async Task InitHub() { ........ //建立連線物件,並實現相關事件 var url = serverUrl + $"/hubs/onlineUser?access_token={authenticateResultDto.AccessToken}"; hubConnection = new HubConnectionBuilder() .WithUrl(url) .WithAutomaticReconnect(new[] { TimeSpan.Zero, TimeSpan.Zero, TimeSpan.FromSeconds(10) }) //自動連線 .Build(); //接收實時資訊 hubConnection.On<MessageInput>("ReceiveMessage", ReceiveMessage); //連線上處理線上使用者 hubConnection.On<OnlineUserList>("OnlineUserList", OnlineUserList); //使用者端收到服務關閉訊息 hubConnection.On("ForceOffline", async (ForceOfflineInput data) => { await CloseHub(); }); try { //開始連線 await hubConnection.StartAsync(); var content = $"連線到伺服器:{serverUrl}"; AddSystemMessage(content); } catch (Exception ex) { Console.WriteLine(ex.StackTrace); var content = $"伺服器連線失敗:{ex.Message}"; AddSystemMessage(content); InitControlStatus(false); return; } }
我們可以看到,使用者端接收伺服器端的訊息處理,通過下面程式碼進行處理。
//接收實時資訊 hubConnection.On<MessageInput>("ReceiveMessage", ReceiveMessage); //連線上處理線上使用者 hubConnection.On<OnlineUserList>("OnlineUserList", OnlineUserList); //使用者端收到服務關閉訊息 hubConnection.On("ForceOffline", async (ForceOfflineInput data) =>
對於訊息的接收處理,我們把它收到一個原生的集合列表中,然後統一處理即可。
/// <summary> /// 訊息處理 /// </summary> /// <param name="data">JSON字串</param> private void ReceiveMessage(MessageInput data) { if (this.onlineUser != null) { var info = new MessageInfo(data); ............. TryAddMessage(ownerId, info); BindTree(); } }
傳送訊息的時候,我們根據指向不同的使用者,構造對應的訊息體傳送(呼叫伺服器端Hub介面)即可,呼叫通過InvokeAsync處理,接收相應的物件。
private async void BtnSendMessage_Click(object sender, EventArgs e) { if (txtMessage.Text.Length == 0) return; var message = new MessageInput() { Title = "訊息", Message = txtMessage.Text, MessageType = MessageTypeEnum.Info, UserId = this.toId, UserIds = new List<string>() }; //判斷傳送人,是單個傳送,還是廣播傳送所有人 var methodName = !string.IsNullOrEmpty(this.toId) ? "ClientsSendMessage" : "ClientsSendMessagetoAll"; await hubConnection.InvokeAsync(methodName, message); }
測試功能正常,我們就可以把表單整合到Winform端的主體介面中了。
在Winform端的登陸處理的時候,我們把SignarR的主要處理邏輯放在全域性類GlobalControl 中,方便呼叫,並定義好幾個常用的物件,如連線,線上使用者資訊,訊息列表等。
並通過定義事件的方式,在訊息變化的時候,通知介面進行更新處理。
public event EventHandler<MessageInfo> SignalRMessageChanged;
因此我們可以在主介面上提供一個入口,供訊息的處理操作。
主表單在介面初始化的時候,呼叫一下全域性類的初始化SignalR的Hub連線即可。
/// <summary> /// 初始化SignalR的處理 /// </summary> private async void InitSignalR() { await Portal.gc.InitHub(); }
這樣就會根據相應的資訊,實現HubConnection的初始化操作了,而且這個連線的生命週期是伴隨整個應用的出現而出現的。
開啟就可以展示線上使用者,並可以和系統相關使用者傳送實時訊息了。如果可以,我們也可以把訊息儲存在資料庫端,然後離線也可以收到儲存起來,供下次登入後進行檢視。
表單可以對SignalR訊息進行實時的更新相應,通過事件的實現。
public partial class FrmSignalClient : BaseDock { public FrmSignalClient() { InitializeComponent(); Portal.gc.SignalRMessageChanged += SignalRMessageChanged; }
由於篇幅的原因,後面在介紹在Vue3+Element的BS端中實現對SignalR訊息整合的處理操作。