UDP通信:无连接的数据报传输
UDP,全称是用户数据报协议。说实话,我第一次接触它的时候,觉得这协议也太「随意」了——发数据前连个招呼都不打。但后来在项目中用多了,才发现这种「随意」恰恰是它的魅力所在。
UDP的特点:简单粗暴,但快
UDP和TCP最大的区别,就是它不建立连接。你想发数据?直接扔出去就行。对方收没收到?不知道。数据顺序乱没乱?也不管。
听起来很不靠谱对吧?但正是这种「不靠谱」,让它拥有了极低的延迟。
UDP的核心特点:
- 无连接:发送前不需要三次握手,直接发
- 不可靠:不保证送达,不保证顺序
- 面向数据报:保留消息边界,一次sendto对应一次recvfrom
- 无拥塞控制:网络堵了?照发不误
- 轻量级:头部只有8字节,TCP是20字节
我在项目中遇到过一个问题:用TCP传实时视频流,结果网络一抖动,画面卡得不行。后来换成UDP,虽然偶尔花屏,但整体流畅度提升了一大截。说白了,有些场景下,「快」比「稳」更重要。
UDP客户端/服务器模型
UDP的编程模型比TCP简单得多。不需要listen,不需要accept,甚至连connect都可以省略。
来看一个最基础的UDP服务器:
// UDP服务器端 - echo server
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int sockfd;
struct sockaddr_in server_addr, client_addr;
char buffer[1024];
socklen_t addr_len = sizeof(client_addr);
// 1. 创建socket
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 2. 绑定地址
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(8888);
bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
// 3. 直接收数据,不需要listen
while(1) {
recvfrom(sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr*)&client_addr, &addr_len);
printf("收到: %s\n", buffer);
sendto(sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr*)&client_addr, addr_len);
}
close(sockfd);
return 0;
}
客户端更简单:
// UDP客户端
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_port = htons(8888);
sendto(sockfd, "Hello", 5, 0,
(struct sockaddr*)&server_addr, sizeof(server_addr));
recvfrom(sockfd, buffer, sizeof(buffer), 0, NULL, NULL);
printf("服务器回复: %s\n", buffer);
嗯,这里要注意:UDP的recvfrom和sendto每次都要指定对方地址。为什么?因为没有连接啊,内核不知道你要跟谁说话。
UDP的广播与组播
这是UDP独有的能力。TCP是点对点的,但UDP可以「一对多」。
广播:吼一嗓子,所有人都能听见
广播地址通常是xxx.xxx.xxx.255。比如局域网是192.168.1.x,广播地址就是192.168.1.255。
我曾经在一个智能家居项目里用过广播。设备上线时发个广播包,网关收到后就知道有新设备了。省去了手动配置IP的麻烦。
// 开启广播选项
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_addr.s_addr = inet_addr("192.168.1.255");
broadcast_addr.sin_port = htons(9999);
sendto(sockfd, "广播消息", 12, 0,
(struct sockaddr*)&broadcast_addr, sizeof(broadcast_addr));
注意:广播会消耗网络带宽。所有设备都会收到广播包,即使它们不关心。生产环境中别滥用,否则网络管理员会来找你喝茶。
组播:只发给感兴趣的人
组播比广播更优雅。它使用D类IP地址(224.0.0.0 ~ 239.255.255.255)。只有加入了某个组播组的设备,才会收到对应的数据。
// 加入组播组
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.1");
mreq.imr_interface.s_addr = INADDR_ANY;
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
组播的好处是:精确投递。只有订阅了该组的设备才会收到数据,不会像广播那样打扰所有人。
UDP与TCP的选择:什么时候用哪个?
这个问题没有标准答案。我个人的经验是:
| 场景 | 推荐协议 | 原因 |
|---|---|---|
| 文件传输、网页浏览 | TCP | 数据必须完整、有序 |
| 实时音视频 | UDP | 延迟敏感,丢几帧没关系 |
| DNS查询 | UDP | 一次请求一次响应,简单快速 |
| 游戏状态同步 | UDP | 需要低延迟,丢包了直接覆盖 |
| 远程登录(SSH) | TCP | 每个按键都不能丢 |
我的建议:如果你不确定用哪个,先用TCP。TCP是「万金油」,大多数场景都能胜任。只有当你明确感受到TCP的延迟或吞吐量成为瓶颈时,再考虑UDP。
你想想看,为什么HTTP/3要用QUIC(基于UDP)?就是因为TCP的队头阻塞问题在弱网环境下太严重了。但QUIC在UDP之上自己实现了可靠传输和拥塞控制——说白了,是在UDP的「地基」上盖TCP的「房子」。
UDP通信知识体系
下面这张图总结了UDP通信的核心脉络:
这张图把UDP的知识点串起来了。从中心出发,左边是编程模型,中间是广播,右边是组播。底部是最终的选择问题——说白了,就是你要在「快」和「稳」之间做个取舍。
我记得有一次做视频会议系统,一开始用的TCP,结果网络一卡,整个画面都卡住了。后来换成UDP,虽然偶尔有马赛克,但至少能看。用户反馈说:「画面有点糊,但至少不卡了。」——这就是UDP的价值所在。
公众号:蓝海资料掘金营,微信deep3321