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
不同點
- 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 應用上,控制檯應該也可以