You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用select处理多端口客户端连接时阻塞问题求助

问题分析与修复方案

看起来你遇到的两个核心问题(select阻塞、依赖usleep),本质都是对select的工作机制理解不到位导致的,咱们一步步拆解问题:

核心错误点

1. 没有重新初始化fd_set

select函数会修改传入的读集合set:它会把没有触发事件的文件描述符从集合中清除,只保留有事件的fd。你现在只在循环外初始化了一次set,第一次select触发后,set里只剩下触发事件的fd,后续循环的select就只会监听这些fd,自然会阻塞在没有新事件的状态。

2. 错误判断已连接客户端的事件

你用i == new_comm和i == new_serv来判断已连接套接字的事件,但new_comm/new_serv只会保存最后一次接受的连接fd,之前的客户端fd完全不会被处理,这会导致除了最后一个客户端,其他连接都无法响应。

3. 缺少对已连接fd的持久化管理

你没有维护一个保存所有已连接客户端fd的集合,每次循环无法把这些fd重新加入select的监听集合,导致后续无法监听这些客户端的读写事件。

修复后的代码示例

首先,我们需要一个全局或静态的集合来保存所有需要监听的fd(包括两个监听套接字和所有已连接客户端),然后每次循环前重新初始化select的集合:

// 定义全局集合保存所有需要监听的fd
fd_set all_fds;
int max_fd;

int main() {
    // 这里省略初始化监听套接字fd_serv、fd_comm的代码...
    
    FD_ZERO(&all_fds);
    FD_SET(fd_serv, &all_fds);
    FD_SET(fd_comm, &all_fds);
    max_fd = max(fd_serv, fd_comm);

    while (1) {
        // 每次循环都要重新创建select的集合,从all_fds复制(避免select修改原始集合)
        fd_set read_set = all_fds;
        int ret = select(max_fd + 1, &read_set, NULL, NULL, NULL);
        if (ret < 0) {
            perror("select");
            exit(EXIT_FAILURE);
        }

        // 遍历所有可能的fd,检查是否有事件触发
        for (int i = 1; i <= max_fd; i++) {
            if (FD_ISSET(i, &read_set)) {
                if (i == fd_serv) {
                    // 处理fd_serv端口的连接请求
                    socklen_t sin_len_serv = sizeof(client_serv_addr);
                    int new_serv = accept(fd_serv, (struct sockaddr *)&client_serv_addr, &sin_len_serv);
                    if (new_serv < 0) {
                        perror("accept");
                        continue; // 单个连接失败不终止整个服务
                    }
                    fd_set_blocking(new_serv, 0);
                    FD_SET(new_serv, &all_fds);
                    if (new_serv > max_fd) {
                        max_fd = new_serv;
                    }
                } else if (i == fd_comm) {
                    // 处理fd_comm端口的连接请求
                    socklen_t sin_len_comm = sizeof(client_comm_addr);
                    int new_comm = accept(fd_comm, (struct sockaddr *)&client_comm_addr, &sin_len_comm);
                    if (new_comm < 0) {
                        perror("accept");
                        continue;
                    }
                    fd_set_blocking(new_comm, 0);
                    FD_SET(new_comm, &all_fds);
                    if (new_comm > max_fd) {
                        max_fd = new_comm;
                    }
                } else {
                    // 处理已连接客户端的读写事件
                    // 这里需要区分当前fd属于哪个监听端口的客户端,你可以维护一个映射表(比如数组)来记录
                    if (is_comm_client(i)) { // 自行实现判断逻辑,比如用数组存储每个fd的类型
                        if (read_command(fd_comm, i, start_time) == 1) {
                            close(i);
                            FD_CLR(i, &all_fds);
                            // 可选:更新max_fd,减少后续遍历范围
                            if (i == max_fd) {
                                while (max_fd > 0 && !FD_ISSET(max_fd, &all_fds)) {
                                    max_fd--;
                                }
                            }
                            return SHUTDOWN;
                        }
                    } else {
                        read_service(i);
                        close(i);
                        FD_CLR(i, &all_fds);
                        if (i == max_fd) {
                            while (max_fd > 0 && !FD_ISSET(max_fd, &all_fds)) {
                                max_fd--;
                            }
                        }
                    }
                }
            }
        }
    }
}

关键修复说明

  1. 维护all_fds集合:这个集合始终保存所有需要监听的fd(监听套接字+已连接客户端),每次循环前把它复制到read_set中传给select,避免select修改原始集合。
  2. 移除usleep:之前需要usleep是因为select的集合不正确,导致程序卡在错误的监听状态,修复集合问题后,usleep完全不需要了。
  3. 正确判断已连接事件:只要不是两个监听套接字,就认为是已连接客户端的事件,不再依赖new_comm/new_serv变量,这样所有客户端的事件都会被处理。
  4. 错误处理优化:把exit(EXIT_FAILURE)改成continue,避免单个连接失败导致整个服务器崩溃。
  5. 动态更新max_fd:当关闭客户端fd后,如果它是当前的max_fd,重新找到最大的fd,减少后续循环的遍历范围(不做也能运行,但更高效)。

另外,你需要维护一个额外的结构(比如数组或哈希表)来记录每个已连接fd属于哪个监听端口,这样在处理读写事件时能正确调用read_command或read_service。

内容的提问来源于stack exchange,提问作者Darren Col

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 09:37:56