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

Unix/Linux 多進程網路程式設計實戰:從 Socket API 到並發伺服器架構

     編輯:關於C語言

前言:為什麼選擇多進程模型?

在 Unix/Linux 系統程式設計中,多進程 是實現並發網路服務的經典模型。相較於多線程,進程擁有獨立地址空間,天然隔離故障(單進程崩潰不影響其他連接),且無需複雜的鎖同步機制。雖然 fork() 複製頁表有開銷,但現代作業系統採用 Copy-on-Write (COW) 機制,實際開銷極低。本文通過一個完整的「單向回應伺服器」範例,系統講解 Socket 程式設計標準流程、錯誤處理最佳實踐、進程管理與訊號安全 I/O。

標準 TCP 伺服器啟動六部曲

  1. socket():建立通訊端點,回傳檔案描述符 (fd)
  2. bind():綁定 IP 地址與埠號
  3. listen():轉為被動監聽狀態,設定連接佇列長度
  4. accept():從已完成連接佇列取出新連接,回傳新 fd
  5. fork():子進程處理 I/O,父進程繼續 accept()
  6. read/write:子進程進行資料收發

伺服器端完整原始碼(詳細中文註解)


/*********************************************************************
 *  Unix 多進程 TCP 回應伺服器
 *  編譯: gcc -o server server.c
 *  執行: ./server <IP> <PORT>  (例: ./server 0.0.0.0 8888)
 *  參考: Stevens "UNIX Network Programming" / "Advanced Programming in the UNIX Environment"
 *********************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>

#define LISTENQ 5
#define BUFFER_SIZE 1024

/* 子進程處理函數:回應歡迎訊息並回傳收到的資料 */
int child_process(int connfd) {
    ssize_t written;
    char *welcome = "Hello, welcome to my echo server!\n";
    
    /* 安全寫入:處理 EINTR 訊號中斷 */
again_write:
    if ((written = write(connfd, welcome, strlen(welcome))) < 0) {
        if (errno == EINTR) goto again_write;  // 被訊號中斷,重試
        perror("write failed");
        return -1;
    }
    printf("Sent welcome message (%zd bytes)\n", written);

    char buffer[BUFFER_SIZE];
    ssize_t n;
    
    /* 迴圈讀取客戶端資料並回傳 */
    for (;;) {
again_read:
        if ((n = read(connfd, buffer, BUFFER_SIZE)) < 0) {
            if (errno == EINTR) goto again_read;
            perror("read failed");
            break;
        }
        if (n == 0) {  // 客戶端關閉連接 (EOF)
            printf("Client closed connection\n");
            break;
        }
        
        buffer[n] = '\0';  // 確保字串結束
        
        /* 取得對端地址資訊用於日誌 */
        struct sockaddr_in peer_addr;
        socklen_t peer_len = sizeof(peer_addr);
        if (getpeername(connfd, (struct sockaddr*)&peer_addr, &peer_len) == 0) {
            printf("Received from %s:%d: %s", 
                   inet_ntoa(peer_addr.sin_addr), ntohs(peer_addr.sin_port), buffer);
        }
        
        /* 回傳資料 (Echo) */
        ssize_t total_written = 0;
        while (total_written < n) {
            written = write(connfd, buffer + total_written, n - total_written);
            if (written <= 0) {
                if (errno == EINTR) continue;
                perror("write echo failed");
                goto cleanup;
            }
            total_written += written;
        }
    }

cleanup:
    close(connfd);
    return 0;
}

/* 訊號處理:回收殭屍進程 */
void sig_chld(int signo) {
    pid_t pid;
    int stat;
    while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) {
        printf("Child %d terminated\n", pid);
    }
    return;
}

int main(int argc, char **argv) {
    if (argc < 3) {
        fprintf(stderr, "Usage: %s <IP> <PORT>\n", argv[0]);
        exit(1);
    }

    /* 註冊 SIGCHLD 處理器,避免殭屍進程 */
    signal(SIGCHLD, sig_chld);

    /* Step 1: socket() */
    int listenfd = socket(PF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) { perror("socket failed"); exit(1); }

    /* Step 2: bind() */
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = PF_INET;
    servaddr.sin_port = htons(atoi(argv[2]));
    servaddr.sin_addr.s_addr = inet_addr(argv[1]);  // 支援 "0.0.0.0" 綁定所有介面
    
    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed"); close(listenfd); exit(1);
    }

    /* Step 3: listen() */
    if (listen(listenfd, LISTENQ) < 0) {
        perror("listen failed"); close(listenfd); exit(1);
    }
    printf("Server listening on %s:%s\n", argv[1], argv[2]);

    /* Step 4: 主迴圈 accept() + fork() */
    struct sockaddr_in cliaddr;
    socklen_t clilen = sizeof(cliaddr);
    int connfd;
    int connect_count = 0;

    for (;;) {
        connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen);
        if (connfd < 0) {
            if (errno == EINTR) continue;  // 被訊號中斷 (如 SIGCHLD),重試 accept
            perror("accept failed");
            continue;  // 不退出主進程,嘗試繼續服務
        }
        
        printf("New connection %d from %s:%d\n", 
               ++connect_count, inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));

        /* Step 5: fork() 子進程處理連接 */
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork failed");
            close(connfd);
        } else if (pid == 0) {  // 子進程
            close(listenfd);  // 子進程不需監聽 fd
            child_process(connfd);
            exit(0);  // 子進程結束
        } else {  // 父進程
            close(connfd);  // 父進程不需已連接 fd
            // 父進程繼續下一輪 accept()
        }
    }
    
    close(listenfd);
    return 0;
}

關鍵技術深度解析

1. 訊號安全 I/O 與 EINTR 處理

當進程處理訊號(如 SIGCHLD)時,阻塞的系統呼叫(read/write/accept)會被中斷,回傳 -1 並設定 errno = EINTR。必須重試,否則會導致資料遺失或連接異常斷開。範例採用 goto again 模式,清晰可靠。

2. 殭屍進程防範:SIGCHLD + waitpid(WNOHANG)

子進程結束後若父進程未 wait(),會變成殭屍進程佔用 PID。解決方案:

  • 註冊 SIGCHLD 處理器
  • 處理器中循環呼叫 waitpid(-1, &stat, WNOHANG) 非阻塞回收所有已結束子進程
  • 注意: 訊號處理器中只能呼叫 Async-signal-safe functions,printf 實際不安全,生產環境應用 write(STDOUT_FILENO, ...) 或單純回收不列印

3. 檔案描述符管理:父子進程必須關閉不需要的 fd

4. 位元組序轉換:htons/ntohs/inet_addr/inet_ntoa

  • 埠號:主機序 ↔ 網路序 (大端序) 必須轉換
  • IP 地址:inet_addr 字串 → 網路序整數;inet_ntoa 網路序 → 字串(非執行緒安全,現代建議用 inet_ntop)

客戶端程式碼(精簡版)


/*********************************************************************
 *  簡易 TCP 客戶端:連接伺服器、接收歡迎訊息、發送標準輸入資料
 *********************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>

#define BUFFER_SIZE 1024

int main(int argc, char **argv) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <IP> <PORT>\n", argv[0]);
        exit(1);
    }

    int sockfd = socket(PF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) { perror("socket"); exit(1); }

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = PF_INET;
    servaddr.sin_port = htons(atoi(argv[2]));
    servaddr.sin_addr.s_addr = inet_addr(argv[1]);

    if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
        perror("connect"); close(sockfd); exit(1);
    }
    printf("Connected to %s:%s\n", argv[1], argv[2]);

    char buffer[BUFFER_SIZE];
    ssize_t n, written;

    /* 非阻塞處理標準輸入與網路 I/O 的簡易方式:雙迴圈 (實務建議用 select/poll/epoll) */
    for (;;) {
        /* 讀取伺服器資料 */
        if ((n = read(sockfd, buffer, BUFFER_SIZE)) > 0) {
            buffer[n] = '\0';
            fputs(buffer, stdout);
            fflush(stdout);
        } else if (n == 0) {
            printf("Server closed connection\n");
            break;
        } else if (errno != EINTR) {
            perror("read"); break;
        }

        /* 從標準輸入讀取並發送 */
        if (fgets(buffer, BUFFER_SIZE, stdin)) {
            size_t len = strlen(buffer);
again_write:
            if ((written = write(sockfd, buffer, len)) < 0) {
                if (errno == EINTR) goto again_write;
                perror("write"); break;
            }
        }
    }
    close(sockfd);
    return 0;
}

編譯與測試


# 編譯
gcc -o server server.c
gcc -o client client.c

# 啟動伺服器 (綁定所有介面 8888 埠)
./server 0.0.0.0 8888

# 另開終端機啟動客戶端
./client 127.0.0.1 8888

# 測試多客戶端並發
./client 127.0.0.1 8888 &
./client 127.0.0.1 8888 &
./client 127.0.0.1 8888 &

進階優化方向

進程關閉保留原因
父進程connfd (已連接 fd)listenfd父進程只負責 accept,不處理 I/O;若不關閉會洩漏 fd,導致連接無法正常關閉 (FIN 不會發送)
子進程listenfdconnfd子進程只負責當前連接 I/O;保留 listenfd 會導致多進程競爭 accept (thundering herd),且阻止埠釋放

總結

多進程模型雖經典,但理解其檔案描述符語意、訊號處理、進程隔離優缺點,是掌握 Unix/Linux 系統程式設計的基石。現代高效能伺服器多採用 事件驅動 + 線程池 (如 Nginx、Redis) 或 io_uring (Linux 5.1+),但核心原理——socket/bind/listen/accept/read/write、錯誤碼處理、資源管理——始終貫通。建議讀者在掌握本範例後,嘗試用 select/poll/epoll 重寫為單進程多路複用版本,體會不同並發模型的設計哲學。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
問題解決方案
fork() 大量進程開銷預先派生進程池 (Pre-forking) / 線程池 / epoll/kqueue/IOCP 事件驅動
驚群效應單進程 accept + 分發 / SO_REUSEPORT (Linux 3.9+)
阻塞 I/O 限制併發非阻塞 I/O + I/O 多路複用 / 非同步 I/O (io_uring)
訊號處理器不安全signalfd + epoll 統一事件迴圈 / pthread_sigmask 專用訊號處理線程
缺乏優雅關閉捕捉 SIGTERM/SIGINT,關閉 listenfd、通知子進程退出、等待回收