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

UDP客户端recv分批读取数据异常:仅成功读取一次

UDP非阻塞客户端分批读取大报文异常问题

UDP服务器收到命令后返回8000字节数据(Wireshark确认已发送),但在Ubuntu 22.04环境下,非阻塞UDP客户端以1024字节分批调用recv读取时,仅首次读取成功(获取1024字节),后续调用均返回-1(错误码11,EWOULDBLOCK);若直接调用recv读取8000字节则可成功获取全部数据。

简化的客户端代码

class ClienSocket
{
public:
    void Init()
    {
        pollfd m_poll = {};
        m_poll.fd = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
        if(m_poll.fd == -1)
        {    
            throw std::runtime_error(GetLastError());
        }

        int optval = 1;
        setsockopt(m_poll.fd, SOL_SOCKET, SO_REUSEADDR, static_cast<const void *>(&optval), sizeof(int));
        int on = 1;
        if(ioctl(m_poll.fd, FIONBIO, &on) < 0)
        {
            throw std::runtime_error(std::string("failed to set the client socket non-blocking: ") + strerror(errno));
        }
    }

    void Run()
    {
        struct sockaddr_in serv_addr;
        m_servaddr.sin_family = AF_INET;
        m_servaddr.sin_addr.s_addr = inet_addr(m_address.c_str());
        m_servaddr.sin_port = htons(static_cast<uint16_t>(m_port));
        m_poll.events = POLLIN;
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_port = htons(m_port);        

        m_running = true;
        if(pthread_create(&m_readThread, nullptr, &ClienSocket::ReadThreadWrapper, this) != 0)
        {
            m_running = false;    
            throw std::runtime_error(std::string("thread creating error"));
        }
    }
    
    void ClienSocket::Write(const char *data, size_t size)
    {
        sendto(m_poll.fd, data, size, MSG_NOSIGNAL, reinterpret_cast<const struct sockaddr *>(&(m_servaddr)), sizeof(sockaddr_in));   
    }

    static void *ClienSocket::ReadThreadWrapper(void *ptr)
    {
        ClienSocket *instance = static_cast<ClienSocket *>(ptr);
        if(instance != nullptr)
        {
            return instance->ReadThreadFunc();
        }

        return nullptr;
    }

    void *ClienSocket::ReadThreadFunc()
    {
        while(m_running)
        {
            int retval = poll(&m_poll, 1, 1000);
            if(retval > 0)
            {
                if(m_poll.revents == POLLIN)
                {
                    bool readMore = true;
                    do
                    {
                        ssize_t readBytes = recv(m_poll.fd, m_readBuffer, READ_BUFFER_SIZE, 0);                        
                        std::cout << readBytes << ", " << errno << std::endl;
                        if (readBytes < 0)
                        {
                            if (errno != EWOULDBLOCK)
                            {
                                throw std::runtime_error(std::string("socket error"));                                
                            }
                        }
                        else if(readBytes == 0)
                        {
                            readMore = false;                            
                        }
                        else
                        {
                            ProcessData(m_readBuffer, readBytes);
                        }                        
                    }
                    while(readMore == true);
                }
            }
        }    
        return nullptr;
    }
    
    void ClienSocket::Wait()
    {
        if(m_running)
        {        
            pthread_join(m_readThread, nullptr);            
        }
    }
    
    void ProcessData(const char *data, size_t length)
    {
        std::cout << length << std::endl;
    }
    
private:
    bool m_running = false;
    int m_port = 3335;
    std::string m_address = "192.168.5.1";
    struct sockaddr_in m_servaddr;
    pollfd m_poll = {};
    pthread_t m_readThread;
    static constexpr size_t READ_BUFFER_SIZE = 1024;
    char m_readBuffer[READ_BUFFER_SIZE];
}

测试用例

ClienSocket client;
client.Init();
client.Run();
client.Write("hello", 5);
client.Wait();

客户端输出

1024, 0
-1, 11
-1, 11
-1, 11
-1, 11
-1, 11
...

问题原因与解决方法

核心原因

UDP是面向报文的协议,内核会把每个UDP报文完整存储在接收缓冲区中。当调用recv读取时,如果缓冲区大小小于报文长度,只会返回缓冲区能容纳的字节数,剩余的字节会被直接丢弃——UDP不会拆分报文,也不会缓存剩余部分供后续读取。这就是首次读1024字节后,后续读取返回EWOULDBLOCK的根本原因,剩下的6976字节已丢失。而直接用8000字节缓冲区读取时,能完整接收整个报文,因此成功。

解决办法

  • 使用足够大的接收缓冲区:确保recv的缓冲区大小不小于UDP报文的最大可能长度(至少8000字节,建议设置为UDP最大报文长度65507字节)。
  • 调整读取逻辑:移除分批循环读取的逻辑,因为UDP不支持拆分读取单个报文。修改后的ReadThreadFunc可简化为:
    void *ClienSocket::ReadThreadFunc()
    {
        while(m_running)
        {
            int retval = poll(&m_poll, 1, 1000);
            if(retval > 0)
            {
                if(m_poll.revents == POLLIN)
                {
                    ssize_t readBytes = recv(m_poll.fd, m_readBuffer, READ_BUFFER_SIZE, 0);                        
                    if (readBytes < 0)
                    {
                        if (errno != EWOULDBLOCK)
                        {
                            throw std::runtime_error(std::string("socket error"));                                
                        }
                    }
                    else if(readBytes > 0)
                    {
                        ProcessData(m_readBuffer, readBytes);
                    }
                }
            }
        }    
        return nullptr;
    }
    
  • 可选:扩大内核接收缓冲区:通过setsockopt设置SO_RCVBUF,确保内核接收缓冲区能容纳大报文,避免报文丢失:
    int rcv_buf_size = 65536; // 64KB,足够容纳最大UDP报文
    setsockopt(m_poll.fd, SOL_SOCKET, SO_RCVBUF, &rcv_buf_size, sizeof(rcv_buf_size));
    

内容的提问来源于stack exchange,提问作者folibis

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 21:21:01