Windows平台select模型下处理SOCKET套接字的最佳方法
Windows平台无STL容器的
select套接字处理方案 你现有代码的问题本质是直接照搬了Linux平台select的遍历逻辑,这套逻辑完全不适用于Winsock实现:
- Linux的
fd_set是位掩码结构,套接字是从0开始递增的int类型,因此可以从0遍历到最大fd值判断是否置位 - Windows的
SOCKET是UINT_PTR类型的句柄,数值不连续、可能远大于FD_SETSIZE,且Winsock的fd_set内部是计数+SOCKET数组的结构,根本不存在用套接字值当位索引的逻辑,强转int+从0遍历的写法从根上就是错的。
不需要依赖vector/unordered_map等STL容器,直接用C风格固定长度数组就能实现正确逻辑,具体实现方式如下:
- 定义长度为
FD_SETSIZE的SOCKET类型固定数组,存储所有活跃套接字(服务端监听套接字+已接受的客户端套接字) - 单独维护一个整数计数变量,记录数组中当前有效套接字的总数
- 数组初始化时全部填充
INVALID_SOCKET(Winsock定义的无效套接字值,不要用Linux的-1),第一个位置存入服务端监听套接字 - 每次调用
select前,遍历自定义数组将所有有效套接字通过FD_SET加入读集合,Windows下select第一个参数无实际意义,直接传0即可 select返回后,遍历自定义的套接字数组,直接用FD_ISSET判断当前SOCKET是否就绪,全程不需要做类型强转- 新客户端接入时,将新套接字存入数组第一个空位,计数加1;客户端断开时,将对应数组位置重置为
INVALID_SOCKET即可。
修正后的代码实现
int Server::handler() { int iResult; timeval timeout; fd_set readFdSet; // 固定长度数组存储所有活跃套接字,无任何STL容器依赖 SOCKET sockList[FD_SETSIZE]; int sockCount = 1; // 初始化套接字列表 for (int i = 0; i < FD_SETSIZE; i++) { sockList[i] = INVALID_SOCKET; } sockList[0] = serv_sock; printf("FD_SETSIZE=%d\n", FD_SETSIZE); // 启动监听 iResult = listen(serv_sock, SOMAXCONN); if (iResult == SOCKET_ERROR) { return SOCKET_ERROR; } timeout.tv_sec = 5; timeout.tv_usec = 0; while (1) { FD_ZERO(&readFdSet); // 将所有有效套接字加入读检测集合 for (int i = 0; i < sockCount; i++) { if (sockList[i] != INVALID_SOCKET) { FD_SET(sockList[i], &readFdSet); } } printf("\tCopied active sockets to readfdset\n"); // Windows下select第一个参数被忽略,传0即可 int res = select(0, &readFdSet, NULL, NULL, &timeout); if (res == SOCKET_ERROR) { break; } if (res == 0) { // 超时逻辑按需处理 continue; } // 遍历自定义套接字列表处理就绪事件 for (int i = 0; i < sockCount; i++) { SOCKET curSock = sockList[i]; if (curSock == INVALID_SOCKET) { continue; } if (!FD_ISSET(curSock, &readFdSet)) { continue; } if (curSock == serv_sock) { // 处理新连接接入 SOCKET clientSock = accept(serv_sock, NULL, NULL); if (clientSock != INVALID_SOCKET) { if (sockCount < FD_SETSIZE) { sockList[sockCount++] = clientSock; } else { // 超过select支持的最大套接字数量,直接拒绝连接 closesocket(clientSock); } } } else { // 处理客户端数据收发 char recvBuf[1024]; int recvLen = recv(curSock, recvBuf, sizeof(recvBuf), 0); if (recvLen <= 0) { // 连接断开,清理套接字 closesocket(curSock); sockList[i] = INVALID_SOCKET; } else { // 业务逻辑处理recvBuf即可 } } } } // 退出前清理所有套接字资源 for (int i = 0; i < sockCount; i++) { if (sockList[i] != INVALID_SOCKET) { closesocket(sockList[i]); } } return 0; }
额外优化提示
如果想减少遍历无效数组项的开销,客户端断开时不需要保留数组空位:直接把数组最后一个有效套接字挪到当前断开的位置,再把sockCount减1即可,全程不需要内存动态分配,逻辑复杂度极低。
不要尝试直接遍历
fd_set内部的fd_array成员,虽然Winsock的内部结构是公开的,但自己维护套接字列表的方式对连接增删的可控性更高,不会出现select返回后集合被修改导致的逻辑异常。
内容的提问来源于stack exchange,提问作者loaded_dypper
相关产品推荐
相关产品推荐

