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 应用上,控制台应该也可以

posted @ 2021-07-17 14:38  .NET好耶  阅读(474)  评论(0编辑  收藏  举报