1. 程式人生 > 實用技巧 >.Net Core3.1 SignalR for WPF Asp.net

.Net Core3.1 SignalR for WPF Asp.net

一、概要

這篇文章將向大家分享最近學習的一種實時通訊框架SignalR。

什麼是SignalR?

  • SignalR是一個.NET Core/.NET Framework的開源實時框架,可使用Long Polling,ServerSent Events和Websocket作為底層傳輸方式。
  • SignalR基於這三種技術構建,抽象於它們之上,它讓你更好的關注業務問題而不是底層傳輸技術問題。
  • SignalR這個框架分伺服器和客戶端,伺服器端支援ASP.NET Core和ASP.NET;而客戶端除了支援瀏覽器的javascript以外,也支援其他型別的客戶端,例如wpf或winfrom桌面應用。

SignalR的作用

SignalR是用來做實時通訊的web應用。

適用場景:

  • 需要從伺服器進行高頻率更新的應用。 示例包括遊戲、社交網路、投票、拍賣、地圖和 GPS 應用。

  • 儀表板和監視應用。 示例包括公司儀表板、即時銷售更新或旅行警報。

  • 協作應用。 協作應用的示例包括白板應用和團隊會議軟體。

  • 需要通知的應用。 社交網路、電子郵件、聊天、遊戲、旅行警報和很多其他應用都需使用通知。

      Server 主動傳送到 Client
      瀏覽器 ← ASP.NET Core Web Server
    

無需瀏覽器發起請求,伺服器可主動的向客戶端推送資料。

SignalR"底層"實現

  • SignalR使用了3種“底層”技術來實現實時Web應用,它分別是Long Polling,ServerSent Events和Websocket.

Polling

  • Polling是實現實時Web的一種笨方法,它就是通過定期的向伺服器傳送請求,來檢視伺服器的資料是否有變化。
  • 如果伺服器資料沒有變化,那麼就返回204 No Content;如果有變化就把最新的資料傳送給客戶端
  • 這就是Polling,很簡單,但是比較浪費資源。
  • SingnalR沒有采用Polling這種技術。

Long Polling

  • Long Polling 和 Polling有類似的地方,客戶端都是傳送請求到伺服器。但是不同之處是:如果伺服器沒有新資料要發給客戶端的話,那麼伺服器會繼續保持連線,知道有新的資料產生,伺服器才把新的資料返回給客戶端。
  • 如果請求發出後一段時間內沒有響應,那麼請求就回超時。這時,客戶端會再次發出請求。

ServerSent Events

  • 使用SSE的話,web伺服器可以在任何時間把資料傳送到瀏覽器,可以稱之為推送。而瀏覽器則會監聽進來的資訊,這些資訊就像流資料一樣,這個連結也會一直保持開放,直到伺服器主動關閉它。
  • 瀏覽器會使用一個叫做EventSource的物件用來處理傳過來的資訊,
  • 缺點:很多瀏覽器都有最大併發連線數的限制,只能傳送文字資訊並且只是單向通訊。
  • 優點:使用方式簡單,基於HTTP協議可自動重連。雖然不支援老的瀏覽器但是很容易進行Polling Fail

Web socket

  • Web socket是不同於HTTP的另一個TCP協議。她使得瀏覽器和伺服器之間的互動式通訊變得可能。使用websocket,訊息可以從伺服器發往客戶端,也可以從客戶端發往伺服器,並且沒有HTTP那樣的延遲。資訊流沒有完成的時候,TCP Socket通常是保持開啟狀態。

  • 使用現代瀏覽器時,SignalR大部分情況下都會使用web socket,這也是最有效的傳輸方式。

  • 全雙工通訊:客戶端和伺服器可以同時往對方傳送訊息。

  • 並且不受SEE的瀏覽器最大連線數限制(6個),大部分瀏覽器對websocket連線數的限制是50個。

  • 訊息型別:可以是文字和二進位制,web socket也支援流媒體(音訊和視訊)

  • 其實正常的HTTP請求也使用了TCP socket。web socket標準使用了握手機制把用於HTTP的socket升級為使用WS協議的websocket的socket。

  • web socket生命週期, 1.HTTP握手 2.通訊/資料交換 3.關閉

  • HTTP握手

    • 每一個websocket開始的時候都是一個簡單的HTTP socket。
    • 客戶端首先發送一個GET請求到伺服器,來請求升級socket。
    • 如果伺服器同意的話,這個socket從這時開始就變成了web socket
  • 訊息型別

    • web socket的訊息型別可以是文字,二進位制。也包括控制類的訊息:Ping/Pong和關閉。
    • 每個訊息由一個或多個Frame組成。

SignalR 回落機制

  • 其中web socket僅支援比較現代的瀏覽器,web伺服器也不能太老。
  • 而Server Sent Events 情況可能好一點,但是也存在同樣的問題。
  • 所以SignalR採用了回落機制,SignalR有能力去協商支援的傳輸型別。
  • 瀏覽器使用三種底層技術是有優先順序的,1.如果瀏覽器較新則使用web socket 2.如果不支援web socket則降級使用ServerSent Events。3.如果ServerSent Events都不支援則使用Long Polling。
  • 一旦連線建立成功則會一直髮送訊息keep live,如果有問題則會丟擲異常。
  • 也可以禁用回落機制,只採用一種通訊方式也可以。

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類裡的方法。
  • 之前說過方法呼叫的時候可以傳遞複雜引數,SignalR可以將引數序列化和反序列化。這些引數被序列化的格式叫做HUB協議,所以HUB協議就是一種用來序列化和反序列化的格式。
  • HUB協議的預設協議是JSON,還支援另外一個協議是MessagePack。MessagePack是二進位制格式的。它比JSON更緊湊,而且處理起來更簡單快速,因為它是二進位制的。
  • 此外,SignalR也可以擴充套件使用其他協議。

橫向擴充套件

  • 這時負載均衡器會保證每個進來的請求按照一定的邏輯分配到可能是不同伺服器上。
  • 在使用web socket的時候,沒什麼問題,因為一旦web socket的連線建立,就像在瀏覽器和伺服器之間打開了一條隧道,伺服器是不會切換的。
  • 但是如果使用Long Polling,就可能是有問題了,因為使用Long Polling的情況下,每次傳送訊息都是不同的請求,而每次請求可能會達到不同的伺服器。不同的伺服器可能不知道前一個伺服器通訊的內容,這就會造成問題。
  • 針對這個問題,我們需要使用Sticky Sessions(粘性會話)。
  • Sticky Sessions貌似有很多種實現方式,但是主要是下面要介紹的這種方式。
  • 作為第一次請求的響應的一部分,負載均衡器會在瀏覽器裡面設定一個Cookie,來表示使用這個伺服器。在後續的請求裡,負載均衡器讀取Cookie,然後把請求分配給同一個伺服器。

相關文件:

  • 開源地址:https://github.com/signalr

  • 官方SignalR介紹:https://docs.microsoft.com/zh-cn/aspnet/signalr/overview/getting-started/introduction-to-signalr

二、詳細內容

接下來開始講解如何實戰構建這樣的一個應用程式,基礎建專案建立各種檔案的步驟我直接跳過了在開發教程中裡有講這裡就不做重複操作了。

一.服務端構建

  • (開發教程)服務端:https://docs.microsoft.com/zh-cn/aspnet/core/tutorials/signalr?view=aspnetcore-5.0&tabs=visual-studio

  • 這裡我只展示與教程中不同的部分,原始碼我會分享在文章結尾的群裡並會在程式碼中寫好註釋方便大家理解。

部分核心原始碼展示:

namespace SinganlRDemo.Hubs
{
    //Hub也有身份認證,只有認證之後才能響應裡面的方法
    //[Authorize]
    public class ChatHub : Hub
    {
        public void Check() 
        {
            //獲取客戶端身份(例:名字)
            var user = Context.User.Identity.Name;
        }

        public async Task SendMessage(string user, string message)
        {
            /*
             * Clients.All代表所有已連線的客戶端
             * 
             * 第一個入參,需要呼叫的客戶端的方法名稱。具體在SinganlRDesktop庫中MainViewModel類裡的108行中體現。
             * 第二、三個入參是被呼叫方法需要的引數。
             */
            await Clients.All.SendAsync("ReceiveMessage", user, message);
        }

        public async Task Login(string name) 
        {
            /*
             * 1.在開發過程中,會有需要獲取客戶端使用的使用者的使用者名稱。
             * Context(Context.ConnectionId)剛好能解決這個問題。Context存在於Hub中。
             */

            //2.如果只需要傳送給指定使用者這樣寫即可。
            //var client = Clients.Client(Context.ConnectionId);
            //await client.SendAsync("online", $"{ name }in the group.");

            //3.傳送給所有使用者。
            await Clients.AllExcept(Context.ConnectionId).SendAsync("online",$"{ name }in the group.");

            //4.將當前獲取到的使用者新增到分組裡和移除出分組
            //await Groups.AddToGroupAsync(Context.ConnectionId,"JusterGroup");
            //await Groups.RemoveFromGroupAsync(Context.ConnectionId, "JusterGroup");

            //對指定分組下的使用者傳送訊息
            await Clients.Group("JusterGroup").SendAsync("online", $"{ name }in the group.");
        }

        public async Task SignOut(string name)
        {
            await Clients.AllExcept(Context.ConnectionId).SendAsync("online", $"{ name }leave the group.");
        }
    }
}

二.客戶端構建(WPF)

(開發教程)客戶端:https://docs.microsoft.com/zh-cn/aspnet/core/signalr/dotnet-client?view=aspnetcore-5.0&tabs=visual-studio

    public MainViewModel() 
    {
        //初始化SignalR的hub,然後指定伺服器地址
        connection = new HubConnectionBuilder()
           .WithUrl("https://localhost:44394/chathub")
           //重連機制
           .WithAutomaticReconnect(new RandomRetryPolicy())
           .Build();

        //關閉連線
        connection.Closed += async (error) =>
        {
            await Task.Delay(new Random().Next(0, 5) * 1000);
            await connection.StartAsync();
        };

        //重連
        connection.Reconnecting += error =>
        {
            Debug.Assert(connection.State == HubConnectionState.Reconnecting);
            // Notify users the connection was lost and the client is reconnecting.
            // Start queuing or dropping messages.
            return Task.CompletedTask;
        };

        //接收訊息
        connection.On<string, string>("ReceiveMessage", (user, message) =>
        {
            Application.Current.Dispatcher.Invoke(()=> 
            {
                var newMessage = $"{user}: {message}";
                TalkMessage += newMessage + "\r\n";
            });
        });

        //離線、上線通知
        connection.On<string>("online", (message) =>
        {
            var newMessage = $"{message}";
            MsgCollection.Add(newMessage);
        });

        connection.StartAsync();
    }

    /// <summary>
    /// 傳送訊息給伺服器
    /// </summary>
    /// <param name="user">使用者名稱</param>
    /// <param name="msg">訊息內容</param>
    /// <returns></returns>
    public async Task Send(string user,string msg) 
    {
        try
        {
            await connection.InvokeAsync("SendMessage",
                user, msg);
        }
        catch (Exception ex)
        {
            MsgCollection.Add(ex.Message);
        }
    }

    /// <summary>
    /// 上線
    /// </summary>
    /// <returns></returns>
    public async Task Login() 
    {
        _userName = $"Person{ new Random().Next(1, 99999)}";
        await connection.InvokeAsync("Login", _userName);
    }

    /// <summary>
    /// 離線
    /// </summary>
    /// <returns></returns>
    public async Task SignOut()
    {
        await connection.InvokeAsync("SignOut", _userName);
        await connection.StopAsync();
    }

三、執行效果