1. 程式人生 > 其它 >SignalR 與客戶端通訊

SignalR 與客戶端通訊

SignalR 與客戶端通訊

寫這篇文章的起因是學習 ASP.NET Core 3.x 時的 SignalR
SignalR 的底層技術中使用了 WebSocket ,於是我就想到了 WPF 的 Socket,畢竟學 Socket 時手搓了一個簡單的聊天系統,就想試試利用 Socket 或者 WebSocket 讓 WPF 程式與 ASP.NET Core 網頁端通訊

但是查了查資料,網頁與 Socket 通訊的例子蠻麻煩的,需要一個 Socket 伺服器,一個 WebSocket 伺服器,同時 WebSocket 伺服器也作為 Socket 的一個客戶端與 Socket 通訊,WebSocket 再去跟網頁通訊
WebSocket 的倒是有,但是要用封裝好的庫,所以就乾脆用 SignalR

當然要先講些概念

Socket 與 Web Socket

這裡具體的可以看看下面這篇部落格

https://www.cnblogs.com/Javi/p/9303020.html

嘛,畢竟我也不喜歡理論,就長話短說,儘快上程式碼吧

Socket

  • Socket 是應用層TCP/IP 協議族通訊的中間軟體抽象層,它是一組介面。位於應用層傳輸控制層之間的一組介面。
  • 在設計模式中,Socket 其實就是一個門面模式,它把複雜的 TCP/IP 協議族隱藏在 Socket 介面後面,對使用者來說,一組簡單的介面就是全部,讓Socket 去組織資料,以符合指定的協議。

Web Socket

  • Web Socket 是基於 HTTP
    協議的,而 HTTP 是應用層協議

不同點

  • Socket 是傳輸控制層協議,WebSocket 是應用層協議。

Socket 參考資料:https://www.jianshu.com/p/066d99da7cbd

TCP/IP協議參考模型把所有的TCP/IP系列協議歸類到四個抽象層中

  • 應用層:TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 等等

  • 傳輸層:TCP,UDP

  • 網路層:IP,ICMP,OSPF,EIGRP,IGMP

  • 資料鏈路層:SLIP,CSLIP,PPP,MTU

吐槽

其實我試過用 Socket 與 WebSocket 通訊,畢竟一般客戶端的速度都比網頁快,而且 Socket 比 Http 更底層
可惜,出師不利,我只能想到 傳輸資訊附加字串判斷呼叫的函式+反射,用反射來實現的話,效率肯定不高,或者反射改成字典+委託?
彳亍口巴,爺去 GitHub 看原始碼了
Microsoft.AspNetCore.SignalR.Client 裡面的 HubConnection

類,應該是客戶端呼叫伺服器的函式時執行

private void LaunchStreams(ConnectionState connectionState, Dictionary<string, object>? readers, CancellationToken cancellationToken)
{
    if (readers == null)
    {
        // if there were no streaming parameters then readers is never initialized
        return;
    }

    foreach (var kvp in readers)
    {
        var reader = kvp.Value;

        // For each stream that needs to be sent, run a "send items" task in the background.
        // This reads from the channel, attaches streamId, and sends to server.
        // A single background thread here quickly gets messy.
        if (ReflectionHelper.IsIAsyncEnumerable(reader.GetType()))
        {
            _ = _sendIAsyncStreamItemsMethod
                .MakeGenericMethod(reader.GetType().GetInterface("IAsyncEnumerable`1")!.GetGenericArguments())
                .Invoke(this, new object[] { connectionState, kvp.Key.ToString(), reader, cancellationToken });
            continue;
        }
        _ = _sendStreamItemsMethod
            .MakeGenericMethod(reader.GetType().GetGenericArguments())
            .Invoke(this, new object[] { connectionState, kvp.Key.ToString(), reader, cancellationToken });
    }
}

private async Task<object?> InvokeCoreAsyncCore(string methodName, Type returnType, object?[] args, CancellationToken cancellationToken)
{
    var readers = default(Dictionary<string, object>);

    CheckDisposed();
    var connectionState = await _state.WaitForActiveConnectionAsync(nameof(InvokeCoreAsync), token: cancellationToken);

    Task<object?> invocationTask;
    try
    {
        CheckDisposed();

        readers = PackageStreamingParams(connectionState, ref args, out var streamIds);

        var irq = InvocationRequest.Invoke(cancellationToken, returnType, connectionState.GetNextId(), _loggerFactory, this, out invocationTask);
        await InvokeCore(connectionState, methodName, irq, args, streamIds?.ToArray(), cancellationToken);

        LaunchStreams(connectionState, readers, cancellationToken);
    }
    finally
    {
        _state.ReleaseConnectionLock();
    }

    // Wait for this outside the lock, because it won't complete until the server responds
    return await invocationTask;
}

大致應該就是這些,如果你知道是 SignalR 裡的哪段,可以告訴我

Microsoft.AspNetCore.SignalR 裡面的 DefaultHubLifetimeManager 類,應該是伺服器呼叫客戶端的對應函式

private Task SendToAllConnections(string methodName, object?[] args, Func<HubConnectionContext, object?, bool>? include, object? state = null, CancellationToken cancellationToken = default)
{
    List<Task>? tasks = null;
    SerializedHubMessage? message = null;

    // foreach over HubConnectionStore avoids allocating an enumerator
    foreach (var connection in _connections)
    {
        if (include != null && !include(connection, state))
        {
            continue;
        }

        if (message == null)
        {
            message = CreateSerializedInvocationMessage(methodName, args);
        }

        var task = connection.WriteAsync(message, cancellationToken);

        if (!task.IsCompletedSuccessfully)
        {
            if (tasks == null)
            {
                tasks = new List<Task>();
            }

            tasks.Add(task.AsTask());
        }
        else
        {
            // If it's a IValueTaskSource backed ValueTask,
            // inform it its result has been read so it can reset
            task.GetAwaiter().GetResult();
        }
    }

    if (tasks == null)
    {
        return Task.CompletedTask;
    }

    // Some connections are slow
    return Task.WhenAll(tasks);
}

SignalR 伺服器程式碼

我這裡直接用的 GitHub 上的一個專案

https://github.com/aspnet/SignalR-samples
應該是啥都沒改,直接執行

WPF 客戶端程式碼

這裡看一個官方文件

https://docs.microsoft.com/zh-cn/aspnet/core/signalr/dotnet-client?view=aspnetcore-3.1&tabs=visual-studio

  • 用 NuGet 裝一個庫:Microsoft.AspNetCore.SignalR.Client

首先是 XAML 介面,隨便用了幾個控制元件,佈局亂了就稍微拖動視窗大小

<Window x:Class="WPF_ChatHub.Client.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPF_ChatHub.Client"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid Background="SkyBlue">
        <Button x:Name="Button_StartConnection" Content="開始連線" Width="100" Height="30" Margin="20,20,680,380" Click="Button_StartConnection_Click"/>
        <Button x:Name="Button_StopConnection" Content="停止連線" Width="100" Height="30" Margin="150,22,550,383" Click="Button_StopConnection_Click"/>
        <Button x:Name="Button_SendMessage" Content="傳送資訊" Width="100" Height="30" Margin="672,22,28,383" Click="Button_SendMessage_Click"/>
        <TextBox x:Name="TextBox_Input" Width="750" Height="40" VerticalAlignment="Top" Margin="0,80,0,0"/>
        <RichTextBox x:Name="RichTextBox_BroadcastMessage" Width="750" Height="300" VerticalAlignment="Bottom"/>
    </Grid>
</Window>

然後是客戶端的程式碼

public partial class MainWindow : Window
{
    //與 SignalR 連線的物件
    private readonly HubConnection _hubConnection;

    private const string USERNAME = "zhangsan";

    public MainWindow()
    {
        InitializeComponent();

        this.Button_StopConnection.IsEnabled = false;
        this.Button_SendMessage.IsEnabled = true;

        //配置連線物件
        this._hubConnection = new HubConnectionBuilder()
            .WithUrl("https://localhost:5001/chathub")
            .Build();

        //繫結從伺服器回撥的方法
        this._hubConnection.On<string, string>("BroadcastMessage", async (name, message) =>
            {
                //加一個換行符
                message += Environment.NewLine;
                //輸出資訊
                await this.BroadcastMessage(name, message);
            });
    }

    private async void Button_StartConnection_Click(object sender, RoutedEventArgs e)
    {
        this.Button_StartConnection.IsEnabled = false;

        //開始連線
        await this._hubConnection.StartAsync();

        this.RichTextBox_BroadcastMessage.AppendText("連線成功" + Environment.NewLine);

        this.Button_StopConnection.IsEnabled = true;
        this.Button_SendMessage.IsEnabled = true;
    }

    private async void Button_StopConnection_Click(object sender, RoutedEventArgs e)
    {
        this.Button_StopConnection.IsEnabled = false;
        this.Button_SendMessage.IsEnabled = false;

        //停止連線
        await this._hubConnection.StopAsync();

        this.RichTextBox_BroadcastMessage.AppendText("斷開連線" + Environment.NewLine);

        this.Button_StartConnection.IsEnabled = true;
    }

    private async void Button_SendMessage_Click(object sender, RoutedEventArgs e)
    {
        StringBuilder inputBuilder = new StringBuilder();
        //由於呼叫是由SignalR伺服器來呼叫輸出函式,所以這裡不需要換行符
        inputBuilder.Append(this.TextBox_Input.Text);

        //傳遞 使用者名稱和文字資訊 資料給 SignalR 伺服器
        await this._hubConnection.InvokeAsync("Send", MainWindow.USERNAME, inputBuilder.ToString());

        //清空輸入框
        this.TextBox_Input.Clear();
    }

    private async Task BroadcastMessage(string username, string message)
    {
        //輸出資訊
        this.RichTextBox_BroadcastMessage.AppendText($"{username} : {message}");
    }
}
  • 我這裡為了儘量把客戶端程式碼整的簡單,所以使用者名稱寫死

  • 連線物件

private readonly HubConnection _hubConnection; 
  • 配置連線物件
new HubConnectionBuilder()
    .WithUrl("https://localhost:5001/chathub")
    .Build();
  • 配置伺服器呼叫 BroadcastMessage 函式時所執行的函式,即伺服器呼叫Clients.All.SendAsync("BroadcastMessage", name, message)時客戶端所做的操作,我這裡偷懶用了 Lambda 表示式
this._hubConnection.On<string, string>("BroadcastMessage", async (name, message) =>
    {
    });
  • 客戶端呼叫伺服器的函式,即呼叫伺服器中名為 Send 的函式,後面的引數就是伺服器中 Send 函式的引數
this._hubConnection.InvokeAsync("Send", MainWindow.USERNAME, inputBuilder.ToString());

效果

啟動 SignalR 專案和 WPF 專案

  • 首先是 SignalR 伺服器

  • 然後是 WPF 客戶端,還要連線上 SignalR 伺服器

  • 伺服器先發送資訊

  • 客戶端傳送資訊

SignalR 與客戶端通訊結束

理論上 Microsoft.AspNetCore.SignalR.Client 這個庫是可以用在不僅僅是 WPF 應用上,控制檯應該也可以