解码服务器IO模型

深入解析服务器IO模型

IO 模型

服务器 IO 模型是服务端网络程序同时处理多个套接字的核心方案,无论是 UDP 还是 TCP 服务器,都需通过合理的 IO 模型应对多客户端请求场景。以下从核心概念到具体模型,结合代码示例与详细解释展开说明。

UDP 与 TCP 服务器的基础特性

UDP 服务器特性

UDP 无需连接,服务端仅需一个套接字即可与任意客户端通信。但默认套接字为阻塞型,调用recvfrom()时若客户端无消息,会导致服务器无限期阻塞,无法执行其他操作。

TCP 服务器特性

TCP 为面向连接协议,客户端连接成功后,服务端会新增一个已连接套接字与之对应。随着客户端数量增加,套接字数量同步增多,需高效管理多个套接字的 IO 操作。

核心 IO 模型详解

非阻塞轮询模型

核心逻辑

将所有套接字设为非阻塞模式,避免因客户端无数据导致服务器卡死;通过持续轮询套接字,检测是否有数据到达并处理。

关键技术:fcntl 函数(设置套接字非阻塞属性)

套接字在 Linux 系统中属于文件,通过fcntl()函数可修改文件(套接字)属性,实现非阻塞模式配置。

/**
 * fcntl函数:修改或获取文件描述符属性,用于配置套接字非阻塞模式
 * @brief 对打开的文件描述符执行指定操作,此处用于设置套接字为非阻塞
 * @param fd 目标文件描述符(此处为套接字描述符)
 * @param cmd 操作命令(F_GETFL获取属性,F_SETFL设置属性)
 * @param arg 命令参数(F_SETFL时传入包含O_NONBLOCK的属性值)
 * @return int 成功返回值取决于cmd:F_GETFL返回文件状态标志;F_SETFL返回0;失败返回-1
 * @note 必须先获取原有属性,通过位或运算添加非阻塞属性,避免覆盖原有配置
 */
int fcntl(int fd, int cmd, ... /* arg */ );
// UDP非阻塞轮询服务器示例
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int set_nonblocking(int sockfd) {
    // 获取套接字原有属性
    int flags = fcntl(sockfd, F_GETFL, 0);
    if (flags == -1) {
        perror("fcntl F_GETFL failed");
        return -1;
    }
    // 位或运算添加非阻塞属性(O_NONBLOCK)
    flags |= O_NONBLOCK;
    // 重新设置套接字属性
    if (fcntl(sockfd, F_SETFL, flags) == -1) {
        perror("fcntl F_SETFL failed");
        return -1;
    }
    return 0;
}

int main() {
    // 创建UDP套接字
    int udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (udp_sock == -1) {
        perror("socket create failed");
        return -1;
    }

    // 设置套接字为非阻塞模式
    if (set_nonblocking(udp_sock) == -1) {
        close(udp_sock);
        return -1;
    }

    // 绑定端口和地址
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(8888);
    if (bind(udp_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
        perror("bind failed");
        close(udp_sock);
        return -1;
    }

    char buf[1024];
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    // 轮询读取客户端数据
    while (1) {
        memset(buf, 0, sizeof(buf));
        // 非阻塞模式下,无数据时返回-1,errno设为EAGAIN或EWOULDBLOCK
        ssize_t recv_len = recvfrom(udp_sock, buf, sizeof(buf)-1, 0, 
                                   (struct sockaddr*)&client_addr, &client_len);
        if (recv_len > 0) {
            printf("收到客户端数据:%s\n", buf);
            // 回复客户端(可选)
            sendto(udp_sock, "已收到", 5, 0, (struct sockaddr*)&client_addr, client_len);
        } else if (recv_len == -1) {
            // 无数据可读,可执行其他操作(如日志打印)
            usleep(10000); // 减少CPU占用
        }
    }

    close(udp_sock);
    return 0;
}

注意事项

  • 配置非阻塞属性时,必须先通过F_GETFL获取原有属性,再用位或运算添加O_NONBLOCK,避免覆盖套接字原有配置。
  • 轮询会持续占用 CPU 资源,可通过usleep()等函数适当延时,降低资源消耗。

多任务并发模型

核心逻辑

通过多进程或多线程同时处理多个套接字,每个套接字分配独立的执行单元,实现并行 IO 操作。通常处理网络套接字数据优先使用多线程(资源开销低于多进程)。

分类实现

  • UDP 多线程实现:单个 UDP 套接字交由专门线程管理,负责收发所有客户端数据。

  • TCP 多线程实现:独立线程监听连接请求,每成功建立一个连接,创建新线程对应已连接套接字。

    image

#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
// 客户端信息结构体(用于链表存储)
typedef struct ClientInfo {
    int conn_fd; // 已连接套接字描述符
    struct sockaddr_in addr; // 客户端地址
    struct ClientInfo* next; // 链表节点指针
} ClientInfo;

// 客户端链表头节点(全局变量,需加锁保护)
ClientInfo* client_list = NULL;
pthread_mutex_t client_mutex = PTHREAD_MUTEX_INITIALIZER;

/**
 * 客户端处理线程函数:负责单个TCP客户端的通信
 * @brief 循环读取客户端数据,处理后回复,断开连接时移除链表节点
 * @param arg 传入ClientInfo结构体指针
 * @return void* 线程返回值(NULL)
 */
void* handle_client(void* arg) {
    ClientInfo* client = (ClientInfo*)arg;
    char buf[1024];
    ssize_t recv_len;

    while (1) {
        memset(buf, 0, sizeof(buf));
        // 读取客户端数据
        recv_len = recv(client->conn_fd, buf, sizeof(buf)-1, 0);
        if (recv_len <= 0) {
            // 客户端断开连接或读取失败
            printf("客户端断开连接\n");
            break;
        }
        printf("收到客户端数据:%s\n", buf);
        // 回复客户端
        send(client->conn_fd, "处理完成", 8, 0);
    }

    // 关闭套接字
    close(client->conn_fd);
    // 从链表中移除客户端信息(加锁保护)
    pthread_mutex_lock(&client_mutex);
    ClientInfo* prev = NULL;
    ClientInfo* curr = client_list;
    while (curr != NULL) {
        if (curr == client) {
            if (prev == NULL) {
                client_list = curr->next;
            } else {
                prev->next = curr->next;
            }
            break;
        }
        prev = curr;
        curr = curr->next;
    }
    pthread_mutex_unlock(&client_mutex);
    // 释放内存
    free(client);
    pthread_exit(NULL);
}

/**
 * 监听线程函数:负责监听TCP连接请求
 * @brief 循环accept客户端连接,创建新线程处理,将客户端信息加入链表
 * @param arg 传入监听套接字描述符
 * @return void* 线程返回值(NULL)
 */
void* listen_thread(void* arg) {
    int listen_fd = *(int*)arg;
    ClientInfo* new_client;
    pthread_t tid;
    socklen_t client_len = sizeof(struct sockaddr_in);

    while (1) {
        // 分配客户端信息内存
        new_client = (ClientInfo*)malloc(sizeof(ClientInfo));
        if (new_client == NULL) {
            perror("malloc failed");
            continue;
        }
        // 接受客户端连接
        new_client->conn_fd = accept(listen_fd, (struct sockaddr*)&new_client->addr, &client_len);
        if (new_client->conn_fd == -1) {
            perror("accept failed");
            free(new_client);
            continue;
        }
        new_client->next = NULL;

        // 将客户端信息加入链表(加锁保护)
        pthread_mutex_lock(&client_mutex);
        new_client->next = client_list;
        client_list = new_client;
        pthread_mutex_unlock(&client_mutex);

        // 创建线程处理客户端通信
        if (pthread_create(&tid, NULL, handle_client, (void*)new_client) != 0) {
            perror("pthread_create failed");
            close(new_client->conn_fd);
            free(new_client);
        }
        // 分离线程,自动释放资源
        pthread_detach(tid);
    }
}

// TCP多线程并发服务器主函数
int main() {
    int listen_fd;
    struct sockaddr_in serv_addr;
    pthread_t listen_tid;

    // 创建监听套接字
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket create failed");
        return -1;
    }

    // 绑定地址和端口
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(8888);
    if (bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
        perror("bind failed");
        close(listen_fd);
        return -1;
    }

    // 开始监听(backlog设为10,允许10个等待连接)
    if (listen(listen_fd, 10) == -1) {
        perror("listen failed");
        close(listen_fd);
        return -1;
    }

    // 创建监听线程
    if (pthread_create(&listen_tid, NULL, listen_thread, (void*)&listen_fd) != 0) {
        perror("pthread_create listen thread failed");
        close(listen_fd);
        return -1;
    }

    // 主线程等待(可添加信号处理等逻辑)
    pthread_join(listen_tid, NULL);
    close(listen_fd);
    pthread_mutex_destroy(&client_mutex);
    return 0;
}

注意事项

  • 用链表存储客户端信息,断开连接时需从链表中移除,操作链表需加互斥锁(pthread_mutex_t),避免线程安全问题。
  • 线程创建后可通过pthread_detach()设置分离属性,自动释放线程资源,无需主线程等待。

异步信号模型

核心逻辑

利用信号的异步特性处理套接字 IO,当套接字有数据到达时,内核触发指定信号,服务器通过信号处理函数处理数据。

关键信号:SIGIO

SIGIO 是内核针对套接字数据到达触发的信号,默认会杀死目标进程,需自定义信号处理函数,且需指定信号宿主(避免多进程 / 线程间信号混乱)。

image

服务器代码

#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int udp_sock; // 全局UDP套接字描述符(信号处理函数中使用)
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);

/**
 * SIGIO信号处理函数:处理UDP套接字的数据接收
 * @brief 当套接字有数据到达时触发,读取数据并回复客户端
 * @param sig 信号编号(此处为SIGIO)
 */
void sigio_handler(int sig) {
    char buf[1024];
    memset(buf, 0, sizeof(buf));
    // 读取客户端数据
    ssize_t recv_len = recvfrom(udp_sock, buf, sizeof(buf)-1, 0, 
                               (struct sockaddr*)&client_addr, &client_len);
    if (recv_len > 0) {
        printf("收到客户端数据:%s\n", buf);
        // 回复客户端
        sendto(udp_sock, "已处理", 9, 0, (struct sockaddr*)&client_addr, client_len);
    }
}

// UDP异步信号驱动服务器
int main() {
    struct sockaddr_in serv_addr;

    // 创建UDP套接字
    udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (udp_sock == -1) {
        perror("socket create failed");
        return -1;
    }

    // 绑定地址和端口
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(8888);
    if (bind(udp_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
        perror("bind failed");
        close(udp_sock);
        return -1;
    }

    // 设置SIGIO信号处理函数
    signal(SIGIO,sigio_handler);

    // 指定SIGIO信号宿主(当前进程)
    if (fcntl(udp_sock, F_SETOWN, getpid()) == -1) {
        perror("fcntl F_SETOWN failed");
        close(udp_sock);
        return -1;
    }

    // 开启套接字异步模式(触发SIGIO信号)
    int flags = fcntl(udp_sock, F_GETFL, 0);
    if (flags == -1) {
        perror("fcntl F_GETFL failed");
        close(udp_sock);
        return -1;
    }
    flags |= O_ASYNC; // 开启异步模式
    if (fcntl(udp_sock, F_SETFL, flags) == -1) {
        perror("fcntl F_SETFL failed");
        close(udp_sock);
        return -1;
    }

    // 主线程阻塞等待信号(可执行其他逻辑)
    while (1) {
        sleep(1); // 避免进程退出
    }

    close(udp_sock);
    return 0;
}

客户端代码

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>

#define BUF_SIZE 1024
#define DEFAULT_PORT 8888
#define DEFAULT_IP "127.0.0.1"

int main(int argc, char *argv[]) {
    int client_sock;
    struct sockaddr_in serv_addr;
    char send_buf[BUF_SIZE];
    char recv_buf[BUF_SIZE];
    ssize_t send_len, recv_len;
    
    // 解析命令行参数(可选指定服务器IP和端口)
    const char *serv_ip = DEFAULT_IP;
    int serv_port = DEFAULT_PORT;
    if (argc == 2) {
        serv_ip = argv[1];
    } else if (argc == 3) {
        serv_ip = argv[1];
        serv_port = atoi(argv[2]);
    } else if (argc > 3) {
        fprintf(stderr, "用法:%s [服务器IP] [端口]\n", argv[0]);
        fprintf(stderr, "示例:%s 127.0.0.1 8888(默认)\n", argv[0]);
        return -1;
    }

    // 创建UDP套接字
    client_sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (client_sock == -1) {
        perror("socket create failed");
        return -1;
    }

    // 配置服务器地址结构
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;                  // IPv4协议
    serv_addr.sin_port = htons(serv_port);           // 服务器端口(主机字节序转网络字节序)
    if (inet_pton(AF_INET, serv_ip, &serv_addr.sin_addr) <= 0) {
        perror("invalid server IP");
        close(client_sock);
        return -1;
    }

    printf("=== UDP客户端启动 ===\n");
    printf("服务器地址:%s:%d\n", serv_ip, serv_port);
    printf("输入消息发送(输入 quit 退出)\n");

    // 循环读取输入并发送数据
    while (1) {
        // 读取用户输入
        printf("> ");
        fflush(stdout);  // 刷新输出缓冲区,确保提示符号正常显示
        if (fgets(send_buf, BUF_SIZE, stdin) == NULL) {
            perror("read input failed");
            break;
        }

        // 去除换行符(fgets会读取回车符)
        send_buf[strcspn(send_buf, "\n")] = '\0';

        // 退出条件(输入quit/exit)
        if (strcmp(send_buf, "quit") == 0 || strcmp(send_buf, "exit") == 0) {
            printf("客户端主动退出\n");
            break;
        }

        // 发送数据到服务器
        send_len = sendto(client_sock, send_buf, strlen(send_buf), 0,
                         (struct sockaddr*)&serv_addr, sizeof(serv_addr));
        if (send_len == -1) {
            perror("sendto failed");
            continue;
        }

        // 接收服务器回复(设置超时避免无限阻塞,可选)
        struct timeval timeout = {3, 0};  // 3秒超时
        setsockopt(client_sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
        
        memset(recv_buf, 0, sizeof(recv_buf));
        recv_len = recvfrom(client_sock, recv_buf, BUF_SIZE-1, 0, NULL, NULL);
        if (recv_len > 0) {
            printf("服务器回复:%s\n", recv_buf);
        } else if (recv_len == 0) {
            printf("服务器关闭连接\n");
        } else {
            printf("接收超时或失败\n");
        }
    }

    // 关闭套接字
    close(client_sock);
    return 0;
}

注意事项

  • 必须自定义 SIGIO 信号处理函数,否则信号触发时会终止进程。
  • 需通过fcntl(udp_sock, F_SETOWN, getpid())指定信号宿主,确保信号发送到当前进程。
  • 仅适用于 UDP 协议:TCP 中连接请求、数据传输、断开连接等操作都会触发 SIGIO,无法通过单一信号区分操作类型,无法精准处理。
  • 信号处理函数中应避免复杂操作,优先采用简单的数据读取和回复逻辑,避免信号重入问题。

多路复用模型(select/poll)

核心逻辑

通过特定接口(select/poll 函数)同时监控多个阻塞套接字,当某个 / 某些套接字状态就绪(可读 / 可写)时,再进行对应 IO 操作。无需多进程 / 线程,即可高效处理多个阻塞套接字。

image

select 函数实现

#include <sys/select.h>
/**
 * select函数:同步IO多路复用,监控多个文件描述符状态
 * @brief 阻塞等待监控的文件描述符就绪,就绪后返回并标记就绪描述符
 * @param nfds 监控的最大文件描述符+1(内核遍历范围)
 * @param readfds 可读状态监控集合(NULL表示不监控)
 * @param writefds 可写状态监控集合(NULL表示不监控)
 * @param exceptfds 异常状态监控集合(NULL表示不监控)
 * @param timeout 超时时间:NULL(无限阻塞)、{0,0}(立即返回)、具体时间(秒+微秒)
 * @return int 成功返回就绪描述符总数;失败返回-1;超时返回0
 * @note 返回后未就绪的描述符会被自动清零,需重新初始化集合
 *       文件描述符集合大小受FD_SETSIZE限制(默认1024)
 */
int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

/**
 * FD_ZERO:清空文件描述符集合
 * @brief 将fd_set中的所有位设为0,初始化集合(必须在使用前调用)
 * @param set 指向要清空的fd_set结构体的指针(不可为NULL)
 * @return void 无返回值
 * @note 若不初始化,集合中会残留随机值,导致select监控异常
 */
void FD_ZERO(fd_set *set);

/**
 * FD_SET:将指定文件描述符添加到集合中
 * @brief 把fd对应的位图位设为1,让select监控该fd
 * @param fd 要添加的文件描述符(必须是有效的非负整数,且小于FD_SETSIZE)
 * @param set 指向目标fd_set结构体的指针(不可为NULL)
 * @return void 无返回值
 * @note 1. fd不能超过FD_SETSIZE(默认1024),否则会越界导致内存错误
 *       2. 重复添加同一fd无效果(位图位已为1),不会报错
 */
void FD_SET(int fd, fd_set *set);

/**
 * FD_CLR:将指定文件描述符从集合中移除
 * @brief 把fd对应的位图位设为0,让select停止监控该fd
 * @param fd 要移除的文件描述符(需是已添加到集合中的有效fd)
 * @param set 指向目标fd_set结构体的指针(不可为NULL)
 * @return void 无返回值
 * @note 移除不存在的fd无效果,不会报错
 */
void FD_CLR(int fd, fd_set *set);

/**
 * FD_ISSET:检查文件描述符是否在就绪集合中
 * @brief 判断fd对应的位图位是否为1,用于select返回后检测哪个fd就绪
 * @param fd 要检查的文件描述符(非负整数)
 * @param set 指向select返回后的fd_set结构体的指针(不可为NULL)
 * @return int 就绪返回非零值(通常为1),未就绪返回0
 * @note select会修改原集合,仅保留就绪fd的位为1,未就绪fd的位会被清0
 */
int FD_ISSET(int fd, fd_set *set);
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>

// TCP多路复用服务器(select实现)
int main() {
    int listen_fd, conn_fd;
    struct sockaddr_in serv_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    fd_set read_fds, temp_fds; // 监控集合(temp_fds用于临时备份)
    int max_fd; // 最大文件描述符

    // 创建监听套接字
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket create failed");
        return -1;
    }

    // 绑定地址和端口
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(8888);
    if (bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
        perror("bind failed");
        close(listen_fd);
        return -1;
    }

    // 开始监听
    if (listen(listen_fd, 10) == -1) {
        perror("listen failed");
        close(listen_fd);
        return -1;
    }

    // 初始化文件描述符集合
    FD_ZERO(&read_fds); // 清空集合
    FD_SET(listen_fd, &read_fds); // 添加监听套接字到监控集合
    max_fd = listen_fd; // 初始最大描述符为监听套接字

    struct timeval timeout = {3, 0}; // 超时时间3秒
    char buf[1024];
    ssize_t recv_len;

    while (1) {
        // 备份监控集合(select会修改原集合,需重新初始化)
        temp_fds = read_fds;

        // 调用select监控可读状态
        int ret = select(max_fd + 1, &temp_fds, NULL, NULL, &timeout);
        if (ret == -1) {
            perror("select failed");
            continue;
        } else if (ret == 0) {
            // 超时,无就绪描述符
            printf("select timeout...\n");
            continue;
        }

        // 遍历所有可能的文件描述符,检查就绪状态
        for (int fd = 0; fd <= max_fd; fd++) {
            if (FD_ISSET(fd, &temp_fds)) { // 检查fd是否在就绪集合中
                if (fd == listen_fd) {
                    // 监听套接字就绪,接受新连接
                    conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
                    if (conn_fd == -1) {
                        perror("accept failed");
                        continue;
                    }
                    printf("新客户端连接,fd=%d\n", conn_fd);
                    FD_SET(conn_fd, &read_fds); // 将新连接加入监控集合
                    if (conn_fd > max_fd) {
                        max_fd = conn_fd; // 更新最大描述符
                    }
                } else {
                    // 已连接套接字就绪,读取数据
                    memset(buf, 0, sizeof(buf));
                    recv_len = recv(fd, buf, sizeof(buf)-1, 0);
                    if (recv_len <= 0) {
                        // 客户端断开连接
                        printf("客户端断开,fd=%d\n", fd);
                        close(fd);
                        FD_CLR(fd, &read_fds); // 从监控集合中移除
                        // 更新max_fd(可选,优化遍历效率)
                        if (fd == max_fd) {
                            while (!FD_ISSET(max_fd, &read_fds) && max_fd > 0) {
                                max_fd--;
                            }
                        }
                    } else {
                        printf("收到数据(fd=%d):%s\n", fd, buf);
                        send(fd, "已处理", 5, 0); // 回复客户端
                    }
                }
            }
        }
    }

    close(listen_fd);
    return 0;
}

poll 函数实现

/**
 * poll函数:同步IO多路复用,监控多个文件描述符状态(select增强版)
 * @brief 阻塞等待监控的文件描述符就绪,通过结构体数组管理监控对象
 * @param fds 监控结构体数组(每个元素对应一个描述符及监控事件)
 * @param nfds 监控的结构体数组长度
 * @param timeout 超时时间(毫秒):-1(无限阻塞)、0(立即返回)、>0(具体毫秒数)
 * @return int 成功返回就绪描述符总数;失败返回-1;超时返回0
 * @note 无需重新初始化监控集合,就绪事件会在结构体中标记
 *       监控数量无FD_SETSIZE限制,仅受系统资源约束
 */
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
#include <sys/poll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>

// TCP多路复用服务器(poll实现)
#define MAX_CLIENT 1024 // 最大客户端数量
int main() {
    int listen_fd, conn_fd;
    struct sockaddr_in serv_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    struct pollfd fds[MAX_CLIENT]; // poll监控结构体数组
    int nfds = 1; // 初始监控数量(仅监听套接字)
    char buf[1024];
    ssize_t recv_len;

    // 创建监听套接字
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket create failed");
        return -1;
    }

    // 绑定地址和端口
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(8888);
    if (bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
        perror("bind failed");
        close(listen_fd);
        return -1;
    }

    // 开始监听
    if (listen(listen_fd, 10) == -1) {
        perror("listen failed");
        close(listen_fd);
        return -1;
    }

    // 初始化poll监控数组(监听套接字,监控可读事件)
    fds[0].fd = listen_fd;
    fds[0].events = POLLIN; // 监控可读事件
    fds[0].revents = 0; // 就绪事件初始化为0

    while (1) {
        // 调用poll监控事件,超时时间3000毫秒(3秒)
        int ret = poll(fds, nfds, 3000);
        if (ret == -1) {
            perror("poll failed");
            continue;
        } else if (ret == 0) {
            printf("poll timeout...\n");
            continue;
        }

        // 遍历监控数组,处理就绪事件
        for (int i = 0; i < nfds; i++) {
            if (fds[i].revents & POLLIN) { // 可读事件就绪
                if (fds[i].fd == listen_fd) {
                    // 监听套接字就绪,接受新连接
                    conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
                    if (conn_fd == -1) {
                        perror("accept failed");
                        continue;
                    }
                    if (nfds >= MAX_CLIENT) {
                        printf("客户端数量已满,拒绝连接\n");
                        close(conn_fd);
                        continue;
                    }
                    // 添加新连接到poll监控数组
                    fds[nfds].fd = conn_fd;
                    fds[nfds].events = POLLIN;
                    fds[nfds].revents = 0;
                    nfds++;
                    printf("新客户端连接,fd=%d,当前连接数=%d\n", conn_fd, nfds-1);
                } else {
                    // 已连接套接字就绪,读取数据
                    memset(buf, 0, sizeof(buf));
                    recv_len = recv(fds[i].fd, buf, sizeof(buf)-1, 0);
                    if (recv_len <= 0) {
                        // 客户端断开连接
                        printf("客户端断开,fd=%d\n", fds[i].fd);
                        close(fds[i].fd);
                        // 移除该客户端(用最后一个元素覆盖,减少遍历)
                        fds[i] = fds[nfds - 1];
                        nfds--;
                        i--; // 重新检查当前位置(已被替换为原最后一个元素)
                    } else {
                        printf("收到数据(fd=%d):%s\n", fds[i].fd, buf);
                        send(fds[i].fd, "已处理", 5, 0);
                    }
                }
                // 清除就绪事件标记
                fds[i].revents = 0;
            }
        }
    }

    close(listen_fd);
    return 0;
}

高效 IO 多路复用 ——epoll 模型

epoll 是 Linux 内核 2.6 版本后引入的高级 IO 多路复用机制,专门针对 select/poll 在高并发场景下的效率瓶颈设计。它通过红黑树管理监控的文件描述符、就绪链表存储就绪事件,实现了 “只关心就绪 fd” 的高效通知机制,是高并发服务器(如 Nginx、Redis)的核心 IO 模型。

epoll 核心原理

epoll 的高效源于其底层两种关键数据结构:

  • 红黑树:用于管理所有需要监控的文件描述符(fd),支持高效的添加、删除、查询操作(时间复杂度 O (log n)),解决了 select/poll “遍历所有 fd” 的效率问题。
  • 就绪链表:内核会主动将就绪的 fd 加入该链表,epoll_wait 只需从链表中获取就绪 fd,无需遍历全部监控对象,大幅提升高并发场景下的性能。

epoll 核心函数详解

epoll 通过 3 个核心函数完成监控流程,以下结合代码注释详细说明:

epoll_create—— 创建 epoll 实例

#include <sys/epoll.h>
/**
 * epoll_create:创建epoll实例,返回用于操作epoll的文件描述符
 * @brief 初始化epoll的红黑树和就绪链表,后续通过该fd管理监控事件
 * @param size 早期版本用于指定监控fd的最大数量,Linux 2.6.8后被忽略(仅需传>0的值)
 * @return int 成功返回epoll实例fd(非负整数),失败返回-1(errno标记错误)
 * @note 每个epoll实例对应一个独立的监控集合,需用close()关闭
 *       size参数仅为兼容性保留,实际监控数量由系统资源决定(无硬限制)
 */
int epoll_create(int size);

epoll_ctl—— 管理监控事件(添加 / 修改 / 删除)

/**
 * epoll_ctl:向epoll实例添加、修改或删除监控的文件描述符及事件
 * @brief 操作epoll红黑树,维护监控的fd和对应的事件类型
 * @param epfd epoll实例的文件描述符(由epoll_create返回)
 * @param op 操作类型:
 *           - EPOLL_CTL_ADD:添加新fd到监控集合
 *           - EPOLL_CTL_MOD:修改已监控fd的事件类型
 *           - EPOLL_CTL_DEL:从监控集合中删除fd(event参数可设为NULL)
 * @param fd 要操作的目标文件描述符(如套接字fd)
 * @param event 指向epoll_event结构体的指针,描述监控的事件类型及附加数据
 * @return int 成功返回0,失败返回-1(errno标记错误,如fd已存在、权限不足)
 * @note 添加fd时,需确保fd非阻塞(尤其ET模式下)
 *       删除fd时,无需指定event的具体内容,仅需传入非NULL指针即可
 */
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

// epoll_event结构体:描述监控的事件类型和附加数据
struct epoll_event {
    uint32_t events;  // 监控的事件类型(位图)
    epoll_data_t data;// 附加数据(可存储fd、指针等,用于区分不同fd)
};

// epoll_data_t联合体:存储附加数据的灵活结构
typedef union epoll_data {
    void    *ptr;     // 指向自定义数据的指针(如客户端信息结构体)
    int      fd;      // 直接存储目标fd(最常用)
    uint32_t u32;     // 32位无符号整数
    uint64_t u64;     // 64位无符号整数
} epoll_data_t;

// 常用事件类型(events参数可选值)
#define EPOLLIN      0x001  // 可读事件(fd有数据可读取)
#define EPOLLOUT     0x004  // 可写事件(fd可写入数据,不阻塞)
#define EPOLLPRI     0x002  // 紧急数据可读事件(如TCP带外数据)
#define EPOLLERR     0x008  // 错误事件(fd发生错误,会自动触发,无需手动设置)
#define EPOLLHUP     0x010  // 挂起事件(fd连接断开,会自动触发,无需手动设置)
#define EPOLLET      0x80000000  // 边缘触发模式(默认是水平触发LT)
#define EPOLLONESHOT 0x40000000  // 一次性事件(触发后自动删除监控,需重新添加)

epoll_wait—— 等待就绪事件

/**
 * epoll_wait:等待epoll实例中监控的fd就绪,获取就绪事件
 * @brief 阻塞等待就绪事件,从就绪链表中复制就绪fd到events数组
 * @param epfd epoll实例的文件描述符
 * @param events 输出参数:用于存储就绪事件的数组(需提前分配内存)
 * @param maxevents events数组的最大容量(必须>0,且不超过监控的fd总数)
 * @param timeout 超时时间(毫秒):
 *                - -1:无限阻塞,直到有fd就绪或被信号中断
 *                - 0:立即返回,无论是否有fd就绪(轮询模式)
 *                - >0:等待指定毫秒数,超时后返回0
 * @return int 成功返回就绪事件的数量(0表示超时),失败返回-1(errno标记错误)
 * @note 就绪事件会被复制到events数组,数组中仅包含就绪的fd,无需遍历全部监控对象
 *       同一fd的多个事件(如EPOLLIN+EPOLLOUT)会被合并为一个就绪项
 */
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epoll 的两种触发模式(关键特性)

epoll 支持水平触发(LT)边缘触发(ET) 两种事件通知方式,直接影响程序逻辑设计,是其与 select/poll 的核心区别之一。

水平触发(LT,Level Trigger)—— 默认模式

  • 触发逻辑:只要 fd 处于就绪状态(如可读),每次调用 epoll_wait 都会返回该 fd,直到 fd 的就绪状态消失(如数据被读完)。
  • 特点
    • 逻辑简单,兼容性好(类似 select/poll 的触发逻辑);
    • 无需一次性读完所有数据,可分多次读取;
    • 允许使用阻塞或非阻塞 fd(推荐非阻塞,避免单个 fd 阻塞影响其他 fd)。
  • 适用场景:新手入门、对性能要求不极致的场景,或需要兼容旧逻辑的代码。

边缘触发(ET,Edge Trigger)—— 高效模式

  • 触发逻辑:仅在 fd 的状态从 “未就绪” 变为 “就绪” 时触发一次,后续即使 fd 仍处于就绪状态(如还有数据未读),也不会再触发,直到状态再次变化(如又有新数据到达)。
  • 特点
    • 效率极高,仅通知状态变化,减少 epoll_wait 的返回次数;
    • 必须使用非阻塞 fd(否则若数据未读完,fd 会阻塞在 read/write,导致其他 fd 无法处理);
    • 必须一次性读完 / 写完所有数据(需循环调用 read/write,直到返回 - 1 且 errno 为 EAGAIN/EWOULDBLOCK)。
  • 适用场景:高并发场景(如万级以上连接),追求极致性能的服务器(如 Nginx)。

epoll 服务器代码示例(TCP)

水平触发(LT)模式示例

#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#define MAX_EVENTS 1024  // epoll_wait返回的最大就绪事件数
#define PORT 8888
int main() {
    int listen_fd, conn_fd, epfd;
    struct sockaddr_in serv_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    struct epoll_event ev, events[MAX_EVENTS]; // ev用于添加事件,events存储就绪事件
    char buf[1024];
    ssize_t recv_len;

    // 创建TCP监听套接字
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket create failed");
        return -1;
    }

    // 绑定地址和端口
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(PORT);
    if (bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
        perror("bind failed");
        close(listen_fd);
        return -1;
    }

    // 开始监听(允许10个等待连接)
    if (listen(listen_fd, 10) == -1) {
        perror("listen failed");
        close(listen_fd);
        return -1;
    }

    // 创建epoll实例
    epfd = epoll_create(1); // size参数忽略,传>0即可
    if (epfd == -1) {
        perror("epoll_create failed");
        close(listen_fd);
        return -1;
    }

    // 将监听套接字添加到epoll监控(LT模式,监控可读事件)
    ev.events = EPOLLIN;       // 可读事件
    ev.data.fd = listen_fd;    // 附加数据:存储监听fd
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) {
        perror("epoll_ctl add listen_fd failed");
        close(listen_fd);
        close(epfd);
        return -1;
    }

    printf("LT模式epoll服务器启动,端口:%d\n", PORT);

    // 循环等待就绪事件
    while (1) {
        // 调用epoll_wait,超时时间-1(无限阻塞)
        int ready_num = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (ready_num == -1) {
            perror("epoll_wait failed");
            continue;
        }

        // 遍历所有就绪事件
        for (int i = 0; i < ready_num; i++) {
            // 情况1:监听套接字就绪(有新客户端连接)
            if (events[i].data.fd == listen_fd) {
                conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
                if (conn_fd == -1) {
                    perror("accept failed");
                    continue;
                }
                printf("新客户端连接,fd=%d\n", conn_fd);

                // 将新连接的fd添加到epoll监控(LT模式,可读事件)
                ev.events = EPOLLIN;
                ev.data.fd = conn_fd;
                if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev) == -1) {
                    perror("epoll_ctl add conn_fd failed");
                    close(conn_fd);
                    continue;
                }
            }
            // 情况2:已连接套接字就绪(有数据可读)
            else if (events[i].events & EPOLLIN) {
                int client_fd = events[i].data.fd;
                memset(buf, 0, sizeof(buf));
                // LT模式:无需循环读,一次读部分数据也可(下次epoll_wait仍会触发)
                recv_len = read(client_fd, buf, sizeof(buf)-1);
                if (recv_len <= 0) {
                    // 客户端断开连接或读取错误
                    if (recv_len < 0) perror("read failed");
                    printf("客户端断开,fd=%d\n", client_fd);
                    // 从epoll中删除该fd,并关闭
                    epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, NULL);
                    close(client_fd);
                } else {
                    printf("收到数据(fd=%d):%s\n", client_fd, buf);
                    // 回复客户端(LT模式可直接写,无需监控可写事件)
                    write(client_fd, "LT模式:已收到数据", 18);
                }
            }
        }
    }

    // 关闭资源(实际不会执行到这里)
    close(listen_fd);
    close(epfd);
    return 0;
}

边缘触发(ET)模式示例

#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#define MAX_EVENTS 1024
#define PORT 8888
// 工具函数:设置fd为非阻塞模式(ET模式必须)
int set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) {
        perror("fcntl F_GETFL failed");
        return -1;
    }
    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
        perror("fcntl F_SETFL failed");
        return -1;
    }
    return 0;
}

int main() {
    int listen_fd, conn_fd, epfd;
    struct sockaddr_in serv_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    struct epoll_event ev, events[MAX_EVENTS];
    char buf[1024];
    ssize_t recv_len;

    // 创建监听套接字(ET模式下,监听fd可阻塞,也可设为非阻塞,不影响)
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket create failed");
        return -1;
    }

    // 绑定端口
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(PORT);
    if (bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
        perror("bind failed");
        close(listen_fd);
        return -1;
    }

    // 监听连接
    if (listen(listen_fd, 10) == -1) {
        perror("listen failed");
        close(listen_fd);
        return -1;
    }

    // 创建epoll实例
    epfd = epoll_create(1);
    if (epfd == -1) {
        perror("epoll_create failed");
        close(listen_fd);
        return -1;
    }

    // 添加监听fd到epoll(ET模式需加EPOLLET标志)
    ev.events = EPOLLIN | EPOLLET; // 可读事件 + 边缘触发
    ev.data.fd = listen_fd;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) {
        perror("epoll_ctl add listen_fd failed");
        close(listen_fd);
        close(epfd);
        return -1;
    }

    printf("ET模式epoll服务器启动,端口:%d\n", PORT);

    // 循环等待就绪事件
    while (1) {
        int ready_num = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (ready_num == -1) {
            perror("epoll_wait failed");
            continue;
        }

        for (int i = 0; i < ready_num; i++) {
            // 情况1:监听fd就绪(新连接)
            if (events[i].data.fd == listen_fd) {
                // ET模式:需循环accept,避免漏接连接(可能同时有多个连接到达)
                while (1) {
                    conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
                    if (conn_fd == -1) {
                        // 没有更多连接时,errno为EAGAIN/EWOULDBLOCK(因listen_fd非阻塞)
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            break;
                        } else {
                            perror("accept failed");
                            break;
                        }
                    }
                    printf("新客户端连接,fd=%d\n", conn_fd);

                    // ET模式:必须将客户端fd设为非阻塞
                    if (set_nonblocking(conn_fd) == -1) {
                        close(conn_fd);
                        continue;
                    }

                    // 添加客户端fd到epoll(ET模式 + 可读事件)
                    ev.events = EPOLLIN | EPOLLET;
                    ev.data.fd = conn_fd;
                    if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev) == -1) {
                        perror("epoll_ctl add conn_fd failed");
                        close(conn_fd);
                        continue;
                    }
                }
            }
            // 情况2:客户端fd就绪(有数据可读)
            else if (events[i].events & EPOLLIN) {
                int client_fd = events[i].data.fd;
                // ET模式:必须循环读,直到数据读完(返回-1且errno为EAGAIN)
                while (1) {
                    memset(buf, 0, sizeof(buf));
                    recv_len = read(client_fd, buf, sizeof(buf)-1);
                    if (recv_len > 0) {
                        printf("收到数据(fd=%d):%s\n", client_fd, buf);
                        // 若需回复,可添加EPOLLOUT事件监控(避免写阻塞)
                        // 此处简化:直接写(因fd非阻塞,写不完会返回EAGAIN,后续监控EPOLLOUT再写)
                        write(client_fd, "ET模式:已收到数据", 18);
                    } else if (recv_len == 0) {
                        // 客户端正常断开
                        printf("客户端断开,fd=%d\n", client_fd);
                        epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, NULL);
                        close(client_fd);
                        break;
                    } else {
                        // 读取错误:仅当errno为EAGAIN时表示数据已读完,其他为真错误
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            break; // 数据读完,退出循环
                        } else {
                            perror("read failed");
                            epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, NULL);
                            close(client_fd);
                            break;
                        }
                    }
                }
            }
        }
    }

    close(listen_fd);
    close(epfd);
    return 0;
}

epoll 与 select/poll 的核心区别

特性selectpollepoll
监控数量限制受 FD_SETSIZE(默认 1024)限制无硬限制,仅受系统资源约束无硬限制,支持万级以上连接
底层数据结构位图(固定大小)数组(动态管理)红黑树(管理监控 fd)+ 就绪链表(存储就绪 fd)
效率遍历所有 fd(O (n)),高并发低效遍历所有监控 fd(O (n)),低效仅处理就绪 fd(O (1)),高并发高效
触发模式仅水平触发(LT)仅水平触发(LT)支持 LT 和 ET(边缘触发,更高效)
fd 重复添加重复添加无效果(位图覆盖)重复添加无效果重复添加会返回错误(EEXIST)
内存拷贝每次调用拷贝全部 fd 集合到内核每次调用拷贝全部 fd 集合到内核仅初始化时拷贝,后续复用(零拷贝)
适用场景低并发(<1000 连接)中低并发(<1 万连接)高并发(万级以上连接,如 Web 服务器)

epoll 使用注意事项

  • ET 模式必须用非阻塞 fd:若 fd 阻塞,read/write 未完成时会阻塞进程,导致其他 fd 无法处理。
  • ET 模式需循环读写:仅触发一次状态变化,需循环调用 read/write 直到返回1errno=EAGAIN/EWOULDBLOCK,避免数据残留。
  • 避免监控不必要的事件:如仅需读数据时,不要监控 EPOLLOUT(否则 fd 可写时会频繁触发,浪费资源)。
  • 及时删除无效 fd:客户端断开后,需通过epoll_ctl(EPOLL_CTL_DEL)删除 fd,避免内存泄漏和错误触发。
  • epoll 实例的关闭:不再使用时需调用close(epfd),释放内核资源(红黑树、就绪链表)。

各 IO 模型对比与适用场景

模型优点缺点适用场景
非阻塞轮询实现简单,无多线程开销CPU 占用高,轮询效率低客户端数量少(<100)、数据频繁传输
多任务并发逻辑清晰,并行处理能力强线程 / 进程资源开销大,高并发性能下降客户端数量适中(<1000)、需独立处理连接
异步信号无需轮询,CPU 利用率高仅支持 UDP,信号处理逻辑受限UDP 协议、数据量少且突发的场景
select/poll单进程处理多连接,资源开销低高并发效率低,select 有连接数限制中低并发(<1 万连接)、兼容性要求高
epoll高并发效率极高,支持 ET 模式,无连接限制仅支持 Linux 系统,ET 模式逻辑复杂高并发场景(万级以上连接,如 Nginx、Redis)

  • 套接字本质:Linux 系统中属于特殊文件,通过文件描述符管理,所有文件操作函数(fcntl、read、write 等)均可用于套接字。
  • 阻塞与非阻塞区别:阻塞模式下 IO 操作未完成时,进程 / 线程会挂起等待;非阻塞模式下会立即返回,通过 errno 标记未就绪状态(EAGAIN/EWOULDBLOCK)。
  • 信号安全:信号处理函数中应避免调用非信号安全函数(如 printf、malloc),优先使用异步信号安全函数(如 write),避免程序异常。
  • 高并发设计原则:高并发服务器(如百万连接)通常结合 “epoll + 非阻塞 IO + 线程池” 设计,epoll 负责高效监控 fd,线程池处理 IO 逻辑,平衡性能与资源开销。

综合示例

服务器

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <sys/select.h>

#define PORT 8889
#define BUF_SIZE 1024
#define MAX_CLIENTS 10  // 最大支持客户端数

// 客户端连接结构体(存储每个客户端的信息)
typedef struct Client {
    int conn_fd;                // 客户端连接套接字
    char ip[INET_ADDRSTRLEN];   // 客户端IP
    int port;                   // 客户端端口
    pthread_t tid;              // 处理该客户端的线程ID
    struct Client *next;        // 链表节点(用于管理多个客户端)
} Client;

// 全局客户端链表(头节点)+ 互斥锁(线程安全操作链表)
Client *client_list = NULL;
pthread_mutex_t client_mutex = PTHREAD_MUTEX_INITIALIZER;

// 安全发送函数:确保数据完整发送
ssize_t safe_send(int fd, const char *buf, size_t len, int flags) {
    size_t total_sent = 0;
    while (total_sent < len) {
        ssize_t sent = send(fd, buf + total_sent, len - total_sent, flags);
        if (sent == -1) {
            if (errno == EINTR) continue;
            perror("[发送失败]");
            return -1;
        }
        total_sent += sent;
    }
    return total_sent;
}

// 从客户端链表中移除指定conn_fd的客户端(线程安全)
void remove_client(int conn_fd) {
    pthread_mutex_lock(&client_mutex);
    Client *prev = NULL, *curr = client_list;

    while (curr != NULL) {
        if (curr->conn_fd == conn_fd) {
            // 找到目标客户端,从链表中移除
            if (prev == NULL) {
                client_list = curr->next;  // 头节点
            } else {
                prev->next = curr->next;   // 中间/尾节点
            }
            printf("\n[客户端管理] 移除客户端:IP=%s:%d(conn_fd=%d)\n",
                   curr->ip, curr->port, curr->conn_fd);
            free(curr);  // 释放节点内存
            break;
        }
        prev = curr;
        curr = curr->next;
    }
    pthread_mutex_unlock(&client_mutex);
}

// 打印当前所有在线客户端(线程安全)
void print_clients() {
    pthread_mutex_lock(&client_mutex);
    Client *curr = client_list;
    if (curr == NULL) {
        printf("[客户端管理] 暂无在线客户端\n");
        pthread_mutex_unlock(&client_mutex);
        return;
    }

    printf("\n[客户端管理] 在线客户端列表:\n");
    int count = 0;
    while (curr != NULL) {
        count++;
        printf("  %d. IP=%s:%d | conn_fd=%d\n",
               count, curr->ip, curr->port, curr->conn_fd);
        curr = curr->next;
    }
    printf("  总计在线:%d 个\n", count);
    pthread_mutex_unlock(&client_mutex);
}

// 客户端处理线程:每个客户端独立线程,核心修复OOB处理逻辑
void *client_handler(void *arg) {
    Client *client = (Client *)arg;
    int conn_fd = client->conn_fd;
    char recv_buf[BUF_SIZE], oob_buf[BUF_SIZE];
    fd_set read_fds, except_fds;  // 读事件集合 + 异常事件集合(关键!)
    int max_fd = conn_fd;
    struct timeval timeout = {3, 0};  // select超时3秒

    printf("\n[线程启动] 开始处理客户端:IP=%s:%d(conn_fd=%d)\n",
           client->ip, client->port, conn_fd);

    // 线程分离:自动释放资源,无需主线程join
    pthread_detach(pthread_self());

    while (1) {
        // 初始化读事件和异常事件集合(每次select前必须重置)
        FD_ZERO(&read_fds);
        FD_ZERO(&except_fds);
        FD_SET(conn_fd, &read_fds);    // 监听普通数据(读事件)
        FD_SET(conn_fd, &except_fds);  // 监听OOB数据(异常事件)【核心修复】

        // 等待事件:读事件(普通数据)或异常事件(OOB数据)
        int ret = select(max_fd + 1, &read_fds, NULL, &except_fds, &timeout);
        if (ret == -1) {
            if (errno == EINTR) continue;
            perror("[select失败]");
            break;
        } else if (ret == 0) {
            continue;  // 超时,继续监听
        }

        // ------------- 处理OOB带外数据(异常事件触发)-------------
        if (FD_ISSET(conn_fd, &except_fds)) {
            memset(oob_buf, 0, BUF_SIZE);
            // 必须用MSG_OOB标志接收,仅1字节有效
            ssize_t oob_len = recv(conn_fd, oob_buf, BUF_SIZE - 1, MSG_OOB);
            if (oob_len == -1) {
                if (errno == EINTR) continue;
                perror("[接收OOB数据失败]");
                continue;
            }
            // 打印OOB数据(明确标记客户端信息)
            printf("\n📢【紧急通知】客户端(%s:%d,conn_fd=%d)发送带外数据:%c(有效字节数:%ld)\n",
                   client->ip, client->port, conn_fd, oob_buf[0], oob_len);
            fflush(stdout);  // 刷新缓冲区,避免日志被覆盖
            continue;
        }

        // ------------- 处理普通数据(读事件触发)-------------
        if (FD_ISSET(conn_fd, &read_fds)) {
            memset(recv_buf, 0, BUF_SIZE);
            ssize_t len = recv(conn_fd, recv_buf, BUF_SIZE - 1, 0);
            if (len == -1) {
                if (errno == EINTR) continue;
                perror("[接收普通数据失败]");
                break;
            } else if (len == 0) {
                printf("\n[客户端断开] 客户端主动断开:IP=%s:%d(conn_fd=%d)\n",
                       client->ip, client->port, conn_fd);
                break;
            }

            // 打印接收的普通数据
            printf("\n[接收数据] 客户端(%s:%d,conn_fd=%d):%s(字节数:%ld)\n",
                   client->ip, client->port, conn_fd, recv_buf, len);
            fflush(stdout);

            // 收到quit指令,客户端主动退出
            if (strcmp(recv_buf, "quit") == 0) {
                printf("[客户端退出] 收到客户端(%s:%d)退出指令\n",
                       client->ip, client->port);
                break;
            }
        }
    }

    // 线程退出:关闭连接,从链表移除客户端
    close(conn_fd);
    remove_client(conn_fd);
    printf("[线程退出] 客户端处理线程结束:IP=%s:%d(conn_fd=%d)\n",
           client->ip, client->port, conn_fd);
    return NULL;
}

// 服务器主线程输入处理:支持管理客户端、发送数据给指定客户端
void *server_input_handler(void *arg) {
    char input_buf[BUF_SIZE];
    printf("\n=== 服务器管理命令 ===\n");
    printf("  list → 查看在线客户端\n");
    printf("  send [conn_fd] [内容] → 给指定客户端发送普通数据(如send 4 hello)\n");
    printf("  oob [conn_fd] [字符] → 给指定客户端发送带外数据(如oob 4 !)\n");
    printf("  help → 查看命令说明\n");
    printf("======================\n");

    while (1) {
        printf("\n服务器命令(输入help查看说明):");
        memset(input_buf, 0, BUF_SIZE);
        if (fgets(input_buf, BUF_SIZE, stdin) == NULL) {
            perror("[读取命令失败]");
            continue;
        }
        input_buf[strcspn(input_buf, "\n")] = '\0';

        // 处理空输入
        if (strlen(input_buf) == 0) continue;

        // 查看在线客户端(list)
        if (strcmp(input_buf, "list") == 0) {
            print_clients();
            continue;
        }

        // 查看帮助(help)
        if (strcmp(input_buf, "help") == 0) {
            printf("\n=== 服务器管理命令 ===\n");
            printf("  list → 查看在线客户端\n");
            printf("  send [conn_fd] [内容] → 给指定客户端发送普通数据(如send 4 hello)\n");
            printf("  oob [conn_fd] [字符] → 给指定客户端发送带外数据(如oob 4 !)\n");
            printf("  help → 查看命令说明\n");
            printf("======================\n");
            continue;
        }

        // 发送普通数据(send [conn_fd] [内容])
        if (strncmp(input_buf, "send ", 5) == 0) {
            int target_fd;
            char content[BUF_SIZE];
            if (sscanf(input_buf, "send %d %[^\n]", &target_fd, content) != 2) {
                printf("错误:命令格式无效!正确格式:send [conn_fd] [内容](如send 4 hello)\n");
                continue;
            }

            // 查找目标客户端是否在线
            pthread_mutex_lock(&client_mutex);
            Client *curr = client_list;
            int found = 0;
            while (curr != NULL) {
                if (curr->conn_fd == target_fd) {
                    found = 1;
                    break;
                }
                curr = curr->next;
            }
            pthread_mutex_unlock(&client_mutex);

            if (!found) {
                printf("错误:conn_fd=%d 的客户端不在线!(输入list查看在线客户端)\n", target_fd);
                continue;
            }

            // 发送普通数据
            if (safe_send(target_fd, content, strlen(content), 0) != -1) {
                printf("✅ 已发送普通数据给客户端(conn_fd=%d):%s(字节数:%zu)\n",
                       target_fd, content, strlen(content));
            }
            continue;
        }

        // 发送带外数据(oob [conn_fd] [字符])
        if (strncmp(input_buf, "oob ", 4) == 0) {
            int target_fd;
            char oob_char;
            if (sscanf(input_buf, "oob %d %c", &target_fd, &oob_char) != 2) {
                printf("错误:命令格式无效!正确格式:oob [conn_fd] [单个字符](如oob 4 !)\n");
                continue;
            }

            // 查找目标客户端是否在线
            pthread_mutex_lock(&client_mutex);
            Client *curr = client_list;
            int found = 0;
            while (curr != NULL) {
                if (curr->conn_fd == target_fd) {
                    found = 1;
                    break;
                }
                curr = curr->next;
            }
            pthread_mutex_unlock(&client_mutex);

            if (!found) {
                printf("错误:conn_fd=%d 的客户端不在线!(输入list查看在线客户端)\n", target_fd);
                continue;
            }

            // 发送带外数据(仅1字节有效)
            if (safe_send(target_fd, &oob_char, 1, MSG_OOB) != -1) {
                printf("✅ 已发送带外数据给客户端(conn_fd=%d):%c(仅1字节有效)\n",
                       target_fd, oob_char);
            }
            continue;
        }

        // 无效命令
        printf("错误:无效命令!输入help查看支持的命令\n");
    }
}

// 服务器Ctrl+C优雅退出处理
void sigint_handler(int sig) {
    printf("\n\n收到Ctrl+C,服务器准备优雅退出...\n");
    // 关闭所有客户端连接
    pthread_mutex_lock(&client_mutex);
    Client *curr = client_list;
    while (curr != NULL) {
        Client *tmp = curr;
        curr = curr->next;
        printf("关闭客户端连接:IP=%s:%d(conn_fd=%d)\n", tmp->ip, tmp->port, tmp->conn_fd);
        close(tmp->conn_fd);
        free(tmp);
    }
    client_list = NULL;
    pthread_mutex_unlock(&client_mutex);
    // 销毁互斥锁
    pthread_mutex_destroy(&client_mutex);
    printf("服务器已退出,所有资源已释放!\n");
    exit(EXIT_SUCCESS);
}

int main() {
    int listen_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    pthread_t input_tid;  // 服务器输入处理线程(管理命令)

    // 信号处理:忽略SIGPIPE,捕获SIGINT(Ctrl+C)
    signal(SIGPIPE, SIG_IGN);
    signal(SIGINT, sigint_handler);

    // 创建监听套接字
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket创建失败");
        exit(EXIT_FAILURE);
    }

    // 地址复用+绑定+监听
    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("绑定失败");
        close(listen_fd);
        exit(EXIT_FAILURE);
    }

    if (listen(listen_fd, MAX_CLIENTS) == -1) {
        perror("监听失败");
        close(listen_fd);
        exit(EXIT_FAILURE);
    }

    // 启动服务器输入处理线程(管理客户端、发送数据)
    if (pthread_create(&input_tid, NULL, server_input_handler, NULL) != 0) {
        perror("创建输入处理线程失败");
        close(listen_fd);
        exit(EXIT_FAILURE);
    }

    printf("=== TCP OOB 多客户端服务器启动 ===\n");
    printf("监听端口:%d\n", PORT);
    printf("最大支持客户端数:%d\n", MAX_CLIENTS);
    printf("服务器PID:%d(Ctrl+C优雅退出)\n", getpid());

    // 主循环:持续接受新客户端连接(不退出)
    while (1) {
        // 接受新连接
        int new_conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addr_len);
        if (new_conn_fd == -1) {
            if (errno == EINTR) continue;
            perror("接受连接失败");
            continue;
        }

        // 检查客户端数量是否达到上限
        pthread_mutex_lock(&client_mutex);
        int client_count = 0;
        Client *curr = client_list;
        while (curr != NULL) {
            client_count++;
            curr = curr->next;
        }
        if (client_count >= MAX_CLIENTS) {
            printf("[连接拒绝] 客户端数量已达上限(%d个),拒绝新连接:IP=%s:%d\n",
                   MAX_CLIENTS, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
            close(new_conn_fd);
            pthread_mutex_unlock(&client_mutex);
            continue;
        }
        pthread_mutex_unlock(&client_mutex);

        // 创建新客户端节点,加入链表
        Client *new_client = (Client *)malloc(sizeof(Client));
        new_client->conn_fd = new_conn_fd;
        strcpy(new_client->ip, inet_ntoa(client_addr.sin_addr));
        new_client->port = ntohs(client_addr.sin_port);
        new_client->next = NULL;

        pthread_mutex_lock(&client_mutex);
        if (client_list == NULL) {
            client_list = new_client;  // 链表为空,作为头节点
        } else {
            curr = client_list;
            while (curr->next != NULL) curr = curr->next;
            curr->next = new_client;  // 加入链表尾部
        }
        printf("\n[新连接] 客户端上线:IP=%s:%d(conn_fd=%d),当前在线数:%d\n",
               new_client->ip, new_client->port, new_client->conn_fd, client_count + 1);
        pthread_mutex_unlock(&client_mutex);

        // 为新客户端创建处理线程
        if (pthread_create(&new_client->tid, NULL, client_handler, new_client) != 0) {
            perror("创建客户端处理线程失败");
            pthread_mutex_lock(&client_mutex);
            free(new_client);  // 释放节点
            pthread_mutex_unlock(&client_mutex);
            close(new_conn_fd);
            continue;
        }
    }

    // 理论上不会执行到这里
    close(listen_fd);
    pthread_mutex_destroy(&client_mutex);
    return 0;
}

客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <sys/select.h>

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8889
#define BUF_SIZE 1024

int sock_fd = -1;  // 与服务器的连接套接字
fd_set read_fds;   // select读集合
int max_fd;        // 最大文件描述符

// SIGURG信号处理函数:接收服务器OOB带外数据
void sigurg_handler(int sig) {
    if (sock_fd < 0) {
        fprintf(stderr, "\n[SIGURG] 无有效连接,忽略\n");
        return;
    }

    char oob_buf[BUF_SIZE];
    memset(oob_buf, 0, BUF_SIZE);

    ssize_t len = recv(sock_fd, oob_buf, BUF_SIZE - 1, MSG_OOB);
    if (len == -1) {
        if (errno == EINTR) return;
        perror("\n[接收OOB失败]");
        return;
    }

    printf("\n📢【紧急通知】收到服务器带外数据:%c(有效字节数:%ld)\n", oob_buf[0], len);
    printf("请输入数据(普通输入/OOB格式:oob:X/quit退出):");
    fflush(stdout);
}

// 安全发送函数:确保数据完整发送
ssize_t safe_send(int fd, const char *buf, size_t len, int flags) {
    size_t total_sent = 0;
    while (total_sent < len) {
        ssize_t sent = send(fd, buf + total_sent, len - total_sent, flags);
        if (sent == -1) {
            if (errno == EINTR) continue;
            perror("[发送失败]");
            return -1;
        }
        total_sent += sent;
    }
    return total_sent;
}

// 初始化select:添加监听的文件描述符(标准输入+套接字)
void init_select() {
    FD_ZERO(&read_fds);
    FD_SET(STDIN_FILENO, &read_fds);
    FD_SET(sock_fd, &read_fds);
    max_fd = (sock_fd > STDIN_FILENO) ? sock_fd : STDIN_FILENO;
}

// 信号处理函数:Ctrl+C优雅退出
void sigint_handler(int sig) {
    printf("\n收到Ctrl+C,客户端优雅退出...\n");
    if (sock_fd > 0) {
        safe_send(sock_fd, "quit", strlen("quit"), 0);
        close(sock_fd);
    }
    exit(EXIT_SUCCESS);
}

int main() {
    struct sockaddr_in server_addr;
    char recv_buf[BUF_SIZE], send_buf[BUF_SIZE];
    struct timeval timeout = {3, 0};  // select超时3秒

    // 注册信号处理函数
    signal(SIGINT, sigint_handler);
    signal(SIGPIPE, SIG_IGN);  // 忽略SIGPIPE(服务器断开后send不崩溃)

    // 创建套接字
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd == -1) {
        perror("socket创建失败");
        exit(EXIT_FAILURE);
    }

    // 配置服务器地址并连接
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
        perror("服务器IP无效");
        close(sock_fd);
        exit(EXIT_FAILURE);
    }
    server_addr.sin_port = htons(SERVER_PORT);

    if (connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("连接服务器失败");
        close(sock_fd);
        exit(EXIT_FAILURE);
    }
    printf("=== 已连接服务器:%s:%d ===\n", SERVER_IP, SERVER_PORT);

    // 配置SIGURG信号(接收OOB数据)
    signal(SIGURG, sigurg_handler);
    fcntl(sock_fd, F_SETOWN, getpid());  // 设置套接字所有者

    // 初始化select,开始双向通信
    init_select();
    printf("\n=== 双向通信已就绪 ===\n");
    printf("输入说明:\n");
    printf("  直接输入内容 → 发送普通数据\n");
    printf("  输入格式 'oob:X'(X为单个字符)→ 发送带外数据(如oob:!)\n");
    printf("  输入 'quit' → 退出程序\n");
    printf("===========================\n");
    printf("请输入数据:");
    fflush(stdout);

    while (1) {
        fd_set tmp_fds = read_fds;
        int ret = select(max_fd + 1, &tmp_fds, NULL, NULL, &timeout);
        if (ret == -1) {
            if (errno == EINTR) continue;
            perror("select失败");
            break;
        } else if (ret == 0) {
            init_select();
            continue;
        }

        // ------------- 处理服务器发送的数据(套接字可读)-------------
        if (FD_ISSET(sock_fd, &tmp_fds)) {
            memset(recv_buf, 0, BUF_SIZE);
            ssize_t len = recv(sock_fd, recv_buf, BUF_SIZE - 1, 0);
            if (len == -1) {
                if (errno == EINTR) continue;
                if (errno == ECONNRESET || errno == EBADF) {
                    printf("\n❌ 服务器主动断开连接\n");
                    break;
                }
                perror("[接收普通数据失败]");
                break;
            } else if (len == 0) {
                printf("\n❌ 服务器正常关闭连接\n");
                break;
            }

            printf("\n[接收服务器普通数据]:%s(字节数:%ld)\n", recv_buf, len);
            if (strcmp(recv_buf, "quit") == 0) {
                printf("\n❌ 收到服务器退出指令,客户端准备关闭\n");
                break;
            }
            printf("请输入数据:");
            fflush(stdout);
        }

        // ------------- 处理用户输入(标准输入可读)-------------
        if (FD_ISSET(STDIN_FILENO, &tmp_fds)) {
            memset(send_buf, 0, BUF_SIZE);
            if (fgets(send_buf, BUF_SIZE, stdin) == NULL) {
                perror("[读取输入失败]");
                break;
            }
            send_buf[strcspn(send_buf, "\n")] = '\0';

            // 处理空输入
            if (strlen(send_buf) == 0) {
                printf("提示:输入不能为空,请重新输入:");
                fflush(stdout);
                continue;
            }

            // 处理退出指令
            if (strcmp(send_buf, "quit") == 0) {
                safe_send(sock_fd, send_buf, strlen(send_buf), 0);
                printf("\n✅ 已发送退出指令,客户端准备关闭...\n");
                break;
            }

            // 处理带外数据
            if (strncmp(send_buf, "oob:", 4) == 0) {
                if (strlen(send_buf) != 5) {
                    printf("错误:OOB格式无效!正确格式:oob:X(X为单个字符)\n");
                    printf("请输入数据:");
                    fflush(stdout);
                    continue;
                }
                char oob_char = send_buf[4];
                if (safe_send(sock_fd, &oob_char, 1, MSG_OOB) != -1) {
                    printf("✅ 已发送带外数据:%c(仅1字节有效)\n", oob_char);
                }
                printf("请输入数据:");
                fflush(stdout);
                continue;
            }

            // 发送普通数据
            if (safe_send(sock_fd, send_buf, strlen(send_buf), 0) != -1) {
                printf("✅ 已发送普通数据:%s(字节数:%zu)\n", send_buf, strlen(send_buf));
            }
            printf("请输入数据:");
            fflush(stdout);
        }

        // 重新初始化select
        init_select();
    }

    // 释放资源
    close(sock_fd);
    printf("客户端已退出,所有资源已释放!\n");
    return 0;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值