TL;博士:
在循环中使用ReceiveAsync(),直到收到Close 帧或取消CancellationToken。这就是您获取消息的方式。发送很简单,只需SendAsync()。不要在CloseOutputAsync() 之前使用CloseAsync() - 因为您想先停止接收循环。否则 - CloseAsync() 会挂起,或者如果您使用 CancellationToken 退出 ReceiveAsync() - CloseAsync() 会抛出。
我从https://mcguirev10.com/2019/08/17/how-to-close-websocket-correctly.html 学到了很多。
完整答案:
使用 Dotnet 客户端,这里有一个从我的真实代码中截取的示例,它说明了握手是如何进行的。大多数人不了解事物如何运作的最重要的事情是收到消息时没有魔术事件。你自己创造它。怎么样?
当接收到一个特殊的Close 帧时,您只需在一个结束的循环中执行ReceiveAsync()。所以当你想断开连接时,你必须用CloseOutputAsync 告诉服务器你关闭了,这样它就会用类似的Cloce 帧回复你的客户端,这样它就可以结束接收了。
我的代码示例仅说明了最基本的外部传输机制。所以你发送和接收原始二进制消息。此时,您无法判断特定服务器响应与您发送的特定请求相关。您必须在编码/解码消息后自己匹配它们。为此使用任何序列化工具,但许多加密货币市场使用 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;
// TODO: requests cleanup code, sub-protocol dependent.
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) {
// TODO: handle serializing requests and deserializing responses, handle matching responses to the requests.
}
private void ResponseReceived(Stream inputStream) {
// TODO: handle deserializing responses and matching them to the requests.
// IMPORTANT: DON'T FORGET TO DISPOSE THE inputStream!
}
public void Dispose() => DisconnectAsync().Wait();
private ClientWebSocket WS;
private CancellationTokenSource CTS;
}
}
顺便说一句,为什么要使用内置的 .NET 以外的其他库?除了微软课程的糟糕文档之外,我找不到任何原因。也许 - 如果出于某种非常奇怪的原因,您想要将现代 WebSocket 传输与古老的 .NET 框架一起使用;)
哦,我还没有测试过这个例子。它取自测试代码,但所有内部协议部分都被删除,只留下传输部分。