使用select()管理多Socket异常:无法进入accept_clients()函数求排查
问题排查:select()多客户端无阻塞服务器无法接受连接的修复方案
我仔细看了你的代码,发现几个核心问题导致服务器无法正常进入连接处理逻辑,甚至连系统的accept调用都没正确执行。下面拆解问题并给出修复后的完整代码:
核心错误点
- 函数名与系统API重名:你自定义的
accept和recv函数和WinSock的系统API完全同名,导致调用混乱——比如你在自定义accept里调用accept(sock, ...)时,实际递归调用了自己的函数,根本没真正触发系统的连接接受逻辑! - 全局fd_set被反复清零:所有函数共用全局的
sset,但每次调用自定义accept或recv时都执行FD_ZERO(&sset),之前添加的socket被清空,select完全无法检测多个socket的状态。 - select使用逻辑错误:你没有在主循环中统一管理所有需要检测的socket(监听socket+已连接客户端socket),而是分散在各个函数中单独处理,这完全违背了select的使用范式。
- recv逻辑颠倒:自定义
recv函数中,当recv(x->cs, ...)返回>0(成功收到数据)时,却调用Server_Status(CLIENT_DIS),完全搞反了接收和断开的逻辑;同时没正确处理客户端主动断开的情况(recv返回0)。 - 客户端状态维护混乱:
PR_CONS的增减逻辑不准确,比如重复处理槽位会导致计数错误,断开时的socket资源也没清理。
修复后的完整代码
#pragma comment(lib, "ws2_32.lib") #include <WinSock2.h> #include <stdio.h> #include <signal.h> #include <conio.h> #include <assert.h> #include <Windows.h> #include <iostream> // 常量定义 #define CLIENT_CON 10 #define CLIENT_DIS 20 #define BF_SZ 100 #define MAX_CONS 5 #define TIMEVAL_SEC 0 #define TIMEVAL_USEC 100000 // 调整为100ms超时,避免高频轮询 SOCKET listen_sock; // 重命名监听socket,避免混淆 int PR_CONS = 0; // 客户端结构体 struct _client { bool con; // 是否已连接 sockaddr_in addr; // 客户端地址信息 SOCKET cs; // 客户端socket }; _client client[MAX_CONS]; fd_set read_fds; // 专门用于select的读集合,主循环统一管理 // 重命名自定义函数,避免和系统API冲突 int accept_new_client(_client*); int recv_client_data(_client*, char*, int); void Server_Status(int); void handle_new_connections(); void handle_client_messages(); int main() { int i = 1; int port = 5150; WSADATA ws; printf(" Echo Server (Multiple client support)\n"); sockaddr_in ser; ser.sin_family = AF_INET; ser.sin_addr.S_un.S_addr = INADDR_ANY; ser.sin_port = htons(port); // 初始化WinSock if (WSAStartup(MAKEWORD(2, 2), &ws) != 0) { printf("WSAStartup failed: %d\n", WSAGetLastError()); return 1; } // 创建监听socket listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (listen_sock == INVALID_SOCKET) { printf("socket failed: %d\n", WSAGetLastError()); WSACleanup(); return 1; } // 设置地址重用 setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, (char*)&i, sizeof(i)); // 绑定端口 if (bind(listen_sock, (SOCKADDR*)&ser, sizeof(ser)) == SOCKET_ERROR) { printf("bind failed: %d\n", WSAGetLastError()); closesocket(listen_sock); WSACleanup(); return 1; } // 开始监听 if (listen(listen_sock, 5) == SOCKET_ERROR) { printf("listen failed: %d\n", WSAGetLastError()); closesocket(listen_sock); WSACleanup(); return 1; } printf("Listening on port %d...\n", port); // 设置监听socket为非阻塞 unsigned long b = 1; if (ioctlsocket(listen_sock, FIONBIO, &b) == SOCKET_ERROR) { printf("ioctlsocket failed: %d\n", WSAGetLastError()); closesocket(listen_sock); WSACleanup(); return 1; } // 初始化客户端数组 for (int i = 0; i < MAX_CONS; i++) { client[i].con = false; client[i].cs = INVALID_SOCKET; } timeval timeout; timeout.tv_sec = TIMEVAL_SEC; timeout.tv_usec = TIMEVAL_USEC; while (true) { // 每次循环重新构建读集合:清零后加入监听socket和所有已连接客户端socket FD_ZERO(&read_fds); FD_SET(listen_sock, &read_fds); for (int i = 0; i < MAX_CONS; i++) { if (client[i].con && client[i].cs != INVALID_SOCKET) { FD_SET(client[i].cs, &read_fds); } } // 调用select检测可读事件 int activity = select(0, &read_fds, NULL, NULL, &timeout); if (activity == SOCKET_ERROR) { printf("select failed: %d\n", WSAGetLastError()); break; } else if (activity == 0) { // 超时无事件,继续循环 continue; } // 处理新连接 if (FD_ISSET(listen_sock, &read_fds)) { handle_new_connections(); } // 处理客户端消息 handle_client_messages(); } // 清理资源 closesocket(listen_sock); for (int i = 0; i < MAX_CONS; i++) { if (client[i].cs != INVALID_SOCKET) { closesocket(client[i].cs); } } WSACleanup(); return 0; } int accept_new_client(_client* x) { int addr_len = sizeof(sockaddr_in); unsigned long b = 1; // 调用系统的accept函数(已避免重名冲突) x->cs = accept(listen_sock, (SOCKADDR*)&x->addr, &addr_len); if (x->cs == INVALID_SOCKET) { int err = WSAGetLastError(); // 非阻塞模式下无新连接时返回WSAEWOULDBLOCK,属于正常情况 if (err != WSAEWOULDBLOCK) { printf("accept failed: %d\n", err); } return false; } // 设置客户端socket为非阻塞 if (ioctlsocket(x->cs, FIONBIO, &b) == SOCKET_ERROR) { printf("ioctlsocket for client failed: %d\n", WSAGetLastError()); closesocket(x->cs); return false; } x->con = true; return true; } int recv_client_data(_client* x, char* buffer, int sz) { int recv_len = recv(x->cs, buffer, sz - 1, 0); // 留一位存字符串结束符 if (recv_len > 0) { // 收到数据,添加结束符 buffer[recv_len] = '\0'; return recv_len; } else if (recv_len == 0) { // 客户端主动断开连接 return 0; } else { int err = WSAGetLastError(); // 非阻塞模式下无数据时返回WSAEWOULDBLOCK,属于正常情况 if (err != WSAEWOULDBLOCK) { printf("recv failed: %d\n", err); } return -1; } } void handle_new_connections() { if (PR_CONS >= MAX_CONS) { printf("Max clients reached, rejecting new connection\n"); // 接受后直接关闭,避免客户端等待 SOCKET temp_sock = accept(listen_sock, NULL, NULL); if (temp_sock != INVALID_SOCKET) { closesocket(temp_sock); } return; } // 找到第一个空闲客户端槽位 for (int i = 0; i < MAX_CONS; i++) { if (!client[i].con) { if (accept_new_client(&client[i])) { Server_Status(CLIENT_CON); printf(" - Client IP: %s\n", inet_ntoa(client[i].addr.sin_addr)); break; } } } } void handle_client_messages() { char buffer[BF_SZ]; for (int i = 0; i < MAX_CONS; i++) { if (client[i].con && FD_ISSET(client[i].cs, &read_fds)) { int recv_len = recv_client_data(&client[i], buffer, BF_SZ); if (recv_len > 0) { // 打印收到的消息并回显给客户端 printf("Received from client %d: %s\n", i, buffer); send(client[i].cs, buffer, recv_len, 0); } else if (recv_len == 0) { // 客户端断开,清理资源 Server_Status(CLIENT_DIS); printf(" - Client %d disconnected\n", i); closesocket(client[i].cs); client[i].con = false; client[i].cs = INVALID_SOCKET; } } } } void Server_Status(int msg) { if (msg == CLIENT_CON) { PR_CONS++; printf("Client has connected (Total: %d)", PR_CONS); } else if (msg == CLIENT_DIS) { PR_CONS--; printf("Client has disconnected (Total: %d)", PR_CONS); } else { printf("Unknown status message"); } }
关键改动说明
- 解决函数重名冲突:把自定义的
accept和recv分别改为accept_new_client和recv_client_data,确保能正确调用系统API。 - 统一管理fd_set:在主循环中每次重新构建读集合,包含监听socket和所有已连接客户端socket,让select一次性检测所有需要关注的事件。
- 正确处理非阻塞socket:区分非阻塞模式下的正常返回(
WSAEWOULDBLOCK)和真正的错误,避免误报。 - 修复recv逻辑:正确识别recv返回值的含义,处理数据接收、客户端断开两种核心场景。
- 完善资源清理:客户端断开时关闭socket并重置结构体状态,避免无效资源占用。
- 添加错误检查:给WinSock关键调用添加错误处理,方便调试定位问题。
修改后服务器就能正常接受多客户端连接,并无阻塞处理消息了。
内容的提问来源于stack exchange,提问作者Dota
相关产品推荐
相关产品推荐

