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

在C语言select()模型中,如何在检查套接字后将其标记为未就绪/已使用状态?

用select()实现持久连接:解决recv()无限触发的问题

我来帮你分析一下问题所在,以及如何修复这个持久连接的实现。

问题根源

你遇到的无限打印"Received."的问题,核心是没有正确处理recv()的返回值,以及对select()的就绪状态逻辑理解不到位:

  • 当recv()返回0时,意味着客户端已经主动关闭了连接(发送了EOF),此时该套接字会一直处于"可读"状态,每次select()都会将其标记为就绪,但你没处理这种情况,导致无限循环调用recv(),每次都返回0。
  • 你在recv()之后直接调用strerror(errno)是错误的——只有当recv()返回-1时,errno才会被设置为有效错误码;当recv()返回0或正数时,errno的值是未定义的(大概率是之前的成功状态),所以你看到的"Success"其实是无效输出。
  • 另外,你的服务端每次只recv4字节,如果客户端发送的数据超过这个长度,剩余数据会留在套接字接收缓冲区,也会导致select()持续触发就绪,直到缓冲区数据被读完。

解决方案:正确实现持久连接

select()本身就是实现持久连接的合适工具,关键是正确处理套接字的各种就绪场景。你需要做以下几点:

1. 严格处理recv()的所有返回值

  • ret == 0:客户端主动关闭连接,服务器应关闭对应套接字,并从master集合中移除它。
  • ret < 0:发生错误(比如连接重置),同样需要关闭套接字并清理。
  • ret > 0:才处理收到的数据,同时注意处理数据不完整的情况。

2. 保持套接字在监控集合中

只要客户端没有主动请求关闭(发送"DEL"),也没有发生错误,就不要关闭套接字,也不要从master集合中移除它——这样select()会持续监控该套接字,有新数据到来时会再次触发处理。

修复后的服务端代码

下面是调整后的核心代码,重点修改了客户端套接字的处理逻辑:

int main () {
    int ret, sd, new_sd, len, i;
    char buffer[1024];
    fd_set master;
    fd_set read_fds;
    int fdmax;
    struct sockaddr_in my_addr, cl_addr; // 补充cl_addr的定义

    FD_ZERO(&master);
    FD_ZERO(&read_fds);

    sd = socket(AF_INET, SOCK_STREAM, 0);
    memset(&my_addr, 0, sizeof(my_addr));
    my_addr.sin_family = AF_INET;
    my_addr.sin_port = htons(4257);
    my_addr.sin_addr.s_addr = INADDR_ANY;

    ret = bind(sd, (struct sockaddr*)&my_addr, sizeof(my_addr));
    if(ret<0) {
        perror("bind error");
        exit(1);
    }

    ret = listen(sd,10);
    if(ret<0) {
        perror("listen error");
        exit(-1);
    }

    FD_SET(sd, &master);
    fdmax = sd;

    for(;;) {
        read_fds = master;
        select(fdmax+1, &read_fds, NULL, NULL, NULL);

        for(i=0; i<=fdmax; i++) {
            if(FD_ISSET(i, &read_fds)) {
                if(i == sd) {
                    len = sizeof(cl_addr);
                    new_sd = accept(sd, (struct sockaddr*)&cl_addr, &len);
                    FD_SET(new_sd, &master);
                    if(new_sd > fdmax)
                        fdmax = new_sd;
                } else {
                    ret = recv(i, buffer, sizeof(buffer)-1, 0); // 留1字节存字符串结束符
                    if(ret == 0) {
                        // 客户端主动关闭连接
                        printf("Client %d closed connection.\n", i);
                        close(i);
                        FD_CLR(i, &master);
                        // 可选:更新fdmax,如果关闭的是当前最大值
                        if(i == fdmax) {
                            while(fdmax > 0 && !FD_ISSET(fdmax, &master)) {
                                fdmax--;
                            }
                        }
                    } else if(ret < 0) {
                        // 接收错误
                        perror("recv error");
                        close(i);
                        FD_CLR(i, &master);
                        if(i == fdmax) {
                            while(fdmax > 0 && !FD_ISSET(fdmax, &master)) {
                                fdmax--;
                            }
                        }
                    } else {
                        // 成功接收数据,添加字符串结束符
                        buffer[ret] = '\0';
                        printf("Received from %d: %s\n", i, buffer);

                        if(strcmp(buffer, "DEL") == 0) {
                            // 客户端请求关闭
                            close(i);
                            FD_CLR(i, &master);
                            printf("%d closed by request.\n", i);
                            if(i == fdmax) {
                                while(fdmax > 0 && !FD_ISSET(fdmax, &master)) {
                                    fdmax--;
                                }
                            }
                        } else {
                            // 回发数据,用实际接收的字节数,而非strlen
                            ret = send(i, buffer, ret, 0);
                            if(ret < 0) {
                                perror("send error");
                                close(i);
                                FD_CLR(i, &master);
                                if(i == fdmax) {
                                    while(fdmax > 0 && !FD_ISSET(fdmax, &master)) {
                                        fdmax--;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    close(sd);
    return 0;
}

关键修改点说明

  1. 补充cl_addr定义:原代码中使用了未定义的cl_addr,修复编译错误。
  2. 正确处理recv返回值:新增ret==0和ret<0的分支,避免无限循环。
  3. 安全处理字符串:recv时预留1字节存'\0',避免缓冲区溢出和字符串处理错误。
  4. 更新fdmax:当关闭的套接字是当前最大值时,遍历master集合更新fdmax,避免select监控不必要的文件描述符。
  5. 用实际接收长度发送数据:避免strlen()在处理非字符串数据时出错。

持久连接的额外建议

  • 如果需要处理大量客户端,可以将套接字设置为非阻塞模式,结合select()实现更高效的IO处理,但会增加代码复杂度。
  • 给select()添加超时时间,方便做定时任务(比如清理超时的空闲连接)。
  • 可以给每个连接维护空闲时间,超过阈值后主动关闭,避免资源浪费。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 11:04:05