WSAWaitForMultipleEvents触发数据可用事件但recv无数据的原因与解决方法
问题原因
- 未校验网络事件的实际类型:调用
WSAEnumNetworkEvents拿到WSANETWORKEVENTS结构后,没有校验lNetworkEvents字段是否真的包含FD_READ标志,也没有检查iErrorCode[FD_READ_BIT]是否存在网络错误。当网络抖动时,可能触发连接异常、端口不可达等隐含事件,此时虽然事件对象被触发,但实际没有可读数据,调用recv自然会返回空或者WSAEWOULDBLOCK错误。 - 非阻塞模式的单次读取缺陷:
WSAEventSelect会自动将socket设置为非阻塞模式,忽略你之前通过ioctlsocket设置的阻塞模式。在高延迟场景下,可能出现内核缓冲区只收到部分IP分片、还未组装成完整可用数据就触发事件通知的边缘情况,此时单次调用recv会因为数据未就绪返回空,等分片全部组装完成后再次调用就能读到数据。 - 事件重置的时序冲突:
WSAEnumNetworkEvents会自动重置关联的手动重置事件对象,如果在该调用完成后、recv调用前,刚好有新的完整数据到达socket缓冲区,由于事件已经被重置,本次不会再触发等待逻辑,但数据已经存在,所以后续重试就能读到。
解决方案
无需额外轮询即可修复的方案如下:
- 新增网络事件校验逻辑:调用
WSAEnumNetworkEvents后,首先检查lNetworkEvents是否包含FD_READ,同时检查对应错误位是否为0,只有确认是合法读事件后再执行读取逻辑。 - 循环读取直到缓冲区为空:在非阻塞模式下,触发
FD_READ后需要循环调用recv,直到返回值小于0且WSAGetLastError()返回WSAEWOULDBLOCK,表示内核缓冲区的所有数据已经被读取完毕,漏读的数据会被本次循环兜底处理。 - 规范事件对象属性:确保socket关联的事件是手动重置类型的事件,符合
WSAEventSelect的调用要求,避免自动重置事件导致的通知丢失问题。
修改后代码示例
bool result = false; DWORD ret; WSANETWORKEVENTS NetworkEvents; ret = WSAWaitForMultipleEvents(2, events, FALSE, WSA_INFINITE, FALSE); if (ret == WAIT_OBJECT_0 + 0) { if (WSAEnumNetworkEvents(sock, events[0], &NetworkEvents) == 0) { // 校验合法读事件 if ((NetworkEvents.lNetworkEvents & FD_READ) && NetworkEvents.iErrorCode[FD_READ_BIT] == 0) { int recvRet; // 循环读取所有可用数据 while ((recvRet = recv(sock, (char*)buf, static_cast<size_t>(len), flags)) > 0) { // 此处处理长度为recvRet的读取数据 result = true; } // 非WSAEWOULDBLOCK错误代表socket异常,按需自行处理 if (recvRet == SOCKET_ERROR && WSAGetLastError() != WSAEWOULDBLOCK) { result = false; } } } } else if (ret == WAIT_OBJECT_0 + 1) { // 此处处理退出事件逻辑 }
内容的提问来源于stack exchange,提问作者Ogi
相关产品推荐
相关产品推荐

