4、UDP通信实战:UDP特点、sendto/recvfrom函数、UDP广播与组播、UDP丢包处理
UDP 这个协议,说实话,刚入行的时候我总觉得它「不太靠谱」。没有连接、没有确认、没有重传,数据发出去就像扔出去的纸飞机——能不能到、到了是不是完整,全看运气。但做了几年网络编程之后,我越来越喜欢它。为什么?因为它快,而且足够简单。
这一章,我们就来把 UDP 彻底聊透。从它的核心特点,到 sendto/recvfrom 这对黄金搭档,再到广播、组播,最后聊聊那个让人头疼的丢包问题。
4.1 UDP 到底「野」在哪?
UDP 的全称是 User Datagram Protocol,用户数据报协议。它和 TCP 最大的区别就一个字:无连接。
TCP 像打电话——先拨号、接通、说话、挂断。UDP 像寄明信片——写好地址扔邮筒,对方收不收得到,你不管。
我个人总结了 UDP 的三个核心特点:
- 无连接:不需要三次握手,直接发数据。节省了建立连接的时间。
- 不可靠:数据可能丢、可能乱序、可能重复。UDP 不保证送达。
- 面向报文:应用层交给 UDP 多大的数据,UDP 就原样发送,不会像 TCP 那样拆分成段。
你想想看,这三个特点放在一起,意味着什么?意味着 UDP 的延迟极低,适合实时性要求高的场景。比如视频通话、在线游戏、DNS 查询。我在项目中做过一个实时数据采集系统,用的就是 UDP。因为数据每秒来几千条,丢几条无所谓,但延迟必须控制在毫秒级。TCP 的重传机制反而成了累赘。
- 视频/音频直播
- 在线游戏(尤其是 FPS 类)
- DNS 查询
- SNMP 网络管理
- 物联网传感器数据上报
4.2 sendto 和 recvfrom:UDP 的收发双雄
TCP 用 connect/accept 建立连接后,直接用 read/write 收发数据。UDP 不一样,它用 sendto 和 recvfrom。这两个函数每次调用都要带上对方的地址信息。
先看原型:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数看着多,其实核心就两个:dest_addr 和 src_addr。sendto 告诉内核「发给谁」,recvfrom 告诉内核「谁发的」。
我刚开始用的时候犯过一个低级错误:recvfrom 的 addrlen 参数忘记初始化。结果 recvfrom 一直返回 -1,errno 是 EINVAL。查了半天才发现,addrlen 必须初始化为 sizeof(struct sockaddr_in)。嗯,这里要注意。
下面是一个最简单的 UDP 收发示例:
// UDP 发送端
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
char *msg = "Hello UDP!";
sendto(sockfd, msg, strlen(msg), 0,
(struct sockaddr *)&server_addr, sizeof(server_addr));
close(sockfd);
// UDP 接收端
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in local_addr;
local_addr.sin_family = AF_INET;
local_addr.sin_port = htons(8888);
local_addr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd, (struct sockaddr *)&local_addr, sizeof(local_addr));
char buf[1024];
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int n = recvfrom(sockfd, buf, sizeof(buf)-1, 0,
(struct sockaddr *)&client_addr, &addr_len);
buf[n] = '\0';
printf("收到: %s\n", buf);
close(sockfd);
注意看,接收端必须调用 bind 绑定端口,否则内核不知道把数据交给谁。发送端可以不 bind,内核会自动分配一个临时端口。
4.3 UDP 广播:一呼百应
广播是 UDP 独有的能力。TCP 是点对点的,UDP 可以一发多收。广播地址通常是 xxx.xxx.xxx.255,或者 255.255.255.255。
实现广播需要两步:
- 设置 socket 选项
SO_BROADCAST - 发送到广播地址
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
int broadcast = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));
struct sockaddr_in broadcast_addr;
broadcast_addr.sin_family = AF_INET;
broadcast_addr.sin_port = htons(9999);
inet_pton(AF_INET, "192.168.1.255", &broadcast_addr.sin_addr);
char *msg = "广播消息";
sendto(sockfd, msg, strlen(msg), 0,
(struct sockaddr *)&broadcast_addr, sizeof(broadcast_addr));
接收端不需要特殊设置,只要 bind 到对应端口,就能收到广播消息。
4.4 UDP 组播:精准投递
组播比广播更优雅。它使用 D 类 IP 地址(224.0.0.0 ~ 239.255.255.255)。只有加入了某个组播组的设备,才能收到发往该组的数据。
组播发送端和普通 UDP 发送几乎一样,只是目标地址换成组播地址:
struct sockaddr_in mcast_addr;
mcast_addr.sin_family = AF_INET;
mcast_addr.sin_port = htons(10000);
inet_pton(AF_INET, "224.0.0.88", &mcast_addr.sin_addr);
接收端需要加入组播组:
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.88");
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
组播的好处是:只有感兴趣的主机才会收到数据,不会像广播那样打扰所有设备。我在做视频监控项目时,就用组播把摄像头画面分发给多个客户端,效果很好。
4.5 UDP 丢包处理:别指望协议,自己来
UDP 丢包是常态,不是异常。为什么?因为 UDP 没有流量控制,没有拥塞控制。发送端发得太快,接收端来不及收,或者中间路由器缓冲区满了,数据包就直接被丢弃。
我做过一个测试:在本地回环地址(127.0.0.1)上,UDP 几乎不丢包。但一旦换成真实网络,丢包率可能达到 5%~10%,甚至更高。
那怎么办?我的经验是:应用层自己做可靠性保障。
常用的方法有:
- 序列号:每个数据包带上一个递增的序号,接收端检测是否有缺失。
- 超时重传:发送端启动定时器,如果超时没收到确认,就重发。
- ACK 确认:接收端收到数据后,回复一个确认包。
- 前向纠错(FEC):发送冗余数据,接收端即使丢了一部分也能恢复。
下面是一个简单的序列号 + 超时重传示例:
// 发送端
uint16_t seq = 0;
while (1) {
struct packet pkt;
pkt.seq = seq++;
memcpy(pkt.data, "hello", 5);
sendto(sockfd, &pkt, sizeof(pkt), 0, ...);
// 等待 ACK,超时重传
struct timeval tv = {1, 0}; // 1秒超时
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
if (recvfrom(sockfd, &ack, sizeof(ack), 0, ...) < 0) {
// 超时,重传
sendto(sockfd, &pkt, sizeof(pkt), 0, ...);
}
}
4.6 本章知识体系
下面这张图,把 UDP 的核心知识点串起来了。从 socket 创建,到收发函数,再到广播/组播,最后是丢包处理策略。你可以把它当作一个快速参考。
UDP 看似简单,但用好它并不容易。我个人觉得,UDP 编程的核心不在于「怎么发」,而在于「发丢了怎么办」。理解了这一点,你才算真正掌握了 UDP。
好了,这一章就到这里。代码不多,但每一行都值得你亲手敲一遍。尤其是广播和组播,找个局域网环境试试,你会对 UDP 有更直观的感受。
公众号:蓝海资料掘金营,微信deep3321