WebSocket通信问题排查及优化

项目中遇到了WebSocket超时问题。具体情况是在OTA升级过程中,解压zip文件会产生解压进度事件,然后将解压进度通过进程通信传递给另一进程,但通信过程中出现了超时异常。

我们发现在解压大文件时,解压进度事件的触发间隔竟然是1毫秒,这个频率实在是太高了。

然而,即使解压事件频率高,也不应该导致通信异常。因此,我进行了定时发送通信事件的测试,以验证进程间通信流程。

WebSocketSharp

当前项目中使用的进程间通信组件是基于kaistseo/UnitySocketIO-WebSocketSharp实现的。在该组件中,我们在主机内设置了一个服务端,并允许多个客户端连接到服务端。客户端之间的通信由服务端来转发数据。具体流程是客户端A发送信息给客户端B,然后客户端B将执行结果反馈给客户端A。

在排查问题时,我们发现各个链路的发送延时都是正常的,包括服务端发送反馈数据给客户端A。然而,客户端A接收数据的延时却非常大。以下是部分返回数据的示例:

并且随着通信时间的增加,延时会变得越来越大。

在查看WebSocketSharp.WebSocket对外事件OnMessage时,我们发现OnMessage是由WebSocket.message()触发的,从_messageEventQueue队列中获取数据。

在循环接收数据时,我们发现了ManualResetEvent。考虑到数据量很大,这里使用了同步信号锁,这必然会导致阻塞。

为什么要设置线程同步锁呢?我们继续深入了解。

WebSocketSharp的数据发送是基于TCPClient实现的。在初始化后,通过_stream.Write (bytes, 0, bytes.Length)来发送数据。

而接收数据则是通过_stream进行读取。可以在startReceiving()方法中看到,使用了WebSocketFrame.ReadFrameAsync (_stream, false,...)来进行数据读取。

我们知道,TCP是面向连接的,提供可靠、顺序的数据流传输。它适用于一对一的通信,即一个TCP连接只能有一个发送方和一个接收方。更多细节可以参考之前我写的文章:.NET TCP、UDP、Socket、WebSocket - 唐宋元明清2188 - 博客园 (cnblogs.com)

然而,在高并发场景下,适当的同步措施仍然是必需的。我们可以使用lock,也可以使用SemaphoreSlim来实现复杂的同步需求。而在这里,使用的是信号锁ManualResetEvent。

再来看看发送端的代码,同样也使用了lock来限制并发操作。

因此,在高并发场景下,WebSocketSharp存在通信阻塞问题。当然,WebSocketSharp已经做得很好了,正常情况下几毫秒内是不会遇到阻塞问题的。例如,设置了3毫秒的定时超频发送后,发送一段时间后的效果如下:

客户端A发送消息,由服务端转发至客户端B,再将客户端B的反馈结果由服务端转发回客户端A,真正的延时只有0-2毫秒!

因此,在项目中遇到的ZIP文件解压进度超快1毫秒的问题,我们需要对zip解压处进行优化,设置并发操作10毫秒内保留最后一个操作。可以参考.NET异步并发操作,只保留最后一次操作 - 唐宋元明清2188 - 博客园 (cnblogs.com),即在10毫秒内最多触发一次解压进度事件。确实,这样的优化也是有必要的,即使通信能够承受这种高并发,UI刷新如此高的帧率也会浪费CPU/GPU资源。

WebSocket

接下来,我们再来看看原生的WebSocket,并编写一个WebSocket通信Demo kybs00/WebSocketDemo (GitHub.com)。

在该Demo中,服务端定时1毫秒向客户端发送Message消息,结果竟然是:

System.InvalidOperationException:“There is already one outstanding 'SendAsync' call for this WebSocket instance. ReceiveAsync and SendAsync can be called simultaneously, but at most one outstanding operation for each of them is allowed at the same time.”

看来发送事件外部也要处理好高并发的场景,1毫秒真的是太猛了。

加上信号量同步后,服务端就能正常发送了。以下是10分钟后客户端接收数据打印的结果,传输几乎没有延时:

此外,我们还尝试了在客户端接收数据时添加信号量同步,但仍然提示服务端发送不支持并行操作的异常。

因此,在发送端加入需要串行处理的信号量,例如上面提到的SemaphoreSlim,以确保完整地写入数据并执行_stream.FlushAsync()。

热门手游下载
下载排行榜