使用 System.Net.WebSockets.SendAsync 同步发送

Sending syncronously with System.Net.WebSockets.SendAsync

提问人:Chris.J 提问时间:2/7/2022 更新时间:2/8/2022 访问量:805

问:

我完全赞成多线程和异步任务,但放弃控制不是我的菜。

我正在做一个项目,其中一个进程通过 websocket 与多个目标通信,发送显然应该尽可能快,但必须相互同步。 考虑将视频流同步流式传输到多个设备。 因此,我不想在第一帧被目标接收到之前继续发送下一帧。

使用 .net WebSockets 时,我已经尝试过使用 HttpListner 和 Kestrel 实现(我猜它们或多或少是相同的),然后是套接字。SendAsync 似乎正在接收数据并在发送数据之前在本地缓冲它,在实际发送之前返回 TaskCompleted。这意味着发送任务基本上是即时完成的。

我当然“可以”实现一个 ACK 模式,其中收件人在收到整个数据包时发送 ACK,但这本身会减慢该过程,我想避免这种情况,因为系统本身已经知道这一点,因为它确实会尝试连续发送排队的包。

这基本上是负责发送数据的代码,以 2048b 的块为单位:

Log.Info($"Send {sendBytes.Length}b");
if (socket!.State == WebSocketState.Open)
{
    await socket.SendAsync(
        sendBuffer,
        WebSocketMessageType.Binary,
        endOfMessage: true,
        cancellationToken: cts
        );
}
Log.Info($"Done "); 

我假设这里的等待实际上会等待传输完成,这是不行的,但它似乎在等待任务完成。

在接收方,这是处理数据,调试响应在之前和之后:

wsClientSession.send("REC " + (String)payloadLength + "b");
ProcessFrame(payload, payloadLength);
wsClientSession.send("ACK " + (String)payloadLength + "b");

发送方端的日志记录显示以下内容:

  • 线程 28 是发送线程,它完成的速度快得令人难以置信。它需要发送 6198 字节,并在 4 次传输中将其分块。显然,这一切都是在第一个数据包离开服务器之前完成的。
  • 其余线程是调试响应的异步接收者。
  • (大小差异是由于数据加密造成的)
2022-02-07 16:03:34.8887 threadid:28 # Info # Send 6198b to 5d8263c6-909c-47a1-b4d0-57d74d6a4665
2022-02-07 16:03:34.8887 threadid:28 # Info # Send 2048b
2022-02-07 16:03:34.8887 threadid:28 # Info # Done
2022-02-07 16:03:34.8887 threadid:28 # Info # Send 2048b
2022-02-07 16:03:34.8887 threadid:28 # Info # Done
2022-02-07 16:03:34.8887 threadid:28 # Info # Send 2048b
2022-02-07 16:03:34.8887 threadid:28 # Info # Done
2022-02-07 16:03:34.8887 threadid:28 # Info # Send 128b
2022-02-07 16:03:34.8887 threadid:28 # Info # Done
2022-02-07 16:03:34.8887 threadid:28 # Info # Send done to 5d8263c6-909c-47a1-b4d0-57d74d6a4665
2022-02-07 16:03:35.0024 threadid:7 # Info # 5d8263c6-909c-47a1-b4d0-57d74d6a4665 - REC 2030b
2022-02-07 16:03:35.0090 threadid:7 # Info # 5d8263c6-909c-47a1-b4d0-57d74d6a4665 - ACK 2030b
2022-02-07 16:03:35.0090 threadid:17 # Info # 5d8263c6-909c-47a1-b4d0-57d74d6a4665 - REC 2030b
2022-02-07 16:03:35.0260 threadid:33 # Info # 5d8263c6-909c-47a1-b4d0-57d74d6a4665 - ACK 2030b
2022-02-07 16:03:35.0260 threadid:33 # Info # 5d8263c6-909c-47a1-b4d0-57d74d6a4665 - REC 2030b
2022-02-07 16:03:35.0260 threadid:30 # Info # 5d8263c6-909c-47a1-b4d0-57d74d6a4665 - ACK 2030b
2022-02-07 16:03:35.0373 threadid:17 # Info # 5d8263c6-909c-47a1-b4d0-57d74d6a4665 - REC 108b
2022-02-07 16:03:35.0373 threadid:4 # Info # 5d8263c6-909c-47a1-b4d0-57d74d6a4665 - ACK 108b

现在,我对这个 WebSocket 实现的理解是错误的吗?是否不可能对发送的数据进行回调/事件或类似处理?我能用另一种方式实现这一点吗? 同步执行这个 true 将是一个解决方案,但我似乎无法强迫这样做。

嘎嘎!!

.NET 异步 WebSocket 同步 SendaSync

评论

0赞 Panagiotis Kanavos 2/7/2022
你没有放弃控制。await somethingAsync()' 与 没有什么不同,只是调用线程不会浪费等待任何东西。 这是一个自相矛盾的说法。任务传输。你有没有使用一种方法,也许只是为了能够使用?这是一个编码错误,因为不能等待方法。它们仅适用于异步事件处理程序something()actually wait for the transmission to complete, which is doesn't do, but it seems to await the task completedness.async voidawaitasync void
0赞 Panagiotis Kanavos 2/7/2022
Thread 28 is the sending thread, which completes impossibly fast. It needs to send 6198 bytes and chuncks this up in 4 transmissions. It is obviously all done before the first packet has left the server.如果您忘记了实际调用 .发布足够的代码来重现问题awaitSendAsync
0赞 Chris.J 2/8/2022
方法本身如下所示: public abstract Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken); ..所以它应该是可以等待的,对吧?那里有一个代码示例正在等待,这就是为什么我感到困惑,因为它的完成速度和它一样快。考虑到有效载荷和接收平台,传输无法在 100uS 内完成,而且这一切都是通过 WiFi 进行的。我完全是为了学习,所以如果有编码错误,那就全靠我了。但首先我需要找到它。

答:

1赞 Stephen Cleary 2/8/2022 #1

我正在做一个项目,其中一个进程通过 websocket 与多个目标通信,发送显然应该尽可能快,但必须相互同步。考虑将视频流同步流式传输到多个设备。因此,我不想在第一帧被目标接收到之前继续发送下一帧。

井。。。实际上。。。如今,视频和音频传输都倾向于使用 UDP,并且故意设计为允许丢弃数据包和帧。在 30(或 60)fps 的源中丢失一帧几乎不明显,但在重新获取旧帧时在视频上放置微调器非常明显。

因此,也许首先要考虑的是您可能希望(或需要)放宽您的要求。网络与硬连线控件不同,任何这样的解决方案本质上都是分布式的,因此必须具有某种最终一致性或某种分区不容忍(请参阅:CAP 定理)。

套接字。SendAsync 似乎正在接收数据并在发送数据之前在本地缓冲它,在实际发送之前返回 TaskCompleted。这意味着发送任务基本上是即时完成的。

这不仅仅是 websockets、.NET,甚至是 Windows。这是(故意)所有 TCP/IP API 的工作方式。当“发送”到达操作系统缓冲区时,它被视为完成。在 TCP/IP 环境中,可以保证数据最终会到达接收方(可能在重试后),或者您的套接字最终会进入错误状态。

我当然“可以”实现一个 ACK 模式,其中收件人在收到整个数据包时发送 ACK,但这本身会减慢该过程,我想避免这种情况,因为系统本身已经知道这一点,因为它确实会尝试连续发送排队的包。

Websocket 确实具有消息抽象,并为您维护消息边界。但是 websocket 是通过 TCP/IP 实现的,TCP/IP 是一种流抽象(不是消息或数据包)。网络本身使用数据包抽象,而数据包是 ACK/RST/etc 发挥作用的地方。因此,操作系统知道数据包级 ACK/etc,并将从 TCP/IP 流缓冲区中删除字节。但是 websocket 没有消息级 ACK,因此无法知道何时收到消息。

Websockets 建立在 TCP/IP 之上,因此您可以获得相同的保证:您知道发送的消息(最终)将被接收,或者(最终)您将收到错误通知。这几乎就是你所得到的;与 TCP/IP 的唯一主要区别是,您得到的是消息抽象而不是流抽象。

因此,您的选择是:

  • 接受最终一致性。同时向所有设备发送消息,并接受某些更新可能会延迟。
  • 接受分区不容错。添加 ACK 响应消息,并在所有设备都 ACKED 后发送下一次更新。(这不能容忍分区,因为如果一个设备断开连接,它们都会中断)。

如果您确实使用 ACK-lockstep 方法,您可能需要考虑使用 UDP/IP 而不是基于 TCP/IP 的协议,但这是一项繁重的工作。

同步执行这个 true 将是一个解决方案,但我似乎无法强迫这样做。

同步 API 不是解决方案。没有 API 可以做你想做的事;所有 TCP/IP 写入在到达操作系统时都认为写入“完成”;无论同步/异步、语言、运行时或操作系统如何,都是如此。

评论

0赞 Chris.J 2/8/2022
感谢您的精彩文章,它(不幸)证实了我对项目方向的怀疑,我会做出相应的调整。
0赞 Stephen Cleary 2/16/2022
今晚对 UDP 套接字的缓冲区进行了一些研究,并在旧的 Windows 设备驱动程序邮件列表上遇到了一个非常有趣的断言:根据该帖子,如果您设置为在 TCP/IP 套接字上,那么发送实际上将等待来自远程计算机的 TCP/IP ACK。因此,可以在该级别获得 ACK 通知。不过,我怀疑您是否可以从任何 websocket API 访问底层套接字。这仍然是一个有问题的解决方案,IMO 在其他客户端获得下一帧之前出现一个客户端错误(并且必须超时)。还有性能...SO_SNDBUF0