Select 模型是 Windows 平台早期高併發網路程式的經典方案,雖然擴展性受限於 FD_SETSIZE (預設 64),但邏輯清晰、無需執行緒同步,適合中小型連線場景。本文提供完整的伺服器/用戶端原始碼與關鍵細節解析。
read_fds、write_fds、except_fdsselect() 阻塞直到任一集合中有 socket 就緒FD_ISSET() 處理對應事件#define DATA_BUFSIZE 8192
typedef struct _SOCKET_INFO {
SOCKET sock;
char buf[DATA_BUFSIZE];
WSABUF wsabuf; // {len, buf}
DWORD bytesRecv, bytesSent;
} SOCKET_INFO, *LPSOCKET_INFO;
LPSOCKET_INFO socketArray[FD_SETSIZE];
DWORD totalSockets = 0;
WSAStartup(MAKEWORD(2,2), &wsaData);
SOCKET listenSock = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
sockaddr_in addr = {AF_INET, htons(9990), INADDR_ANY};
bind(listenSock, (SOCKADDR*)&addr, sizeof(addr));
listen(listenSock, 5);
CreateSocketInfo(listenSock); // 加入陣列
while (1) {
FD_SET readSet = {0}, writeSet = {0};
FD_SET(listenSock, &readSet);
for (DWORD i=0; i<totalSockets; i++) {
SOCKET s = socketArray[i]->sock;
FD_SET(s, &readSet);
FD_SET(s, &writeSet); // 簡化:同時監讀寫
}
int ready = select(0, &readSet, &writeSet, NULL, NULL);
if (ready == SOCKET_ERROR) break;
for (DWORD i=0; i<totalSockets && ready>0; i++) {
LPSOCKET_INFO si = socketArray[i];
SOCKET s = si->sock;
if (FD_ISSET(s, &readSet)) {
ready--;
if (s == listenSock) { // 新連線
SOCKET client = accept(listenSock, NULL, NULL);
CreateSocketInfo(client);
} else { // 接收資料
si->wsabuf.len = DATA_BUFSIZE; si->wsabuf.buf = si->buf;
DWORD flags = 0, recvd;
int ret = WSARecv(s, &si->wsabuf, 1, &recvd, &flags, NULL, NULL);
if (ret == SOCKET_ERROR) {
if (WSAGetLastError() != WSAEWOULDBLOCK) FreeSocketInfo(i);
} else if (recvd == 0) {
FreeSocketInfo(i); // 客戶端關閉
} else {
si->bytesRecv = recvd; // 回聲伺服器:收到即準備發送
}
}
}
if (FD_ISSET(s, &writeSet) && si->bytesRecv > si->bytesSent) {
ready--;
si->wsabuf.buf = si->buf + si->bytesSent;
si->wsabuf.len = si->bytesRecv - si->bytesSent;
DWORD sent;
int ret = WSASend(s, &si->wsabuf, 1, &sent, 0, NULL, NULL);
if (ret == SOCKET_ERROR) {
if (WSAGetLastError() != WSAEWOULDBLOCK) FreeSocketInfo(i);
} else {
si->bytesSent += sent;
if (si->bytesSent == si->bytesRecv) // 發送完畢重置
si->bytesRecv = si->bytesSent = 0;
}
}
}
}
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
connect(sock, (SOCKADDR*)&srvAddr, sizeof(srvAddr));
char buf[64];
while (1) {
printf("輸入字串: "); fgets(buf, sizeof(buf), stdin);
buf[strcspn(buf, "\n")] = 0;
if (strcmp(buf, "quit") == 0) break;
send(sock, buf, strlen(buf), 0);
int n = recv(sock, buf, sizeof(buf)-1, 0);
if (n > 0) { buf[n] = 0; printf("回聲: %s\n", buf); }
}
closesocket(sock); WSACleanup();
| 議題 | 說明 | 解決方案 |
|---|---|---|
| FD_SETSIZE 限制 | 預設 64,超過即未定義行為 | 定義 FD_SETSIZE 1024 在 #include <winsock2.h> 之前,或改用 IOCP |
| 非阻塞模式 | 搭配 Select 必須設 ioctlsocket(FIONBIO) | 或使用 WSA_FLAG_OVERLAPPED 建立 socket 並處理 WSAEWOULDBLOCK |
| 緩衝區管理 | WSARecv/WSASend 需手動追蹤偏移與剩餘長度 | 封裝 WSABUF + bytesRecv/Sent 狀態機 |
| 優雅關閉 | 收到 0 位元組表示對端關閉 | 立即 FreeSocketInfo 釋放資源並從陣列移除 |
| select() 第一參數 | Windows 忽略此參數,填 0 即可 | 跨平台代碼保留 maxfd+1 |
select 後需遍歷陣列檢查 FD_ISSET,連線數大時 CPU 佔用高FreeSocketInfo 需前移後續元素WSAWaitForMultipleEvents教學用途 Select 模型極佳;生產環境新專案建議直接採用 IOCP 或成熟非同步框架。若維護既有 Select 代碼,務必加上 FD_SETSIZE 調大、完善錯誤碼處理、連線超時機制與壓力測試驗證。