TL; DR:
使用
ReceiveAsync()
循环接收消息,直到接收到
Close
帧或
CancellationToken
被取消。发送消息很简单,只需使用
SendAsync()
即可。不要在使用
CloseOutputAsync()
之前使用
CloseAsync()
,因为您需要先停止接收循环。否则,
CloseAsync()
将挂起,或者如果您使用
CancellationToken
退出
ReceiveAsync()
,
CloseAsync()
将抛出异常。
我从
https://mcguirev10.com/2019/08/17/how-to-close-websocket-correctly.html中学到了很多。
完整回答:
使用Dotnet客户端,以下是一个从我的实际代码中剪切出来的示例,说明了如何进行握手。大多数人不理解它的操作方式的最重要的事情是,在接收到消息时没有魔术事件。你自己创建它。如何?
只需在循环中执行
ReceiveAsync()
,当接收到特殊的
Close
帧时结束循环。因此,当您想要断开连接时,必须告诉服务器您使用
CloseOutputAsync
关闭,以便它会回复一个类似的
Close
帧到您的客户端,以便能够结束接收。
我的代码示例仅说明了最基本的外部传输机制。因此,您发送和接收原始二进制消息。此时,您无法告诉特定服务器响应是否与您发送的特定请求相关联。您必须在编码/解码消息之后自己进行匹配。使用任何序列化工具都可以,但许多加密货币市场使用Google的协议缓冲区。名称说明了一切;)
为了匹配,可以使用任何唯一的随机数据。您需要令牌,在C#中,我使用
Guid
类。
然后,我使用请求/响应匹配使请求不依赖于事件。
SendRequest()
方法等待匹配响应到达,或者...连接已关闭。非常方便,可以使代码比事件驱动的方法更易读。当然,您仍然可以在接收到消息时调用事件,只需确保它们没有与需要响应的任何请求匹配即可。
哦,对于在我的
async
方法中等待,我使用
SemaphoreSlim
。每个请求将其自己的信号量放入特殊字典中,当我获得响应时,我通过响应令牌找到条目,释放信号量,处理它,从字典中删除。看起来很复杂,但实际上非常简单。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
namespace Example {
public class WsClient : IDisposable {
public int ReceiveBufferSize { get; set; } = 8192;
public async Task ConnectAsync(string url) {
if (WS != null) {
if (WS.State == WebSocketState.Open) return;
else WS.Dispose();
}
WS = new ClientWebSocket();
if (CTS != null) CTS.Dispose();
CTS = new CancellationTokenSource();
await WS.ConnectAsync(new Uri(url), CTS.Token);
await Task.Factory.StartNew(ReceiveLoop, CTS.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
public async Task DisconnectAsync() {
if (WS is null) return;
if (WS.State == WebSocketState.Open) {
CTS.CancelAfter(TimeSpan.FromSeconds(2));
await WS.CloseOutputAsync(WebSocketCloseStatus.Empty, "", CancellationToken.None);
await WS.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
}
WS.Dispose();
WS = null;
CTS.Dispose();
CTS = null;
}
private async Task ReceiveLoop() {
var loopToken = CTS.Token;
MemoryStream outputStream = null;
WebSocketReceiveResult receiveResult = null;
var buffer = new byte[ReceiveBufferSize];
try {
while (!loopToken.IsCancellationRequested) {
outputStream = new MemoryStream(ReceiveBufferSize);
do {
receiveResult = await WS.ReceiveAsync(buffer, CTS.Token);
if (receiveResult.MessageType != WebSocketMessageType.Close)
outputStream.Write(buffer, 0, receiveResult.Count);
}
while (!receiveResult.EndOfMessage);
if (receiveResult.MessageType == WebSocketMessageType.Close) break;
outputStream.Position = 0;
ResponseReceived(outputStream);
}
}
catch (TaskCanceledException) { }
finally {
outputStream?.Dispose();
}
}
private async Task<ResponseType> SendMessageAsync<RequestType>(RequestType message) {
}
private void ResponseReceived(Stream inputStream) {
}
public void Dispose() => DisconnectAsync().Wait();
private ClientWebSocket WS;
private CancellationTokenSource CTS;
}
}
顺便问一下,为什么要使用除了.NET内置库之外的其他库?我找不到任何理由,除非是Microsoft类的文档可能不太好。也许 - 如果出于某种非常奇怪的原因,您想要在古老的.NET Framework中使用现代WebSocket传输 ;)
哦,我还没有测试过这个例子。它来自已经测试过的代码,但所有内部协议部分都被删除,只剩下传输部分。
public async Task SendMessageAsync(string message) { ArraySegment bytesToSend = new ArraySegment(Encoding.UTF8.GetBytes(message));
await WS.SendAsync(bytesToSend, WebSocketMessageType.Text, true, CancellationToken.None);
}
- Sean Griffin