在搶佔式多任務 OS (Windows NT/2000/XP+) 中,採用同步阻塞 + 多執行緒 模式,邏輯清晰且效能可媲美非同步模型。本文針對同步 Winsock 程式設計,精選四大關鍵優化技巧:關閉 Nagle 算法降低小封包延遲、調整 TCP 區段大小與收發緩衝區、縮短連線超時阻塞、實現可靠斷線偵測。
Nagle 算法預設開啟,將小封包緩衝等待 ACK 或累積至 MSS,導致即時互動 (鍵盤輸入、遊戲指令、高頻感測器) 延遲 40~200ms。
// 建立 socket 後、connect 前設定
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
int nodelay = 1; // 1=關閉 Nagle, 0=開啟
int err = setsockopt(s, IPPROTO_TCP, TCP_NODELAY,
(const char*)&nodelay, sizeof(nodelay));
if (err == SOCKET_ERROR) {
int e = WSAGetLastError();
TRACE(_T("setsockopt TCP_NODELAY 失敗: %d\n"), e);
}
| 參數 | 預設值 | 調整建議 | 影響 |
|---|---|---|---|
| MSS (TCP 區段大小) | 1460 bytes (以太網 MTU 1500 - IP/TCP 標頭) | 一般勿修改,Microsoft 認為 1460 為最佳效率 | 單次 send 超過 MSS 會自動分片,接收端需自行組裝訊息邊界 |
| 發送緩衝區 (SO_SNDBUF) | 8192 bytes | 高頻寫入可增至 64KB~256KB | 減少 send 阻塞機率,但佔用核心非分頁池 |
| 接收緩衝區 (SO_RCVBUF) | 8192 bytes | 高頻讀取/延遲讀取可增至 64KB~256KB | 容許更多亂序封包重組,減少視窗滑動頻率 |
// 調整緩衝區範例
int sndbuf = 65536, rcvbuf = 65536;
setsockopt(s, SOL_SOCKET, SO_SNDBUF, (char*)&sndbuf, sizeof(sndbuf));
setsockopt(s, SOL_SOCKET, SO_RCVBUF, (char*)&rcvbuf, sizeof(rcvbuf));
// 讀取實際生效值 (系統可能雙倍/限制)
int actual; int len = sizeof(actual);
getsockopt(s, SOL_SOCKET, SO_SNDBUF, (char*)&actual, &len);
TCP_NODELAY=1)send ≤ 1400 bytes (留 60 bytes 給 IP 選項/擴展)預設 connect 同步阻塞約 20 秒 (TCP SYN 重傳機制)。若伺服器離線/防火牆封鎖,UI 將凍結 20 秒。
sockaddr_in addr = {AF_INET, htons(port), inet_addr("192.168.1.100")};
HOSTENT* pHost = gethostbyaddr((const char*)&addr.sin_addr, 4, AF_INET);
if (!pHost) {
int err = WSAGetLastError();
// 無法解析/路由不可達,直接報錯,不呼叫 connect
return FALSE;
}
// 路由可達,再呼叫 connect (仍可能被防火牆封鎖)
// 1. 設定非阻塞
u_long mode = 1; ioctlsocket(s, FIONBIO, &mode);
// 2. 發起連線 (立即返回 WSAEWOULDBLOCK)
int rc = connect(s, (sockaddr*)&addr, sizeof(addr));
if (rc == SOCKET_ERROR && WSAGetLastError() == WSAEWOULDBLOCK) {
// 3. select 等待可寫 (連線完成) 或例外
fd_set writefds, exceptfds;
FD_ZERO(&writefds); FD_SET(s, &writefds);
FD_ZERO(&exceptfds); FD_SET(s, &exceptfds);
timeval tv = {5, 0}; // 5 秒超時
int sel = select(0, NULL, &writefds, &exceptfds, &tv);
if (sel > 0 && FD_ISSET(s, &writefds)) {
// 4. 驗證連線真正成功 (getsockopt SO_ERROR)
int err = 0; len = sizeof(err);
getsockopt(s, SOL_SOCKET, SO_ERROR, (char*)&err, &len);
if (err == 0) { /* 連線成功 */ }
}
}
// 5. 恢復阻塞模式 (可選)
mode = 0; ioctlsocket(s, FIONBIO, &mode);
使用 IcmpSendEcho (IPHLPAPI) 或原始套接字發送 ICMP Echo Request,但受防火牆/路由器封鎖 ICMP 影響,不可作為唯一依據。
TCP 連線若無資料傳輸,永遠不會自動偵測斷線 (網線拔除、對方強制關機、路由器重啟)。
// Windows 預設:2 小時無資料才發送第一次 Keep-Alive,間隔 1 秒,重試 10 次
// 可透過登錄修改全域:HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters
// KeepAliveTime (ms), KeepAliveInterval (ms), TcpMaxDataRetransmissions
// 或針對單一 socket 設定 (Windows SIO_KEEPALIVE_VALS)
struct tcp_keepalive ka = {1, 30000, 5000}; // 啟用, 30秒無資料發第一次, 間隔5秒
DWORD dwBytes;
WSAIoctl(s, SIO_KEEPALIVE_VALS, &ka, sizeof(ka), NULL, 0, &dwBytes, NULL, NULL);
// 發送端:每 10 秒發送心跳封包
struct Heartbeat { DWORD magic=0xA5A5A5A5; DWORD seq++; };
send(s, (char*)&hb, sizeof(hb), 0);
// 接收端:設定接收超時
int timeout = 35000; // 35 秒無資料視為斷線
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout));
// recv 返回 WSAETIMEDOUT 即判定斷線
int n = recv(s, buf, len, 0);
if (n == SOCKET_ERROR && WSAGetLastError() == WSAETIMEDOUT) {
// 處理斷線重連
}
| 優化目標 | 關鍵 API/選項 | 典型數值 | 適用場景 |
|---|---|---|---|
| 降低小封包延遲 | TCP_NODELAY | 1 (關閉) | 即時指令、遊戲、高頻感測器 |
| 減少 send 阻塞 | SO_SNDBUF | 65536~262144 | 高速寫入、非同步發送 |
| 容許亂序重組 | SO_RCVBUF | 65536~262144 | 高延遲鏈路、無線網路 |
| 縮短連線超時 | 非阻塞 connect + select | 3~5 秒 | UI 回應關鍵、行動網路 |
| 偵測靜默斷線 | 應用層心跳 + SO_RCVTIMEO | 心跳 10s, 超時 35s | 長連線、行動/無線環境 |