基於SqlSugar的開發框架循序漸進介紹(25)-- 基於SignalR實現多端的訊息通訊

2023-04-07 21:00:26

基於ASP.NET Core SignalR 可以實現使用者端和伺服器之間進行即時通訊。本篇隨筆介紹一些SignalR的基礎知識,以及結合對SqlSugar的開發框架的支援,實現SignalR的多端處理整合,從而實現Winform使用者端,基於Vue3+ElementPlus的BS端整合,後面也可以實現對行動端的SignalR的整合通訊。

適合 SignalR 的應用場景:

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

SignalR 自動選擇伺服器和使用者端能力範圍內的最佳傳輸方法,如WebSockets、Server-Sent Events、長輪詢。Hub 是一種高階管道,允許使用者端和伺服器相互呼叫方法。 SignalR 自動處理跨計算機邊界的排程,並允許使用者端呼叫伺服器上的方法,反之亦然。SignalR 提供兩個內建中心協定:基於 JSON 的文字協定和基於 MessagePack 的二進位制協定。

使用者端負責通過 HubConnection 物件建立到伺服器終結點的連線。 Hub 連線在每個目標平臺中表示:

當中心連線範例成功啟動後,訊息可以自由地雙向流動。 使用者可以自由地將通知傳送到伺服器,以及從伺服器接收通知。 使用者端是任何已連線的應用程式,例如(但不限於)Web 瀏覽器、移動應用或桌面應用。

1、SignalR伺服器端

在.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;
    }
}

.NET 使用者端

使用者端在其 .On(...) 處理程式中返回結果,如下所示:

hubConnection.On("GetMessage", async () =>
{
    Console.WriteLine("Enter message:");
    var message = await Console.In.ReadLineAsync();
    return message;
});

Typescript 使用者端

hubConnection.on("GetMessage", async () => {
    let promise = new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("message");
        }, 100);
    });
    return promise;
});

Java 使用者端

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 屬性,該屬性包含以下用於伺服器和使用者端之間通訊的屬性:

屬性說明
All 對所有連線的使用者端呼叫方法
Caller 對呼叫了中心方法的使用者端呼叫方法
Others 對所有連線的使用者端呼叫方法(呼叫了方法的使用者端除外)

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;
        }

 

2、SignalR使用者端

.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訊息整合的處理操作。