萬盛學電腦網

您现在的位置: 萬盛學電腦網 >> 程式設計 >  >> 網路程式設計 >> 程式語言綜合

C 語言 Socket 多執行緒伺服器:限制客戶端連線數完整實作

架構概覽

  • 主執行緒:socket → bind → listen → accept 迴圈,連線數計數、超限拒絕、建立工作者執行緒。
  • 工作者執行緒:每連線一執行緒,收發資料、處理業務邏輯、優雅關閉。
  • 同步原語:WaitForMultipleObjects 等待所有工作者結束。

Windows 平台完整程式碼 (Winsock)

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include 
#include 
#include 
#include  // _beginthreadex

#pragma comment(lib, "ws2_32.lib")

#define MAX_CLIENTS 3
#define BUF_SIZE 4096

// 執行緒參數結構
typedef struct {
    SOCKET clientSock;
    int clientId;
} ThreadArg;

unsigned __stdcall WorkerThread(void* arg)
{
    ThreadArg* ta = (ThreadArg*)arg;
    SOCKET sock = ta->clientSock;
    int id = ta->clientId;
    free(ta); // 參數結構體在執行緒內釋放

    char buf[BUF_SIZE];
    int recvLen;

    // 歡迎訊息
    const char* welcome = "Welcome! Type 'exit' to quit.\r\n";
    send(sock, welcome, (int)strlen(welcome), 0);
    printf("[SYS] Client #%d connected.\n", id);

    while (1) {
        recvLen = recv(sock, buf, BUF_SIZE - 1, 0);
        if (recvLen <= 0) break; // 斷線或錯誤

        buf[recvLen] = '\0';
        printf("[Client #%d] %s", id, buf);

        if (strncmp(buf, "exit", 4) == 0) {
            const char* bye = "Goodbye!\r\n";
            send(sock, bye, (int)strlen(bye), 0);
            break;
        }

        // 回聲
        send(sock, buf, recvLen, 0);
    }

    closesocket(sock);
    printf("[SYS] Client #%d disconnected.\n", id);
    _endthreadex(0);
    return 0;
}

int main()
{
    WSADATA wsa;
    if (WSAStartup(MAKEWORD(2,2), &wsa) != 0) {
        fprintf(stderr, "WSAStartup failed: %d\n", WSAGetLastError());
        return 1;
    }

    SOCKET listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (listenSock == INVALID_SOCKET) {
        fprintf(stderr, "socket failed: %d\n", WSAGetLastError());
        WSACleanup(); return 1;
    }

    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
    addr.sin_port = htons(9000);

    if (bind(listenSock, (struct sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) {
        fprintf(stderr, "bind failed: %d\n", WSAGetLastError());
        closesocket(listenSock); WSACleanup(); return 1;
    }

    if (listen(listenSock, SOMAXCONN) == SOCKET_ERROR) {
        fprintf(stderr, "listen failed: %d\n", WSAGetLastError());
        closesocket(listenSock); WSACleanup(); return 1;
    }

    printf("Server listening on 0.0.0.0:9000, max clients=%d\n", MAX_CLIENTS);

    HANDLE threads[MAX_CLIENTS] = {0};
    int clientCount = 0;

    while (1) {
        struct sockaddr_in clientAddr;
        int addrLen = sizeof(clientAddr);
        SOCKET clientSock = accept(listenSock, (struct sockaddr*)&clientAddr, &addrLen);
        if (clientSock == INVALID_SOCKET) {
            fprintf(stderr, "accept failed: %d\n", WSAGetLastError());
            continue;
        }

        if (clientCount >= MAX_CLIENTS) {
            const char* msg = "Server busy: max clients reached.\r\n";
            send(clientSock, msg, (int)strlen(msg), 0);
            closesocket(clientSock);
            printf("[SYS] Rejected connection (max clients).\n");
            continue;
        }

        ThreadArg* arg = (ThreadArg*)malloc(sizeof(ThreadArg));
        arg->clientSock = clientSock;
        arg->clientId = clientCount + 1;

        HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, WorkerThread, arg, 0, NULL);
        if (!hThread) {
            free(arg);
            closesocket(clientSock);
            fprintf(stderr, "_beginthreadex failed\n");
            continue;
        }

        threads[clientCount++] = hThread;
        printf("[SYS] Accepted client #%d, total=%d\n", arg->clientId, clientCount);
    }

    // 正常情況不會到達這裡(無限迴圈),關閉邏輯省略
    WaitForMultipleObjects(clientCount, threads, TRUE, INFINITE);
    for (int i = 0; i < clientCount; i++) CloseHandle(threads[i]);
    closesocket(listenSock);
    WSACleanup();
    return 0;
}

關鍵技術點解析

技術點說明
_beginthreadex vs CreateThread**必須用 _beginthreadex**,它會正確初始化 CRT 執行緒區域儲存 (TLS),避免記憶體洩漏與 errno 汙染。
參數傳遞動態配置 ThreadArg,執行緒入口第一時間 free,主執行緒不負責釋放。
連線數限制原子讀寫 clientCount(單執行緒 accept 迴圈天然安全),超限即拒絕並回應。
優雅關閉工作者執行緒收到 exit 或 recv==0 即關閉 Socket、結束執行緒。
資源清理主執行緒 WaitForMultipleObjects 等待所有工作者,再 CloseHandle、closesocket、WSACleanup。

Linux/POSIX 移植版 (pthread)

// gcc -pthread server.c -o server
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define MAX_CLIENTS 3
#define BUF_SIZE 4096

typedef struct { int sockfd; int id; } ThreadArg;

void* worker(void* arg) {
    ThreadArg* ta = (ThreadArg*)arg;
    int sock = ta->sockfd;
    int id = ta->id;
    free(ta);

    char buf[BUF_SIZE];
    ssize_t n;
    const char* welcome = "Welcome! Type 'exit' to quit.\n";
    write(sock, welcome, strlen(welcome));
    printf("[SYS] Client #%d connected.\n", id);

    while ((n = read(sock, buf, BUF_SIZE - 1)) > 0) {
        buf[n] = '\0';
        printf("[Client #%d] %s", id, buf);
        if (strncmp(buf, "exit", 4) == 0) {
            write(sock, "Goodbye!\n", 9);
            break;
        }
        write(sock, buf, n); // echo
    }
    close(sock);
    printf("[SYS] Client #%d disconnected.\n", id);
    return NULL;
}

int main() {
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(9000);
    bind(listenfd, (struct sockaddr*)&addr, sizeof(addr));
    listen(listenfd, SOMAXCONN);
    printf("Server listening on 0.0.0.0:9000, max clients=%d\n", MAX_CLIENTS);

    pthread_t threads[MAX_CLIENTS];
    int count = 0;

    while (1) {
        struct sockaddr_in cli;
        socklen_t len = sizeof(cli);
        int connfd = accept(listenfd, (struct sockaddr*)&cli, &len);
        if (connfd < 0) { perror("accept"); continue; }

        if (count >= MAX_CLIENTS) {
            write(connfd, "Server busy\n", 12);
            close(connfd);
            continue;
        }

        ThreadArg* arg = malloc(sizeof(ThreadArg));
        arg->sockfd = connfd;
        arg->id = ++count;
        pthread_create(&threads[count-1], NULL, worker, arg);
        pthread_detach(threads[count-1]); // 自動回收,簡化主執行緒
    }
    // 不會執行到這裡
    close(listenfd);
    return 0;
}

生產環境增強建議

  • 執行緒池:替換逐連線建立執行緒,使用 IOCP (Windows) / epoll + 固定執行緒池 (Linux) 解決 C10K 問題。
  • 心跳/逾時:setsockopt(SO_RCVTIMEO) 或應用層心跳檢測死連線。
  • 訊號處理:捕捉 SIGINT/SIGTERM,優雅關閉監聽 Socket、通知工作者退出。
  • 日誌與監控:結構化日誌、連線計數指標、錯誤率告警。

結語

「每連線一執行緒 + 連線數上限」是經典並發模型的入門範本。掌握 _beginthreadex/pthread_create 正確用法、參數傳遞所有權轉移、Socket 資源確定性釋放,即可寫出穩定的 TCP 伺服器雛形。高併發場景請進階至 **IOCP / epoll + 非阻塞 I/O + 執行緒池** 架構。

copyright © 萬盛學電腦網 all rights reserved