WaitForMultipleObjects监听Socket永久阻塞问题修复方案
WaitForMultipleObjects监听Socket永久阻塞问题修复
问题场景
- 编写单元测试验证
WaitForMultipleObjects与Socket配合使用的有效性,实现简易UDP服务端-客户端通信协议 - 服务端逻辑:绑定Socket后调用
WaitForMultipleObjects等待客户端数据,收到数据后打印内容随即休眠5秒 - 故障现象:客户端在服务端5秒休眠期内发送2条消息时,第一条可正常显示,第二条会导致
WaitForMultipleObjects永久阻塞 - 实际项目约束:需要同时等待Socket和HANDLE类型的Windows事件对象,无法使用仅支持Socket的
select方案,必须掌握WaitForMultipleObjects监听Socket的正确用法
故障根因
代码存在3个核心错误,直接导致事件丢失、永久阻塞:
- 违规手动重置WSA事件:通过
WSAEventSelect绑定的Socket事件对象,禁止手动调用ResetEvent。该类事件的状态由Winsock内核自动管理:对应网络事件触发时内核自动将事件置为有信号,当所有待处理网络IO被处理完成后,内核会自动将事件重置为无信号。手动提前重置会直接清掉休眠期间新数据包触发的事件信号,后续等待逻辑再也收不到通知。 - 事件触发后仅读取一次数据:UDP是数据报协议,休眠期间内核缓冲区可能堆积多个数据包。
WaitForMultipleObjects返回仅代表至少存在1个待读取数据包,而非恰好1个。仅读取一次就回到等待逻辑,剩余数据包虽然留在缓冲区,但事件已经被手动重置,内核不会重复触发通知,直接导致永久阻塞。 - 未初始化
recv_len变量:原代码sendto调用时使用了未赋值的recv_len,触发未定义行为,可能导致回包失败、内存错误等异常。
正确使用规范
使用WaitForMultipleObjects监听Socket必须严格遵守WSA事件模型规则:
- 用
WSACreateEvent创建事件对象,通过WSAEventSelect绑定Socket和需要监听的网络事件(如FD_READ、FD_WRITE、FD_CLOSE) - 事件对象和其他普通Windows HANDLE对象可直接按顺序放入
WaitForMultipleObjects的句柄数组,同时等待,按返回值判断触发对象即可 - 等待函数返回、确认是Socket事件触发后,绝对不要手动重置事件
- 必须将Socket设置为非阻塞模式,循环调用对应IO函数直到返回
WSAEWOULDBLOCK错误,确认所有堆积的待处理数据全部读完,内核才会自动重置事件,新数据到来时会再次触发信号
修复后服务端代码
/* 修复后 UDP Server */ #include<winsock2.h> #include<windows.h> #include <ws2tcpip.h> #include<stdio.h> #include<iostream> #pragma comment(lib,"ws2_32.lib") //Winsock Library #define BUFLEN 512 // 缓冲区最大长度 #define PORT 8888 // UDP监听端口 int main() { SOCKET s; struct sockaddr_in server, si_other; int slen , recv_len; char buf[BUFLEN]; WSADATA wsa; HANDLE SEvent; slen = sizeof(si_other) ; // 初始化Winsock printf("\nInitialising Winsock..."); if (WSAStartup(MAKEWORD(2,2),&wsa) != 0) { printf("Failed. Error Code : %d",WSAGetLastError()); exit(EXIT_FAILURE); } printf("Initialised.\n"); // 创建Socket if((s = socket(AF_INET , SOCK_DGRAM , 0 )) == INVALID_SOCKET) { printf("Could not create socket : %d" , WSAGetLastError()); } printf("Socket created.\n"); // 填充服务端地址结构 server.sin_family = AF_INET; server.sin_addr.s_addr = INADDR_ANY; server.sin_port = htons( PORT ); // 绑定端口 if( bind(s ,(struct sockaddr *)&server , sizeof(server)) == SOCKET_ERROR) { printf("Bind failed with error code : %d" , WSAGetLastError()); exit(EXIT_FAILURE); } puts("Bind done"); // 将Socket设置为非阻塞模式,支持循环读取所有缓冲区数据 u_long mode = 1; ioctlsocket(s, FIONBIO, &mode); // 创建WSA事件,绑定FD_READ通知 SEvent = WSACreateEvent(); WSAEventSelect( s, SEvent, FD_READ); // 持续处理数据 while(1) { printf("Waiting for data...\n"); fflush(stdout); INT r = WaitForMultipleObjectsEx(1,&SEvent,FALSE,INFINITE,TRUE); if( r == WAIT_OBJECT_0) { // 循环读取所有待处理数据包,直到缓冲区读空 while (true) { memset(buf,'\0', BUFLEN); recv_len = recvfrom(s, buf, BUFLEN, 0, (struct sockaddr *) &si_other, &slen); if (recv_len == SOCKET_ERROR) { int err = WSAGetLastError(); if (err == WSAEWOULDBLOCK) { // 所有数据读取完成,退出循环,内核会自动重置事件 break; } printf("recvfrom() failed with error code : %d" , err); exit(EXIT_FAILURE); } // 打印客户端信息和收到的数据 printf("Received packet from %s:%d\n", inet_ntoa(si_other.sin_addr), ntohs(si_other.sin_port)); printf("Data: %s\n" , buf); // 回传相同数据给客户端 if (sendto(s, buf, recv_len, 0, (struct sockaddr*) &si_other, slen) == SOCKET_ERROR) { printf("sendto() failed with error code : %d" , WSAGetLastError()); exit(EXIT_FAILURE); } } // 移除原代码中手动ResetEvent的调用,禁止手动修改WSA事件状态 printf("Start 5s sleep...\n"); Sleep(5000); printf("Sleep end, continue waiting...\n"); } else { std::cerr<<"WaitForMultipleObject() Error ( "<<GetLastError()<<" )"<<std::endl; exit(0); } } closesocket(s); WSACleanup(); return 0; }
原客户端代码逻辑无核心问题,仅需注意替换不安全的
gets函数即可正常使用。
内容的提问来源于stack exchange,提问作者R27J02
相关产品推荐
相关产品推荐

