C#高效能 TCP 服務的多種實現方式

發表於2016-02-17

本篇文章的主旨是使用 .NET/C# 實現 TCP 高效能服務的不同方式,包括但不限於如下內容:

在 .NET/C# 中對於 Socket 的支援均是基於 Windows I/O Completion Ports 完成埠技術的封裝,通過不同的 Non-Blocking 封裝結構來滿足不同的程式設計需求。以上方式均已在 Cowboy.Sockets 中有完整實現,並且 APM 和 TAP 方式已經在實際專案中應用。Cowboy.Sockets 還在不斷的進化和完善中,如有任何問題請及時指正。

雖然有這麼多種實現方式,但抽象的看,它們是一樣一樣的,用兩個 Loop 即可描述:Accept LoopRead Loop,如下圖所示。(這裡提及的 “Loop” 指的是一種迴圈方式,而非特指 while/for 等關鍵字。)

  • 在任何 TCP Server 的實現中,一定存在一個 Accept Socket Loop,用於接收 Client 端的 Connect 請求以建立 TCP Connection。
  • 在任何 TCP Server 的實現中,一定存在一個 Read Socket Loop,用於接收 Client 端 Write 過來的資料。

如果 Accept 迴圈阻塞,則會導致無法快速的建立連線,服務端 Pending Backlog 滿,進而導致 Client 端收到 Connect Timeout 的異常。如果 Read 迴圈阻塞,則顯然會導致無法及時收到 Client 端發過來的資料,進而導致 Client 端 Send Buffer 滿,無法再傳送資料。

從實現細節的角度看,能夠導致服務阻塞的位置可能在:

  1. Accept 到新的 Socket,構建新的 Connection 需要分配各種資源,分配資源慢;
  2. Accept 到新的 Socket,沒有及時觸發下一次 Accept;
  3. Read 到新的 Buffer,判定 Payload 訊息長度,判定過程長;
  4. Read 到新的 Buffer,發現 Payload 還沒有收全,繼續 Read,則可能會導致一次 Buffer Copy;
  5. Payload 接收完畢,進行 Serialization 轉成可識別的 Protocol Message,序列化慢;
  6. 由 Business Module 來處理相應的 Protocol Message,處理過程慢;

1-2 涉及到 Accept 過程和 Connection 的建立過程,3-4 涉及到 ReceiveBuffer 的處理過程,5-6 涉及到應用邏輯側的實現。

Java 中著名的 Netty 網路庫從 4.0 版本開始對於 Buffer 部分做了全新的嘗試,採用了名叫 ByteBuf 的設計,實現 Buffer Zero Copy 以減少高併發條件下 Buffer 拷貝帶來的效能損失和 GC 壓力。DotNettyOrleans ,Helios 等專案正在嘗試在 C# 中進行類似的 ByteBuf 的實現。

APM 方式:TcpSocketServer

TcpSocketServer 的實現是基於 .NET Framework 自帶的 TcpListenerTcpClient 的更進一步的封裝,採用基於 APM 的 BeginXXX 和 EndXXX 介面實現。

TcpSocketServer 中的 Accept Loop 指的就是,

  • BeginAccept -> EndAccept-> BeginAccept -> EndAccept -> BeginAccept -> …

每一個建立成功的 Connection 由 TcpSocketSession 來處理,所以 TcpSocketSession 中會包含 Read Loop,

  • BeginRead -> EndRead -> BeginRead -> EndRead -> BeginRead -> …

TcpSocketServer 通過暴露 Event 來實現 Connection 的建立與斷開和資料接收的通知。

使用也是簡單直接,直接訂閱事件通知。

TAP 方式:AsyncTcpSocketServer

AsyncTcpSocketServer 的實現是基於 .NET Framework 自帶的 TcpListener 和 TcpClient 的更進一步的封裝,採用基於 TAP 的 async/await 的 XXXAsync 介面實現。

然而,實際上 XXXAsync 並沒有建立什麼神奇的效果,其內部實現只是將 APM 的方法轉換成了 TAP 的呼叫方式。

AsyncTcpSocketServer 中的 Accept Loop 指的就是,

每一個建立成功的 Connection 由 AsyncTcpSocketSession 來處理,所以 AsyncTcpSocketSession 中會包含 Read Loop,

為了將 async/await 非同步到底,AsyncTcpSocketServer 所暴露的介面也同樣是 Awaitable 的。

使用時僅需將一個實現了該介面的物件注入到 AsyncTcpSocketServer 的建構函式中即可。

當然,對於介面的實現也不是強制了,也可以在建構函式中直接注入方法的實現。

SAEA 方式:TcpSocketSaeaServer

SAEA 是 SocketAsyncEventArgs 的簡寫。SocketAsyncEventArgs 是 .NET Framework 3.5 開始支援的一種支援高效能 Socket 通訊的實現。SocketAsyncEventArgs 相比於 APM 方式的主要優點可以描述如下:

The main feature of these enhancements is the avoidance of the repeated allocation and synchronization of objects during high-volume asynchronous socket I/O. The Begin/End design pattern currently implemented by the Socket class for asynchronous socket I/O requires a System.IAsyncResult object be allocated for each asynchronous socket operation.

也就是說,優點就是無需為每次呼叫都生成 IAsyncResult 等物件,向原生 Socket 更靠近一些。

使用 SocketAsyncEventArgs 的推薦步驟如下:

  1. Allocate a new SocketAsyncEventArgs context object, or get a free one from an application pool.
  2. Set properties on the context object to the operation about to be performed (the callback delegate method and data buffer, for example).
  3. Call the appropriate socket method (xxxAsync) to initiate the asynchronous operation.
  4. If the asynchronous socket method (xxxAsync) returns true in the callback, query the context properties for completion status.
  5. If the asynchronous socket method (xxxAsync) returns false in the callback, the operation completed synchronously. The context properties may be queried for the operation result.
  6. Reuse the context for another operation, put it back in the pool, or discard it.

重點在於池化(Pooling),池化的目的就是為了重用和減少執行時分配和垃圾回收的壓力。

TcpSocketSaeaServer 即是對 SocketAsyncEventArgs 的應用和封裝,並實現了 Pooling 技術。TcpSocketSaeaServer 中的重點是 SaeaAwaitable 類,SaeaAwaitable 中內建了一個 SocketAsyncEventArgs,並通過 GetAwaiter 返回 SaeaAwaiter 來支援 async/await 操作。同時,通過 SaeaExtensions 擴充套件方法對來擴充套件 SocketAsyncEventArgs 的 Awaitable 實現。

SaeaPool 則是一個 QueuedObjectPool 的衍生實現,用於池化 SaeaAwaitable 例項。同時,為了減少 TcpSocketSaeaSession 的構建過程,也實現了 SessionPool 即 QueuedObjectPool。

TcpSocketSaeaServer 中的 Accept Loop 指的就是,

每一個建立成功的 Connection 由 TcpSocketSaeaSession 來處理,所以 TcpSocketSaeaSession 中會包含 Read Loop,

同樣,TcpSocketSaeaServer 對外所暴露的介面也同樣是 Awaitable 的。

使用起來也是簡單直接:

RIO 方式:TcpSocketRioServer

從 Windows 8.1 / Windows Server 2012 R2 開始,微軟推出了 Registered I/O Networking Extensions 來支援高效能 Socket 服務的實現,簡稱 RIO。

The following functions are supported for Windows Store apps on Windows 8.1, Windows Server 2012 R2, and later. Microsoft Visual Studio 2013 Update 3 or later is required for Windows Store apps.

  • RIOCloseCompletionQueue
  • RIOCreateCompletionQueue
  • RIOCreateRequestQueue
  • RIODequeueCompletion
  • RIODeregisterBuffer
  • RIONotify
  • RIOReceive
  • RIOReceiveEx
  • RIORegisterBuffer
  • RIOResizeCompletionQueue
  • RIOResizeRequestQueue
  • RIOSend
  • RIOSendEx

到目前為止,.NET Framework 還沒有推出對 RIO 的支援,所以若想在 C# 中實現 RIO 則只能通過 P/Invoke 方式,RioSharp 是開源專案中的一個比較完整的實現。

Cowboy.Sockets 直接引用了 RioSharp 的原始碼,放置在 Cowboy.Sockets.Experimental 名空間下,以供實驗和測試使用。

同樣,通過 TcpSocketRioServer 來實現 Accept Loop,

通過 TcpSocketRioSession 來處理 Read Loop,

測試程式碼一如既往的類似:

參考資料

相關文章