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
这里具体的可以看看下面这篇博客
嘛,毕竟我也不喜欢理论,就长话短说,尽快上代码吧
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 客户端代码
这里看一个官方文档
- 用 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 应用上,控制台应该也可以