在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; }
关键修改点说明
- 补充
cl_addr定义:原代码中使用了未定义的cl_addr,修复编译错误。 - 正确处理recv返回值:新增
ret==0和ret<0的分支,避免无限循环。 - 安全处理字符串:recv时预留1字节存
'\0',避免缓冲区溢出和字符串处理错误。 - 更新fdmax:当关闭的套接字是当前最大值时,遍历master集合更新fdmax,避免select监控不必要的文件描述符。
- 用实际接收长度发送数据:避免
strlen()在处理非字符串数据时出错。
持久连接的额外建议
- 如果需要处理大量客户端,可以将套接字设置为非阻塞模式,结合select()实现更高效的IO处理,但会增加代码复杂度。
- 给select()添加超时时间,方便做定时任务(比如清理超时的空闲连接)。
- 可以给每个连接维护空闲时间,超过阈值后主动关闭,避免资源浪费。
内容的提问来源于stack exchange,提问作者MartiB
相关产品推荐
相关产品推荐

