寫網路程式入門易 (建 Socket、Connect、收發),但支撐「少則一個、多則數千個」並發連線的穩定架構才是真正挑戰。本文聚焦 Windows NT/2000 平台,基於 Winsock 2 的完成端口機制,構建高擴展性伺服器架構。核心原則同樣適用於客戶端。
WSAAsyncSelect()、Unix select() 易用但無法滿足大規模擴展需求。重疊 I/O 完成 → 內核將通知放入完成端口隊列 → 工作者執行緒取出處理。Socket 可在任意時刻關聯完成端口。
// 1. 建立完成端口
HANDLE hIocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
// 2. 將 Socket 關聯至完成端口 (第三參數為 Completion Key,可攜帶上下文)
CreateIoCompletionPort((HANDLE)s, hIocp, (ULONG_PTR)0, 0);
GetQueuedCompletionStatus() 取得完成通知。標準 OVERLAPPED 無法識別操作類型。擴展結構攜帶上下文:
typedef struct _OVERLAPPEDPLUS {
OVERLAPPED ol; // 必須為第一成員 (或用 CONTAINING_RECORD)
SOCKET s, sclient; // 監聽/客戶 Socket
int OpCode; // 操作類型
WSABUF wbuf; // 資料緩衝區
DWORD dwBytes, dwFlags;
// 其他上下文...
} OVERLAPPEDPLUS;
#define OP_READ 0
#define OP_WRITE 1
#define OP_ACCEPT 2
發起操作時填充 OpCode,完成時透過 CONTAINING_RECORD 取回完整上下文。
DWORD WINAPI WorkerThread(LPVOID) {
ULONG_PTR PerHandleKey;
OVERLAPPED* Overlap;
OVERLAPPEDPLUS* OverlapPlus;
DWORD dwBytesXfered;
while (1) {
BOOL ret = GetQueuedCompletionStatus(hIocp, &dwBytesXfered,
(PULONG_PTR)&PerHandleKey, &Overlap, INFINITE);
if (!ret) continue; // 操作失敗
OverlapPlus = CONTAINING_RECORD(Overlap, OVERLAPPEDPLUS, ol);
switch (OverlapPlus->OpCode) {
case OP_ACCEPT: {
// 客戶 Socket 在 OverlapPlus->sclient
CreateIoCompletionPort((HANDLE)OverlapPlus->sclient, hIocp, 0, 0);
// 分配新 OVERLAPPEDPLUS 發起讀取
OVERLAPPEDPLUS* newolp = AllocateOverlappedPlus();
newolp->s = OverlapPlus->sclient;
newolp->OpCode = OP_READ;
PrepareSendBuffer(&newolp->wbuf);
WSASend(newolp->s, &newolp->wbuf, 1, &newolp->dwBytes, 0, &newolp->ol, NULL);
FreeOverlappedPlus(OverlapPlus);
SetEvent(hAcceptThread); // 通知 Accept 執行緒繼續
break;
}
case OP_READ: {
// 處理接收資料...
// 重新投遞讀取
memset(&OverlapPlus->ol, 0, sizeof(OVERLAPPED));
WSARecv(OverlapPlus->s, &OverlapPlus->wbuf, 1, &OverlapPlus->dwBytes,
&OverlapPlus->dwFlags, &OverlapPlus->ol, NULL);
break;
}
case OP_WRITE: {
// 處理發送完成...
break;
}
}
}
}

send/WSASend → AFD 內部發送緩衝區 (受 SO_SNDBUF 限制) → 立即返回 → 後台發送。SO_RCVBUF) → 應用 recv/WSARecv 拷貝到用戶緩衝區。SO_SNDBUF/SO_RCVBUF = 0):會鎖定用戶緩衝區頁面、導致系統資源耗盡、效能反降。WSARecv 掛載,確保 AFD 總有用戶緩衝區可用,內部緩衝區自然閒置。| 資源 | 限制 | 應對策略 |
|---|---|---|
| 網路頻寬 | 物理上限 | 流控、背壓、優先級隊列 |
| 虛擬記憶體 | 系統分頁檔 | Lookaside Lists 重用記憶體;SetWorkingSetSize 鎖定工作集 |
| 鎖定頁面 (Non-paged Pool) | ~ 物理記憶體 1/8 | 限制並發重疊操作數;緩衝區對齊頁邊界避免額外鎖頁 |
| 非分頁池 | 系統固定配額 | 保守估算連線數;監控 ERROR_INSUFFICIENT_RESOURCES;優雅降級關閉部分連線 |
核心原則:不計算精確配額 (版本差異大),而是保守設計、監控錯誤碼、優雅退避。
AcceptEx 守候連線。lpOutputBuffer 可同時接收首包資料,但若客戶只連不發,會佔用 Accept 槽位 → DoS 攻擊向量。getsockopt(SO_CONNECT_TIME),連線建立超時無資料則關閉 Socket。WSAIoctl(SIO_GET_EXTENSION_FUNCTION_POINTER) 獲取 AcceptEx 等 Microsoft 特有擴展函數指標,避免每次調用內部 WSAIoctl 開銷。TransmitFile:單次調用發送整個檔案,僅兩次模式切換 (CreateFile + TransmitFile),遠優於 ReadFile+WSASend 循環。TransmitPackets (Win2000+):單次調用發送多個檔案/記憶體緩衝區組合,支援 TF_REUSE_SOCKET 重用 Socket 回 AcceptEx 池。AcceptEx (附帶接收緩衝區)。AcceptEx 投遞數量 (壓測調優)。SO_CONNECT_TIME 輪詢防禦 DoS。TransmitFile/Packets 發送靜態/大量資料。大規模 Winsock 伺服器非可怕:核心是監聽 Socket + 完成端口 + 重疊收發 + 合理守候數量。真正挑戰在於資源配額管理——防非分頁池耗盡、防鎖頁超限、防頻寬過載。遵循本文原則,配合壓測調優,即可構建生產級高併發網路服務。