UNIX下C语言编程与实践63-UNIX 并发 Socket 编程:非阻塞套接字与轮询模型

在 UNIX 环境下的 Socket 编程中,"并发"是处理多客户端连接的核心需求。传统阻塞套接字模型中,一个进程/线程只能处理一个连接,效率极低;而非阻塞套接字结合轮询模型,通过"非阻塞+循环查询"的方式,可让单个进程同时处理多个套接字事件(如连接请求、数据接收),是实现轻量级并发的基础方案。本文的核心内容,详细解析非阻塞套接字的设置方法、轮询模型的实现逻辑,结合实例演示其应用,并深入分析该模型的优缺点与优化方向。

一、核心概念:阻塞与非阻塞套接字

在讲解非阻塞模型前,需先明确"阻塞套接字"与"非阻塞套接字"的本质区别------二者的核心差异在于套接字函数调用时的等待行为

1.1 阻塞套接字(默认模式)

默认情况下,UNIX 套接字为"阻塞模式":当调用 accept(接收连接)、recv(接收数据)、connect(发起连接)等函数时,若"期望的事件未发生"(如无客户端连接、无数据到达),进程会一直阻塞在该函数调用处,无法执行其他操作。

局限性 :阻塞模式下,单个进程只能处理一个套接字事件------若进程阻塞在 accept 等待连接,就无法同时接收已连接客户端的数据;若阻塞在 recv 等待数据,就无法处理新的连接请求,完全不具备并发能力。

1.2 非阻塞套接字

非阻塞套接字通过修改套接字描述符的属性,改变了函数调用的等待行为:无论"期望的事件是否发生",套接字函数(如 acceptrecv)都会立即返回 。若事件发生(如存在新连接、有数据到达),函数返回正常结果;若事件未发生,函数返回 -1,并通过 errno 告知"无事件需重试"(而非真正的错误)。

核心价值:非阻塞套接字让进程摆脱"单一阻塞点"的限制------通过循环轮询多个非阻塞套接字,可在单个进程内并发处理"接收连接""读取数据"等多种事件,是实现轻量级并发的基础。

二、非阻塞套接字的设置:fcntl 函数

UNIX 中通过 fcntl 函数(File Control)修改套接字描述符的属性,添加 O_NONBLOCK 标志即可将套接字设为非阻塞模式。这是实现非阻塞轮询模型的第一步。

2.1 fcntl 函数原型与核心参数

复制代码
#include <fcntl.h>

// 获取或设置文件描述符(含套接字描述符)的属性
int fcntl(int fd, int cmd, ... /* arg */ );

在设置非阻塞套接字时,核心参数含义如下:

参数 功能说明 非阻塞设置中的取值
fd 需要操作的文件描述符,此处为套接字描述符(如 listen_sockconn_sock 侦听套接字或连接套接字的描述符
cmd 操作命令,用于指定"获取属性"或"设置属性" F_GETFL(获取当前属性标志)、F_SETFL(设置新属性标志)
arg 可选参数,随 cmd 变化------F_GETFL 时无需该参数;F_SETFL 时为新的属性标志 添加 O_NONBLOCK 标志后的属性值

2.2 设置非阻塞模式的三步流程

设置非阻塞套接字需遵循"获取-修改-设置"的三步流程,避免直接覆盖原有属性标志(如保留套接字的其他默认属性)。具体步骤如下:

  1. 步骤1:获取当前套接字的属性标志
    调用 fcntl(fd, F_GETFL) 获取当前套接字的属性标志(返回值为当前标志组合),若失败则返回 -1
    示例代码:

    复制代码
    int flags = fcntl(sockfd, F_GETFL, 0);
    if (flags == -1) {
        perror("fcntl(F_GETFL) failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
  2. 步骤2:添加 O_NONBLOCK 标志
    通过"按位或"操作(|)将 O_NONBLOCK 标志添加到当前属性中,确保不破坏原有标志(如套接字的读写权限标志)。
    示例代码:

    复制代码
    flags |= O_NONBLOCK;  // 为属性标志添加非阻塞特性
  3. 步骤3:设置新的属性标志
    调用 fcntl(fd, F_SETFL, flags) 将修改后的属性标志写入套接字,完成非阻塞模式设置。
    示例代码:

    复制代码
    if (fcntl(sockfd, F_SETFL, flags) == -1) {
        perror("fcntl(F_SETFL) failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    printf("套接字 %d 已设置为非阻塞模式\n", sockfd);

2.3 封装非阻塞设置函数(

为了代码复用,常将"非阻塞设置"封装为独立函数。例如:

复制代码
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

// 函数功能:将指定套接字设为非阻塞模式
int set_nonblock(int sockfd) {
    int flags = fcntl(sockfd, F_GETFL, 0);
    if (flags == -1) {
        perror("fcntl(F_GETFL) failed");
        return -1;
    }
    // 添加 O_NONBLOCK 标志,保留原有标志
    if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) {
        perror("fcntl(F_SETFL) failed");
        return -1;
    }
    return 0;
}

注意 :非阻塞模式是"套接字描述符级别的属性"------若通过 fork 创建子进程,子进程继承的套接字会保持与父进程相同的非阻塞属性;若关闭套接字后重新创建,需重新设置非阻塞模式。

三、非阻塞轮询模型的实现:循环处理多套接字

非阻塞轮询模型的核心逻辑是:通过无限循环,周期性查询多个非阻塞套接字的状态,若有事件发生则处理,无事件则继续循环。该模型可让单个进程同时处理"侦听新连接"和"接收已连接客户端数据"两类事件。

3.1 模型核心流程

以 TCP 服务器为例,非阻塞轮询模型的完整流程如下:

  1. 创建侦听套接字(socket)、绑定地址(bind)、设置侦听(listen);
  2. 将侦听套接字设为非阻塞模式(通过 set_nonblock 函数);
  3. 初始化"已连接套接字列表",用于存储所有与客户端建立的非阻塞连接套接字;
  4. 进入无限循环,执行以下操作:
    • 处理新连接 :调用非阻塞 accept,若有新客户端连接则创建连接套接字,设为非阻塞并加入"已连接列表";
    • 处理数据接收 :遍历"已连接列表",对每个连接套接字调用非阻塞 recv,若有数据则读取并处理,若客户端关闭连接则移除该套接字;
    • (可选)添加延时,降低 CPU 占用(优化轮询频率)。

3.2 关键:非阻塞函数的返回值与 errno 处理

特别强调,非阻塞模式下,套接字函数的返回值和 errno 是判断"事件是否发生"的核心依据------返回 -1 不一定是错误,需结合 errno 进一步判断。常见场景如下:

函数调用 返回值 errno 取值 含义与处理方式
accept(listen_sock, ...)(非阻塞) >0 - 成功接收新连接,返回连接套接字描述符------需将该套接字设为非阻塞并加入管理列表
accept(listen_sock, ...)(非阻塞) -1 EAGAINEWOULDBLOCK 无新连接请求(正常情况)------无需处理,继续轮询
accept(listen_sock, ...)(非阻塞) -1 其他值(如 EBADFEINTR 函数调用错误(如套接字无效、被信号中断)------需处理错误(如关闭套接字、重试)
recv(conn_sock, ...)(非阻塞) >0 - 成功接收数据,返回接收字节数------处理数据(如打印、解析)
recv(conn_sock, ...)(非阻塞) 0 - 客户端正常关闭连接(发送 FIN 数据包)------关闭连接套接字,从管理列表中移除
recv(conn_sock, ...)(非阻塞) -1 EAGAINEWOULDBLOCK 无数据可接收(正常情况)------无需处理,继续轮询
recv(conn_sock, ...)(非阻塞) -1 其他值(如 ECONNRESET 连接异常(如客户端强制关闭)------关闭套接字,从列表中移除

重要提示EAGAINEWOULDBLOCK 在多数 UNIX 系统中是同义词(值相同),均表示"非阻塞模式下无事件发生,需再次尝试",处理逻辑完全一致。

3.3 完整实例:非阻塞轮询服务器

基于非阻塞程序设计思路,实现一个 TCP 服务器:通过非阻塞侦听套接字接收多客户端连接,通过轮询处理每个客户端的数据发送请求。

3.3.1 实例代码
复制代码
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

#define PORT 9001          // 侦听端口
#define MAX_CONN 10        // 最大并发连接数
#define BUF_SIZE 1024      // 数据缓冲区大小
#define POLL_DELAY 100000  // 轮询延时(微秒),降低CPU占用

// 错误处理宏
#define ERROR_CHECK(ret, msg) \
    if (ret == -1) { \
        perror(msg); \
        exit(EXIT_FAILURE); \
    }

// 将套接字设为非阻塞模式
int set_nonblock(int sockfd) {
    int flags = fcntl(sockfd, F_GETFL, 0);
    ERROR_CHECK(flags, "fcntl(F_GETFL) failed");
    if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) {
        perror("fcntl(F_SETFL) failed");
        return -1;
    }
    return 0;
}

int main() {
    int listen_sock;                  // 侦听套接字
    struct sockaddr_in serv_addr;     // 服务器地址结构
    int conn_socks[MAX_CONN] = {0};   // 已连接套接字列表(初始化为0,0表示无效)
    int conn_count = 0;               // 当前已连接客户端数量
    char buf[BUF_SIZE];               // 数据接收缓冲区

    // 步骤1:创建侦听套接字
    listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    ERROR_CHECK(listen_sock, "socket() failed");
    printf("创建侦听套接字成功,listen_sock = %d\n", listen_sock);

    // 步骤2:设置端口复用(避免TIME_WAIT状态占用端口)
    int reuse = 1;
    ERROR_CHECK(setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)), 
                "setsockopt(SO_REUSEADDR) failed");

    // 步骤3:绑定地址与端口
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 监听所有网卡
    serv_addr.sin_port = htons(PORT);
    ERROR_CHECK(bind(listen_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)), 
                "bind() failed");

    // 步骤4:设置侦听
    ERROR_CHECK(listen(listen_sock, MAX_CONN), "listen() failed");
    printf("服务器已启动,监听端口 %d(最大并发连接数:%d)\n", PORT, MAX_CONN);

    // 步骤5:将侦听套接字设为非阻塞模式
    ERROR_CHECK(set_nonblock(listen_sock), "set_nonblock(listen_sock) failed");

    // 步骤6:进入非阻塞轮询循环
    while (1) {
        // -------------------------- 阶段1:处理新连接请求 --------------------------
        struct sockaddr_in client_addr;
        socklen_t client_addr_len = sizeof(client_addr);
        // 非阻塞accept:无新连接则立即返回-1,errno设为EAGAIN/EWOULDBLOCK
        int new_conn = accept(listen_sock, (struct sockaddr*)&client_addr, &client_addr_len);
        if (new_conn > 0) {
            // 检查是否超过最大连接数
            if (conn_count >= MAX_CONN) {
                printf("警告:已达最大连接数(%d),拒绝新客户端连接\n", MAX_CONN);
                close(new_conn);
                continue;
            }
            // 将新连接套接字设为非阻塞模式
            if (set_nonblock(new_conn) == -1) {
                close(new_conn);
                continue;
            }
            // 将新连接加入列表
            conn_socks[conn_count++] = new_conn;
            printf("新客户端连接:IP=%s, Port=%d, conn_sock=%d(当前连接数:%d)\n", 
                   inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), 
                   new_conn, conn_count);
        } else if (new_conn == -1 && errno != EAGAIN && errno != EWOULDBLOCK) {
            // 排除"无新连接"的正常情况,处理其他错误
            perror("accept() failed (non-EAGAIN)");
        }

        // -------------------------- 阶段2:处理已连接客户端数据 --------------------------
        for (int i = 0; i < conn_count; i++) {
            int curr_sock = conn_socks[i];
            if (curr_sock == 0) continue;  // 跳过已关闭的无效套接字

            // 非阻塞recv:无数据则立即返回-1,errno设为EAGAIN/EWOULDBLOCK
            memset(buf, 0, BUF_SIZE);
            ssize_t recv_len = recv(curr_sock, buf, BUF_SIZE - 1, 0);
            if (recv_len > 0) {
                // 成功接收数据,打印并回复
                printf("从 conn_sock=%d 接收数据:%s(长度:%zd 字节)\n", 
                       curr_sock, buf, recv_len);
                // 简单回复客户端(非阻塞send,此处省略错误处理)
                send(curr_sock, "Server received: ", 16, 0);
                send(curr_sock, buf, recv_len, 0);
            } else if (recv_len == 0) {
                // 客户端正常关闭连接
                printf("conn_sock=%d 客户端正常关闭连接(当前连接数:%d→%d)\n", 
                       curr_sock, conn_count, conn_count - 1);
                close(curr_sock);
                // 将列表中最后一个有效套接字移到当前位置,填补空缺
                conn_socks[i] = conn_socks[--conn_count];
                conn_socks[conn_count] = 0;  // 标记为无效
                i--;  // 重新检查当前位置(因已替换为最后一个套接字)
            } else if (recv_len == -1 && errno != EAGAIN && errno != EWOULDBLOCK) {
                // 连接异常(如客户端强制关闭)
                perror("recv() failed (non-EAGAIN)");
                printf("conn_sock=%d 连接异常,关闭(当前连接数:%d→%d)\n", 
                       curr_sock, conn_count, conn_count - 1);
                close(curr_sock);
                conn_socks[i] = conn_socks[--conn_count];
                conn_socks[conn_count] = 0;
                i--;
            }
        }

        // -------------------------- 阶段3:添加轮询延时 --------------------------
        // 无事件时休眠,降低CPU占用(关键优化)
        usleep(POLL_DELAY);
    }

    // 理论上不会到达此处,关闭侦听套接字
    close(listen_sock);
    return 0;
}
3.3.2 实例说明与运行结果

代码核心逻辑解析:

  • 侦听套接字非阻塞 :通过 set_nonblock 函数将 listen_sock 设为非阻塞,确保 accept 无新连接时不阻塞,可继续处理其他事件;
  • 连接套接字非阻塞 :新客户端连接后,立即将 new_conn 设为非阻塞,避免后续 recv 阻塞影响其他连接;
  • 轮询处理 :循环中先处理新连接,再遍历已连接列表处理数据,无事件时通过 usleep(POLL_DELAY) 休眠,降低 CPU 占用;
  • 连接管理 :用数组 conn_socks 管理已连接套接字,客户端关闭连接时及时移除并调整数组,避免无效套接字占用资源。

运行结果(启动服务器后,用多个 telnet 客户端连接并发送数据):

复制代码
创建侦听套接字成功,listen_sock = 3
服务器已启动,监听端口 9001(最大并发连接数:10)
新客户端连接:IP=127.0.0.1, Port=54321, conn_sock=4(当前连接数:1)
新客户端连接:IP=127.0.0.1, Port=54322, conn_sock=5(当前连接数:2)
从 conn_sock=4 接收数据:Hello Server!(长度:13 字节)
从 conn_sock=5 接收数据:Non-blocking Test(长度:15 字节)
conn_sock=4 客户端正常关闭连接(当前连接数:2→1)
从 conn_sock=5 接收数据:Goodbye(长度:7 字节)

四、非阻塞轮询模型的优缺点与适用场景

对非阻塞轮询模型的评价非常客观:它是并发 Socket 编程的"基础方案",但并非"最优方案",需根据实际需求判断是否适用。

4.1 优点

  • 轻量级并发:无需创建多进程或多线程,单个进程即可处理多个客户端连接------避免了进程/线程切换的开销,内存占用低;
  • 实现简单 :核心逻辑基于"循环+非阻塞函数",无需理解复杂的 I/O 多路复用机制(如 selectepoll),上手门槛低;
  • 灵活性高:可自由控制轮询频率和事件处理顺序(如优先处理新连接,或优先处理数据接收),适配简单业务场景。

4.2 缺点(重点强调)

核心缺陷 :非阻塞轮询模型的最大问题是"CPU 资源浪费"------即使无任何事件发生,进程仍需频繁循环调用套接字函数,导致 CPU 占用率居高不下(若无 usleep 延时,CPU 占用可能达 100%)。

  • 轮询效率低:当套接字数量较多时(如 hundreds 级别),循环遍历所有套接字会产生大量"无效调用"(多数套接字无事件),效率随套接字数量增长而急剧下降;
  • 延时问题 :为降低 CPU 占用添加的 usleep 延时,会导致事件响应存在"延迟"(如延时 100ms,数据到达后最多需 100ms 才会被处理),不适用于低延时业务;
  • 错误处理复杂 :需精确区分"非阻塞无事件"(EAGAIN)和"真正错误"(如 ECONNRESET),若处理不当易导致功能异常(如误判连接关闭)。

4.3 适用场景

基于上述优缺点,非阻塞轮询模型仅适用于以下场景:

  • 小规模并发:客户端连接数量较少(如 tens 级别),轮询遍历的"无效调用"少,CPU 浪费可接受;
  • 低实时性需求 :业务对事件响应延时不敏感(如日志收集、非实时数据传输),可通过 usleep 平衡 CPU 占用与延时;
  • 简单业务逻辑:无需复杂的事件优先级处理,仅需"接收连接+收发数据"的基础功能(如简单的回声服务器、测试工具)。

不适用场景 :高并发(hundreds+ 客户端)、低延时(如实时通信、高频交易)、复杂业务逻辑的场景,应优先选择 I/O 多路复用模型(如 epollkqueue)。

五、常见错误与优化方法

总结了非阻塞轮询模型的常见问题及解决方案,以下是关键要点:

5.1 常见错误与解决方法

常见错误 错误原因 解决方案
CPU 占用率过高(无事件时达 100%) 未添加轮询延时,进程无事件时仍频繁循环调用套接字函数,产生大量无效操作 在循环末尾添加 usleepnanosleep 延时(如 100μs~1ms),根据业务延时需求调整延时长度
误判"非阻塞无事件"为错误,导致连接异常关闭 未正确处理 errno,将 EAGAIN/EWOULDBLOCK 视为"连接错误",错误调用 close 调用非阻塞函数后,若返回 -1,需先检查 errno 是否为 EAGAIN/EWOULDBLOCK,仅当为其他值时才判定为错误
新连接套接字未设为非阻塞,导致后续 recv 阻塞 仅将侦听套接字设为非阻塞,忽略了"新创建的连接套接字默认是阻塞模式"的特性 通过 accept 获得新连接套接字后,必须立即调用 set_nonblock 将其设为非阻塞模式
客户端关闭连接后,套接字未从管理列表中移除,导致循环中频繁无效调用 未处理 recv 返回 0 的情况(客户端正常关闭),或 recv 返回 ECONNRESET 的情况(客户端强制关闭) 收到 recv 返回 0 或"非 EAGAIN 错误"时,立即 close 套接字,并从管理列表中移除,避免后续无效轮询

5.2 关键优化方向

5.2.1 优化 CPU 占用:动态调整轮询延时

固定延时(如 100μs)无法适配"事件密集"和"事件稀疏"两种场景------事件密集时延时会增加响应延迟,事件稀疏时延时仍可能导致 CPU 浪费。优化方案:

  • 事件密集时(如短时间内多次接收数据),减小延时(如 10μs);
  • 事件稀疏时(如连续多轮无事件),逐渐增大延时(如从 10μs→100μs→1ms);
  • 一旦有事件发生,立即将延时重置为最小值,确保后续事件响应及时。
5.2.2 优化轮询效率:减少无效套接字遍历

当多数套接字无事件时,循环遍历所有套接字会产生大量"无效 recv 调用"。优化方案:

  • 维护"活跃套接字列表":仅将"近期有事件发生"的套接字加入轮询列表,无事件的套接字定期检查(如每 10 轮检查一次);
  • 结合简单的状态标记:为每个套接字添加"是否可能有事件"的标记(如"数据接收就绪""连接待处理"),仅轮询标记为"可能有事件"的套接字。

六、拓展:非阻塞套接字与 I/O 多路复用的结合

非阻塞套接字的"最佳搭档"是 I/O 多路复用模型(如 selectpollepoll)------通过 I/O 多路复用函数"批量监控"多个非阻塞套接字,仅当套接字有事件发生时才进行处理,从根本上解决了非阻塞轮询模型"CPU 浪费"和"效率低"的问题。

6.1 核心原理:I/O 多路复用的"事件通知"机制

I/O 多路复用函数(以 select 为例)的核心逻辑是:

  1. 将需要监控的非阻塞套接字(如侦听套接字、连接套接字)加入"文件描述符集合"(如读集合 readfds);
  2. 调用 select 函数,进程会阻塞直到"集合中有套接字发生事件"或"超时";
  3. select 返回后,通过 FD_ISSET 检查哪些套接字有事件发生,仅对这些套接字调用 acceptrecv 等函数。

优势对比:非阻塞轮询模型是"主动查询所有套接字",而 I/O 多路复用是"被动等待事件通知"------前者的无效调用随套接字数量增长而增加,后者的效率不受套接字数量影响(仅处理有事件的套接字)。

6.2 简单示例:非阻塞套接字 + select

以下代码片段演示如何结合非阻塞套接字与 select,实现高效的事件处理(基于前文非阻塞服务器改造):

复制代码
#include <sys/select.h>

// ... 省略前文的 socket 创建、bind、listen、set_nonblock 等代码 ...

int main() {
    // ... 省略初始化代码 ...

    fd_set readfds;          // 监控"读事件"的文件描述符集合
    int max_fd = listen_sock; // 集合中最大的文件描述符(select 要求)
    struct timeval timeout;   // select 超时时间

    while (1) {
        // 步骤1:初始化文件描述符集合
        FD_ZERO(&readfds);                // 清空集合
        FD_SET(listen_sock, &readfds);    // 加入侦听套接字(监控新连接)
        // 将所有已连接套接字加入集合(监控数据接收)
        for (int i = 0; i < conn_count; i++) {
            if (conn_socks[i] > 0) {
                FD_SET(conn_socks[i], &readfds);
                if (conn_socks[i] > max_fd) {
                    max_fd = conn_socks[i]; // 更新最大文件描述符
                }
            }
        }

        // 步骤2:设置 select 超时时间(如 100ms,避免永久阻塞)
        timeout.tv_sec = 0;
        timeout.tv_usec = 100000;

        // 步骤3:调用 select,监控读事件
        int ready = select(max_fd + 1, &readfds, NULL, NULL, &timeout);
        if (ready == -1) {
            perror("select() failed");
            continue;
        } else if (ready == 0) {
            // 超时无事件,继续循环(无需 usleep,select 已阻塞等待)
            continue;
        }

        // 步骤4:处理有事件的套接字
        // 4.1 处理新连接(侦听套接字有事件)
        if (FD_ISSET(listen_sock, &readfds)) {
            // 非阻塞 accept(此时必有新连接,不会返回 EAGAIN)
            int new_conn = accept(listen_sock, NULL, NULL);
            if (new_conn > 0) {
                set_nonblock(new_conn); // 设为非阻塞
                conn_socks[conn_count++] = new_conn;
                printf("新客户端连接:conn_sock=%d\n", new_conn);
            }
        }

        // 4.2 处理数据接收(已连接套接字有事件)
        for (int i = 0; i < conn_count; i++) {
            int curr_sock = conn_socks[i];
            if (curr_sock > 0 && FD_ISSET(curr_sock, &readfds)) {
                // 非阻塞 recv(此时必有数据或连接关闭,不会返回 EAGAIN)
                ssize_t recv_len = recv(curr_sock, buf, BUF_SIZE, 0);
                // ... 省略数据处理、连接关闭等逻辑(同前文) ...
            }
        }
    }

    // ... 省略后续代码 ...
}

该方案的核心优势:

  • 无 CPU 浪费select 阻塞等待事件,无事件时进程休眠,CPU 占用极低;
  • 高效事件处理 :仅对"有事件的套接字"调用 accept/recv,无无效调用;
  • 扩展性好 :支持监控大量套接字(select 受限于 FD_SETSIZE,但 epoll 无此限制)。

七、总结

非阻塞套接字与轮询模型是 UNIX 并发 Socket 编程的"入门级方案",其核心价值在于"用简单的逻辑实现轻量级并发"。基于《精通UNIX下C语言编程与项目实践笔记.pdf》的内容,可总结出以下关键结论:

  1. 非阻塞设置是基础 :通过 fcntl 函数添加 O_NONBLOCK 标志,是实现非阻塞模型的前提,需遵循"获取-修改-设置"三步流程;
  2. 轮询模型的核心是"立即返回+errno 判断" :非阻塞函数返回 -1 时,需通过 errno 区分"无事件"和"真正错误",否则易导致功能异常;
  3. 优缺点明确,场景受限:轮询模型轻量、简单,但存在 CPU 浪费和效率低的缺陷,仅适用于小规模、低实时性需求的场景;
  4. 与 I/O 多路复用结合是进阶方向 :非阻塞套接字配合 select/epoll,可实现"高效、低 CPU 占用"的高并发处理,是生产环境的主流方案。

对于初学者,掌握非阻塞轮询模型是理解"并发 Socket 编程"的关键一步------它能帮助你深入理解套接字的"阻塞/非阻塞"特性,为后续学习 I/O 多路复用、异步 I/O 等高级模型打下坚实基础。

相关推荐
我是华为OD~HR~栗栗呀3 小时前
23届考研-Java面经(华为OD)
java·c++·python·华为od·华为·面试
Javatutouhouduan3 小时前
Java程序员如何深入学习JVM底层原理?
java·jvm·java面试·后端开发·java架构师·java程序员·互联网大厂
王嘉俊9253 小时前
设计模式--享元模式:优化内存使用的轻量级设计
java·设计模式·享元模式
奔跑吧邓邓子4 小时前
【C语言实战(6)】解锁C语言循环密码:for循环实战探秘
c语言·实战·for循环
GilgameshJSS4 小时前
STM32H743-ARM例程15-RTC
c语言·arm开发·stm32·实时音视频
2301_803554524 小时前
C++联合体(Union)详解:与结构体的区别、联系与深度解析
java·c++·算法
pu_taoc4 小时前
深入剖析:基于epoll与主从Reactor模型的高性能服务器设计与实现
服务器·c语言·c++·vscode
EnCi Zheng4 小时前
SpringBoot 配置文件完全指南-从入门到精通
java·spring boot·后端
烙印6014 小时前
Spring容器的心脏:深度解析refresh()方法(上)
java·后端·spring