本文將深入探討如何運用 C# 搭配 Winsock 與 IOCP (I/O Completion Port) API,從零開始建置一個高效能的 Echo Server。這不僅涉及平台呼叫 (P/Invoke)、反射與多執行緒等核心技術,更深入應用了 GCHandle、SafeHandle 與 Marshal 等進階互通機制。筆者已在前文詳細剖析過 Winsock API 細節,若讀者對底層 Socket 操作尚感陌生,建議先參閱 MSDN 文件建立基礎認知。
.NET 內建的 System.Threading.NativeOverlapped 完整對應 Win32 的 OVERLAPPED 結構。但在實際開發中,WSASend 與 WSAReceive 往往需要攜帶更多業務上下文資料,而不僅是單一重疊結構。經過多方嘗試,筆者設計了 WaOverlapped 結構:在原始結構尾部新增一個指標,指向自訂類別的 GC 控制代碼。如此工作執行緒即可透過此指標取得完整的單次 I/O 資料物件 (PerIoOperationData),有效解決資料關聯難題。
當透過 GCHandle.Alloc 取得物件控制代碼後,若不再使用務必呼叫 Free() 釋放。疏忽此步驟將導致受控物件無法被垃圾回收器回收,造成非預期的記憶體壓力。
若 WSASend 或 WSAReceive 返回錯誤碼 6 (ERROR_INVALID_HANDLE),十之八九是傳入的單次 I/O 資料結構 (PerIoOperationData) 初始化不正確,導致 Socket 控制代碼無效。
傳遞給底層 API 的 Overlapped 物件若未在記憶體中釘住,執行階段將拋出例外,且無法透過 GetLastWin32Error 取得錯誤碼。正確做法是使用 GCHandle.Alloc(overlappedObj, GCHandleType.Pinned) 將其固定在記憶體特定位址,確保非受控程式碼能正確存取。
目前實作尚未進行深度優化。未來可著手:將 PerIoOperationData、PerHandleData 及 Socket 物件封裝為物件池重複使用;以 AcceptEx 取代 Accept,利用預先建立的 Socket 接受連線;採用環狀緩衝區 (Ring Buffer) 減少記憶體分配與複製開銷。這些優化實驗留待後續文章分享。
完整專案需在 Windows Server 2008 環境下開啟「不安全程式碼」編譯選項,編譯後可使用 Telnet 進行連線測試。以下列出核心類別架構與關鍵邏輯:
[StructLayout(LayoutKind.Sequential)]
class PerIoOperationData
{
public WaOverlapped Overlapped;
public WSABuffer DataBuf;
public readonly byte[] Buffer = new byte[DATA_BUFSIZE];
public uint BytesSEND;
public uint BytesRECV;
}
[StructLayout(LayoutKind.Sequential)]
class PerHandleData
{
public SafeSocketHandle Socket;
}
[StructLayout(LayoutKind.Sequential)]
public class WaOverlapped
{
public IntPtr InternalLow;
public IntPtr InternalHigh;
public int OffsetLow;
public int OffsetHigh;
public IntPtr EventHandle;
public IntPtr State; // 指向 GCHandle 的指標
}
GetQueuedCompletionStatusWSA_FLAG_OVERLAPPED)bind) 並進入監聽狀態 (listen)accept 新連線,並將每個 Client Socket 關聯至完成端口PerHandleData、PerIoOperationData,並投遞首個 WSARecv 非同步接收請求工作執行緒於 GetQueuedCompletionStatus 取得完成通知後:
CompletionKey 取得 PerHandleData (Socket 資訊)lpOverlapped 解碼 WaOverlapped.State 取得 PerIoOperationData (I/O 上下文)BytesTransferred == 0 視為對端斷開,釋放資源並關閉 SocketBytesRECV 與 BytesSEND 狀態機判斷:若有未發送完資料則投遞 WSASend,否則投遞下一個 WSARecvPinned 新的 Overlapped 並更新緩衝區指標與長度public class SafeSocketHandle : SafeHandleMinusOneIsInvalid
{
protected override bool ReleaseHandle()
{
return Win32Api.closesocket(base.handle) != SocketError.SocketError;
}
}
[DllImport("ws2_32.dll", SetLastError = true)]
internal static extern SocketError WSARecv(
[In] SafeSocketHandle socketHandle,
[In, Out] ref WSABuffer buffer,
[In] int bufferCount,
out int bytesTransferred,
[In, Out] ref SocketFlags socketFlags,
[In] IntPtr overlapped,
[In] IntPtr completionRoutine);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern unsafe bool GetQueuedCompletionStatus(
SafeFileHandle CompletionPort,
out uint lpNumberOfBytes,
out IntPtr lpCompletionKey,
out IntPtr lpOverlapped,
uint dwMilliseconds);
完整程式碼包含日誌類別 Logger、WSABuffer、WSAData 等結構定義,以及 Win32Api 靜態類別封裝所有 P/Invoke 簽章。建立專案時請務必勾選專案屬性 → Build → "Allow unsafe code"。
編譯執行後,伺服器將於 TCP 5150 埠監聽。開啟命令提示字元輸入 telnet localhost 5150,輸入任意文字即可收到伺服器原封不動回傳的 Echo 回應,驗證非同步收發流程正常運作。
透過 IOCP 模型配合 Winsock 底層 API,我們在 .NET 環境下實現了高併發、低延遲的網路伺服器雛形。關鍵在於妥善管理非受控記憶體 (GCHandle.Pinned)、正確處理重疊結構擴展與生命週期。後續優化方向明確:物件池化、AcceptEx 零拷貝接受、環狀緩衝區等技術將進一步釋放效能潛力。