udp通信无连接,sendto/recvfrom直接操作ip+端口,无握手、重传、顺序保证;需注意跨平台初始化、地址清零、非阻塞设置及变长数据包的安全收发。

UDP通信必须先理解“无连接”意味着什么
UDP不建立连接,sendto 和 recvfrom 直接操作IP+端口,没有三次握手、没有重传、没有顺序保证。这意味着:发送方调用 sendto 成功,只代表数据进了内核发送缓冲区,不代表对方收到;接收方调用 recvfrom 时,必须自己解析源地址(sockaddr_in),否则无法回包。
常见错误现象:sendto 返回成功但对方收不到——大概率是目标IP写错、防火墙拦截、或没绑定本地端口(客户端可省略,但显式 bind 更可控);recvfrom 阻塞却一直等不到数据——检查是否用 SOCK_DGRAM 创建 socket,是否监听了正确端口,是否启用了 INADDR_ANY 而非 127.0.0.1。
Windows和Linux下socket初始化差异不能忽略
Windows必须先调用 WSAStartup,结束前调用 WSACleanup;Linux直接用 socket 即可。跨平台代码里容易漏掉 Windows 初始化,导致 socket() 返回 INVALID_SOCKET(Windows)或 -1(Linux),但错误码含义不同——Windows 查 WSAGetLastError(),Linux 查 errno。
-
SOCK_DGRAM必须和AF_INET搭配,别误用AF_UNIX - 设置非阻塞需用
ioctlsocket(Windows)或fcntl(Linux),不能混用 - 本地地址结构体要清零:
memset(&addr, 0, sizeof(addr)),否则sin_zero未初始化可能引发奇怪行为
如何安全地收发变长数据包
UDP单次 recvfrom 最多读取65507字节(IPv4),但实际应用中很少发这么大包。更关键的是:你永远不知道一次 recvfrom 会收到多少字节——它按“一个UDP报文”为单位返回,不是流式读取。所以不能假设每次都能读满缓冲区,也不能把多次 recvfrom 的数据拼起来当TCP用。
立即学习“C++免费学习笔记(深入)”;
实操建议:
- 定义固定头部(如4字节长度字段 + 4字节类型字段),再跟实际载荷,避免粘包/截断判断困难
- 接收缓冲区大小至少设为65536,防止截断(
setsockopt(..., SOL_SOCKET, SO_RCVBUF, ...)) - 发送前检查数据长度是否超
65507 - sizeof(IP首部) - sizeof(UDP首部),超了就分包或换TCP
示例片段(接收端):
char buf[65536];
socklen_t addr_len = sizeof(struct sockaddr_in);
ssize_t n = recvfrom(sockfd, buf, sizeof(buf)-1, 0, (struct sockaddr*)&addr, &addr_len);
if (n > 0) {
buf[n] = '\0'; // 注意:UDP数据不带'\0',字符串处理前必须手动补
}为什么用 select 或 epoll 前要先设非阻塞
select 只告诉你“这个 socket 可读”,不代表 recvfrom 一定能立刻返回有效数据——如果 UDP 报文在 select 返回后、recvfrom 执行前被内核丢弃(比如缓冲区满),recvfrom 仍会阻塞(除非 socket 是非阻塞模式)。
所以必须:
- 创建 socket 后立即设为非阻塞(
ioctlsocket(..., FIONBIO, &on)或fcntl(..., O_NONBLOCK)) -
select返回可读后,recvfrom若返回-1且errno == EAGAIN或WSA_EWOULDBLOCK,说明已无数据,应退出本次循环 - 别在
select中等待写事件(UDP socket 几乎总是可写的,意义不大)
真正需要关注的,是接收缓冲区是否溢出——这通常意味着处理太慢,而不是 socket 不可读。
UDP 的简单性是假象。真正难的是边界处理:超时重传要不要加、校验和怎么算、丢包后业务层怎么恢复、多线程访问 socket 时锁怎么加。这些不会出现在“Hello World”例子里,但上线后第一个报警往往就来自这里。











