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
相关产品推荐
相关产品推荐

