.NET WebSocket高併發通訊阻塞問題

唐宋元明清2188發表於2024-09-04

專案上遇到使用WebSocket超時問題,具體情況是這樣的,OTA升級過程中,解壓zip檔案會有解壓進度事件,將解壓進度透過程序通訊傳給另一程序,通訊提示超時異常

小夥伴堂園發現大檔案使用Zip解壓,解壓進度事件間隔竟然是1ms,簡直超大頻率啊

但是,解壓事件超頻也不應該通訊異常啊,於是我透過1ms定時傳送通訊事件,測試了下程序間通訊流程。

WebSocketSharp

當前程序間通訊元件是基於kaistseo/UnitySocketIO-WebSocketSharp實現,主機內設定一服務端,多個客戶端連線服務端,客戶端通訊由服務端轉發資料。客戶端A傳送給B後,客戶端B會將執行結果反饋給客戶A。

那在定位中發現,各個鏈路傳送延時都是正常的,包括服務端傳送反饋資料給到客戶端A,但客戶端A接收資料延時很大,下面是部分返回資料:

並且通訊時間久了之後,延時會越來越大

這裡是WebSocketSharp.WebSocket對外事件OnMessage:

 1     private void WebSocketOnMessage(object sender, MessageEventArgs e)
 2     {
 3         if (!e.IsText)
 4         {
 5             //暫時不支援
 6             return;
 7         }
 8         Debug.WriteLine($"{DateTime.Now.ToString("HH:mm:ss fff")},{e.Data}");
 9 
10         var receivedMessage = JsonConvertSlim.Decode<ChannelServerMessage>(e.Data);
11         xxxxx
12     }

我們繼續往下看,OnMessage是由WebSocket.message()觸發,從_messageEventQueue佇列中獲取資料:

 1     private void message ()
 2     {
 3       MessageEventArgs e = null;
 4       lock (_forMessageEventQueue) {
 5         if (_inMessage || _messageEventQueue.Count == 0 || _readyState != WebSocketState.Open)
 6           return;
 7 
 8         _inMessage = true;
 9         e = _messageEventQueue.Dequeue ();
10       }
11 
12       _message (e);
13     }

迴圈接收資料是這樣拿的:

 1     private void startReceiving ()
 2     {
 3       xxxx
 4       _receivingExited = new ManualResetEvent (false);
 5       Action receive = () => WebSocketFrame.ReadFrameAsync (_stream, false,
 6             frame => {
 7               if (!processReceivedFrame (frame) || _readyState == WebSocketState.Closed) {
 8                 var exited = _receivingExited;
 9                 if (exited != null)
10                   exited.Set ();
11                 return;
12               }
13               // Receive next asap because the Ping or Close needs a response to it.
14               receive ();
15               xxxx
16               message ();
17             },
18             xxxx
19           );
20       receive ();
21     }

這裡我看到了ManualResetEvent。。。資料量那麼大,這裡搞個同步訊號鎖,肯定會堵住咯

為何設定執行緒同步鎖呢?我們往下看

WebSocketSharp資料傳送是基於TCPClient實現的:

1     _tcpClient = new TcpClient (_proxyUri.DnsSafeHost, _proxyUri.Port);
2     _stream = _tcpClient.GetStream ();

初始化後透過_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一個object來限制併發操作:

 1     private bool send (Opcode opcode, Stream stream)
 2     {
 3       lock (_forSend) {
 4         var src = stream;
 5         var compressed = false;
 6         var sent = false;
 7         xxxxx 
 8         sent = send (opcode, stream, compressed);
 9         xxxxx 
10         return sent;
11       }
12     }

所以WebSocketSharp在高併發場景下是存在通訊阻塞問題的。當然,WebSocketSharp已經實現的很好了,正常的話幾ms都不會遇到阻塞問題,如下設定3ms定時超頻傳送、傳送一段時間後:

客戶端A傳送訊息,由服務端轉發至客戶B,再將客戶端B的反饋結果由服務端轉發回客戶端A,真正延時才0-2ms!

所以上方專案中遇到的ZIP檔案解壓進度超快1ms,只能要ZIP解壓處最佳化下,設定併發操作10ms內保留最後一個操作,可以參考 .NET非同步併發操作,只保留最後一次操作 - 唐宋元明清2188 - 部落格園 (cnblogs.com),即10ms最多觸發一次解壓進度事件。確實也應該這麼最佳化,通訊即使撐住這種高併發,UI重新整理這麼高幀率也有點浪費CPU/GPU資源。

WebSocket

我們再看看原生的WebSocket,寫個WebSocket通訊Demo kybs00/WebSocketDemo (github.com)

服務端定時1ms使勁往客戶端傳送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.”

看來傳送事件外部也要處理好高併發的場景,1ms真的是太猛了

 1     private SemaphoreSlim _sendLock = new SemaphoreSlim(1);
 2     private async void Timer_Elapsed(object sender, ElapsedEventArgs e)
 3     {
 4         var message = $"{DateTime.Now.ToString("HH:mm:ss fff")},hello from server";
 5 
 6         await _sendLock.WaitAsync();
 7         await BroadcastAsync("test", message);
 8         _sendLock.Release();
 9         Console.WriteLine(message);
10     }

加完訊號量同步,服務端就能正常傳送了。下面是10分鐘後客戶端接收資料列印,傳輸幾乎無延時:

另外,也嘗試了單獨在客戶端接收新增訊號量同步,依然是提示服務端傳送不支援並行的異常。

所以原生WebSocket在傳送端加個需要序列處理比如上面的SemaphoreSlim訊號量,保證完整的寫入完資料、執行_stream.FlushAsync()。

相關文章