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

Linux 網路程式設計:I/O 多路復用 select() 完整實戰指南

     編輯:關於C

為什麼需要 I/O 多路復用?

傳統阻塞 I/O 模型下,單執行緒只能處理單一連線;多進程/多執行緒雖能並發,但上下文切換與記憶體開銷巨大。I/O 多路復用 讓單一執行緒同時監控多個檔案描述符,僅在有事件(可讀/可寫/異常)發生時才喚醒處理,極大降低系統開銷,是高併發伺服器(Nginx、Redis 早期版本)的核心基石。

select() 核心 API

#include 
#include 

int select(int maxfdp1,
           fd_set *readset,
           fd_set *writeset,
           fd_set *exceptset,
           struct timeval *timeout);
// 返回:就緒描述符總數;0=超時;-1=錯誤

關鍵參數詳解

  • maxfdp1:最大描述符值 + 1(內核輪詢範圍 0 ~ maxfdp1-1)。
  • readset / writeset / exceptset:輸入/輸出傳遞關注集合,返回時被修改為就緒集合。不關注者傳 NULL。
  • timeout:
    • NULL:永久阻塞
    • tv_sec=0, tv_usec=0:非阻塞輪詢
    • 指定時間:定時阻塞

fd_set 操作巨集

FD_ZERO(fd_set *set);      // 清空
FD_SET(int fd, fd_set *set); // 加入
FD_CLR(int fd, fd_set *set); // 移除
FD_ISSET(int fd, fd_set *set); // 測試是否就緒

完整 TCP Echo Server 實作

支援多客戶端並發,單執行緒處理所有連線。

Server 端

// select_server.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define MAXLINE 1024
#define LISTENQ 5
#define SERV_PORT 8787

int main(void) {
    int listenfd, connfd, maxfd, maxi, nready;
    int client[FD_SETSIZE];
    fd_set rset, allset;
    char buf[MAXLINE];
    socklen_t clilen;
    struct sockaddr_in cliaddr, servaddr;

    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);
    bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    listen(listenfd, LISTENQ);

    maxfd = listenfd;
    maxi = -1;
    for (int i = 0; i < FD_SETSIZE; i++) client[i] = -1;
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);

    for (;;) {
        rset = allset; // select 會修改集合,每次需重置
        nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
        if (nready < 0) {
            if (errno == EINTR) continue;
            perror("select error");
            exit(1);
        }

        // 新連線
        if (FD_ISSET(listenfd, &rset)) {
            clilen = sizeof(cliaddr);
            connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
            printf("新連線: %s:%d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));

            int i;
            for (i = 0; i < FD_SETSIZE; i++)
                if (client[i] < 0) { client[i] = connfd; break; }
            if (i == FD_SETSIZE) { fprintf(stderr, "客戶端過多\n"); exit(1); }

            FD_SET(connfd, &allset);
            if (connfd > maxfd) maxfd = connfd;
            if (i > maxi) maxi = i;
            if (--nready <= 0) continue;
        }

        // 處理資料
        for (int i = 0; i <= maxi; i++) {
            int sockfd = client[i];
            if (sockfd < 0) continue;
            if (FD_ISSET(sockfd, &rset)) {
                int n = read(sockfd, buf, MAXLINE);
                if (n == 0) { // 客戶端關閉
                    close(sockfd);
                    FD_CLR(sockfd, &allset);
                    client[i] = -1;
                } else {
                    write(STDOUT_FILENO, buf, n); // 印到螢幕
                    write(sockfd, buf, n);        // 回射給客戶端
                }
                if (--nready <= 0) break;
            }
        }
    }
}

Client 端

// select_client.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define MAXLINE 1024
#define SERV_PORT 8787

int main(void) {
    int sockfd, maxfdp1;
    fd_set rset;
    char sendline[MAXLINE], recvline[MAXLINE];
    struct sockaddr_in servaddr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    FD_ZERO(&rset);
    for (;;) {
        FD_SET(STDIN_FILENO, &rset);
        FD_SET(sockfd, &rset);
        maxfdp1 = (STDIN_FILENO > sockfd ? STDIN_FILENO : sockfd) + 1;
        select(maxfdp1, &rset, NULL, NULL, NULL);

        if (FD_ISSET(sockfd, &rset)) {
            int n = read(sockfd, recvline, MAXLINE);
            if (n == 0) { printf("Server 關閉\n"); exit(0); }
            write(STDOUT_FILENO, recvline, n);
        }
        if (FD_ISSET(STDIN_FILENO, &rset)) {
            int n = read(STDIN_FILENO, sendline, MAXLINE);
            if (n == 0) { FD_CLR(STDIN_FILENO, &rset); continue; }
            write(sockfd, sendline, n);
        }
    }
}

編譯與測試

gcc select_server.c -o server
gcc select_client.c -o client
./server &          # 背景啟動
./client            # 開啟多個終端機測試

select() 的侷限與演進

缺點影響現代替代方案
FD_SETSIZE 限制(通常 1024)無法支援超大規模連線poll()、epoll (Linux)、kqueue (BSD/macOS)、IOCP (Windows)
每次呼叫需全量複製 fd_setO(N) 開銷,N 大時效能急劇下降epoll 等僅傳遞就緒列表,O(1)
不支援邊緣觸發需自行管理緩衝區避免重複通知epoll EPOLLET 模式

總結

select() 是理解 I/O 多路復用概念的最佳切入點,適合中小型並發場景(<1000 連線)。生產環境高併發建議直接使用 epoll (Linux) 或跨平台庫 libevent/libuv,但掌握 select() 的 FD 集合操作與事件迴圈邏輯,是通往高效能網路程式設計的必經之路。

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