首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C >> C 語言入門

C 語言實作 Select 模型 TCP 伺服器與用戶端:Windows 平台完整範例

     編輯:C 語言入門

Select 模型是 Windows 平台早期高併發網路程式的經典方案,雖然擴展性受限於 FD_SETSIZE (預設 64),但邏輯清晰、無需執行緒同步,適合中小型連線場景。本文提供完整的伺服器/用戶端原始碼與關鍵細節解析。

Select 模型核心機制

  • 維護三組 socket 集合:read_fds、write_fds、except_fds
  • select() 阻塞直到任一集合中有 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

效能與擴展性考量

  • O(N) 掃描:每次 select 後需遍歷陣列檢查 FD_ISSET,連線數大時 CPU 佔用高
  • 陣列移除 O(N):FreeSocketInfo 需前移後續元素
  • 替代方案:
    ▸ WSAEventSelect:事件驅動,配合 WSAWaitForMultipleEvents
    ▸ IOCP (I/O Completion Ports):Windows 最高效模型,支援萬級併發
    ▸ libuv / Boost.Asio:跨平台非同步網路庫

現代化建議

教學用途 Select 模型極佳;生產環境新專案建議直接採用 IOCP 或成熟非同步框架。若維護既有 Select 代碼,務必加上 FD_SETSIZE 調大、完善錯誤碼處理、連線超時機制與壓力測試驗證。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved