You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WaitForMultipleObjects监听Socket永久阻塞问题修复方案

WaitForMultipleObjects监听Socket永久阻塞问题修复

问题场景

  • 编写单元测试验证WaitForMultipleObjects与Socket配合使用的有效性,实现简易UDP服务端-客户端通信协议
  • 服务端逻辑:绑定Socket后调用WaitForMultipleObjects等待客户端数据,收到数据后打印内容随即休眠5秒
  • 故障现象:客户端在服务端5秒休眠期内发送2条消息时,第一条可正常显示,第二条会导致WaitForMultipleObjects永久阻塞
  • 实际项目约束:需要同时等待Socket和HANDLE类型的Windows事件对象,无法使用仅支持Socket的select方案,必须掌握WaitForMultipleObjects监听Socket的正确用法

故障根因

代码存在3个核心错误,直接导致事件丢失、永久阻塞:

  1. 违规手动重置WSA事件:通过WSAEventSelect绑定的Socket事件对象,禁止手动调用ResetEvent。该类事件的状态由Winsock内核自动管理:对应网络事件触发时内核自动将事件置为有信号,当所有待处理网络IO被处理完成后,内核会自动将事件重置为无信号。手动提前重置会直接清掉休眠期间新数据包触发的事件信号,后续等待逻辑再也收不到通知。
  2. 事件触发后仅读取一次数据:UDP是数据报协议,休眠期间内核缓冲区可能堆积多个数据包。WaitForMultipleObjects返回仅代表至少存在1个待读取数据包,而非恰好1个。仅读取一次就回到等待逻辑,剩余数据包虽然留在缓冲区,但事件已经被手动重置,内核不会重复触发通知,直接导致永久阻塞。
  3. 未初始化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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 04:15:41