UDP 通信
UDP 通信
时间:2026/04/09
关键词:报文、无连接、不可靠传输、
sendto/recvfrom、MTU、丢包重传、滑动窗口
核心目标:先把 UDP 的“简单”理解透,再搞清楚为什么很多实时协议愿意在 UDP 之上自己补可靠性。
1. UDP 是什么
UDP(User Datagram Protocol)是面向报文的传输层协议。
它的特点很直接:
- 无连接:通信前不需要三次握手
- 报文边界保留:发送一次
sendto,接收端看到的是一个完整报文,天然没有 TCP 粘包问题 - 尽力而为:协议本身不保证送达、不保证顺序、不保证不重复
- 开销小、时延低:头部只有 8 字节,协议栈处理也更轻
所以 UDP 很适合:
- 实时音视频
- 在线游戏
- DNS
- 广播 / 组播
- 能容忍少量丢包,但很在意时延的业务
2. UDP 和 TCP 的核心差异
| 维度 | UDP | TCP |
|---|---|---|
| 连接语义 | 无连接 | 面向连接 |
| 数据形式 | 报文 | 字节流 |
| 可靠性 | 不保证 | 保证送达、按序、去重 |
| 顺序 | 不保证 | 保证 |
| 重传 | 应用层自己做 | 内核协议栈负责 |
| 流量控制 | 没有 | 有 |
| 拥塞控制 | 没有 | 有 |
| 时延 | 更低 | 较稳定但更重 |
最重要的一点:
UDP 只是“帮你发包”,TCP 则是“帮你把传输这件事做完整”。
3. Linux 下 UDP 的常用接口
3.1 创建 socket
1 | int fd = socket(AF_INET, SOCK_DGRAM, 0); |
SOCK_DGRAM 表示 UDP 套接字。
3.2 服务端绑定地址
1 | bind(fd, (struct sockaddr*)&addr, sizeof(addr)); |
和 TCP 一样,服务端通常需要 bind 到固定 IP/端口。
3.3 接收与发送
1 | ssize_t n = recvfrom(fd, buf, sizeof(buf), 0, |
这是一对最常见的 UDP I/O 接口:
recvfrom:收到数据时,顺便告诉你包来自谁sendto:每次发送时明确指定目标地址
3.4 connect() 对 UDP 也有用
1 | connect(fd, (struct sockaddr*)&peer, sizeof(peer)); |
UDP 上的 connect() 不是建立连接,而是:
- 给这个 socket 绑定一个默认对端
- 之后可以直接用
send/recv - 内核只接收该对端发来的报文
- 某些错误能更直接反馈给调用方
所以很多客户端 UDP 程序也会先 connect(),这样代码更简洁。
4. UDP 通信的基本流程
4.1 服务端
socket(AF_INET, SOCK_DGRAM, 0)bind()- 循环
recvfrom() - 根据来源地址决定回包对象
sendto()返回结果
4.2 客户端
socket(AF_INET, SOCK_DGRAM, 0)- 可选:
connect() sendto()或send()recvfrom()或recv()
和 TCP 相比,UDP 没有:
listen()accept()
因为它没有连接队列这一层。
5. UDP 的工程注意点
5.1 没有粘包,不等于没有协议设计
UDP 保留报文边界,但应用层仍然要定义:
- 包头
- 消息类型
- 序号
- 校验
- 重传策略
否则出了问题很难排查。
5.2 丢包、乱序、重复都是正常现象
UDP 编程默认就要接受这些情况:
- 某个包永远收不到
- 后发的包先到
- 同一个包被收到两次
这不是异常,而是 UDP 的正常工作方式。
5.3 尽量避免 IP 分片
如果 UDP 报文过大,IP 层可能分片。分片带来的问题:
- 任一分片丢失,整个报文作废
- 网络设备对分片不友好
- 性能和稳定性都变差
工程上通常建议:
- 单个应用层包尽量控制在 MTU 以下
- 以太网常见安全值可以先按
1200字节左右设计
这也是 QUIC 常见的保守做法之一。
5.4 高性能场景通常要配合事件循环
在 Linux 上,大量 UDP socket 或高频收发通常会配合:
epoll- 非阻塞 socket
- 定时器管理重传
- 批量收发(如
recvmmsg/sendmmsg)
单线程阻塞式 recvfrom() 适合入门,不适合高并发服务。
6. 一个最小 UDP 回显服务端
1 |
|
这个例子足够说明 UDP 服务端的基本模型:
- 一个 socket
- 不断收报文
- 每个报文都自带来源地址
- 回包时显式指定目标
7. 用 UDP 实现 TCP,应该怎么想
这里更准确的说法不是“把 TCP 原样重写一遍”,而是:
在 UDP 之上补齐 TCP 的核心能力,做出一个“可靠、有序、可控”的传输层。
这是很多实时网络框架的常见思路,例如:
- KCP:在 UDP 上实现可靠传输,强调低延迟
- QUIC:在 UDP 上实现更现代的连接、多路复用、拥塞控制与加密
7.1 先分清目标
如果你只是想让消息“更可靠”,那通常只需要做:
- 序号
- ACK
- 超时重传
- 简单窗口
如果你想完整模拟 TCP,则还要补:
- 连接管理
- 按序交付
- 流量控制
- 拥塞控制
- RTT 估计与重传定时器
- 关闭连接
真正困难的部分其实不是“发包”,而是:
- 如何在复杂网络环境里稳定地控制发送节奏
7.2 最小协议头可以这样设计
1 | struct PacketHeader { |
如果只是做“可靠消息”而不是“字节流”,这个头已经够搭框架了。
7.3 把 TCP 的能力拆成 6 层功能
1. 连接管理
在 UDP 上自己定义连接状态:
CLOSEDSYN_SENTSYN_RECVESTABLISHEDFIN_WAIT
可以直接借用 TCP 的思路:
- 客户端发
SYN(seq=x) - 服务端回
SYN|ACK(seq=y, ack=x+1) - 客户端再发
ACK(ack=y+1)
这样做的价值是:
- 双方都知道初始序号
- 可以建立会话状态
- 便于后续超时、重传和断线清理
2. 可靠性
发送端维护一个未确认发送队列:
- 每发一个包,都放进
unacked_map - 记录发送时间、重传次数
- 定时扫描超时包并重发
接收端收到包后返回 ACK。
最简单的 ACK 策略可以先做:
- 累计确认:
ack = 下一个期待收到的序号
再进一步可做:
- 选择确认(SACK):显式告诉对端哪些乱序包已经收到
3. 有序交付
如果包乱序到达:
- 先放入接收缓冲区
- 只有当
seq == expected_seq时才向上层提交 - 提交后继续检查后续缓存是否已连续
这就是 TCP “按序交付”的核心。
4. 流量控制
需要告诉对方:
- 我这边接收缓冲还有多大
也就是在包头里放 wnd,类似 TCP 的接收窗口。
否则发送方可能发得太快,把接收方内存顶爆。
5. 拥塞控制
这是最难的一层。
最偷懒的版本可以先不做,只设固定发送速率,但这不是真正的 TCP 级能力。
更接近 TCP 的做法是引入:
- 慢启动
- 拥塞避免
- 丢包后乘法减小
如果没有拥塞控制,你的协议在局域网看起来正常,到了公网通常就会失控。
6. 关闭连接
同样可以模仿 TCP:
- 一端发
FIN - 对端回
ACK - 双方完成收尾和资源回收
否则 session 容易泄漏。
8. 一个“UDP 版 TCP”最小实现框架
如果从工程结构上设计,可以拆成下面几层:
- UdpSocket 层
负责socket/bind/sendto/recvfrom和epoll - Session 层
用peer_addr + conn_id管理会话状态 - Reliability 层
管理seq/ack、发送窗口、重传队列、乱序缓冲 - Timer 层
负责 RTO、心跳、超时断线 - Congestion / Flow Control 层
控制能发多少、发多快 - Application 层
真正的业务协议
一个发送端主循环大概是:
1 | 应用层提交消息 |
一个接收端主循环大概是:
1 | 收到 UDP 报文 |
9. 真正落地时的几个关键取舍
9.1 先做“可靠消息”,再做“可靠字节流”
如果目标是游戏状态同步、命令包、RPC:
- 做可靠消息协议通常更简单
如果非要完全模拟 TCP 的“字节流语义”,复杂度会显著上升,因为你还要处理:
- 流式重组
- 半关闭
- 更复杂的缓冲管理
9.2 不要一开始就试图复刻内核 TCP
更现实的路线是:
- 先做连接 + seq + ack + 重传
- 再做乱序缓存 + 滑动窗口
- 再补 RTT / RTO
- 最后再考虑拥塞控制
否则很容易一上来就把实现写散。
9.3 现代工程更常见的是“在 UDP 上做定制协议”
很多时候我们并不是要“重新发明 TCP”,而是要:
- 保留 UDP 的低时延和灵活性
- 只补自己需要的那部分可靠性
例如:
- 关键帧可靠重传
- 状态包不重传,只发送最新值
- 控制消息必须有 ACK
这比完整复刻 TCP 更符合实时系统的实际需求。
10. 小结
可以把 UDP 记成一句话:
UDP 提供的是“轻量发报文”的能力,可靠、有序、流控、拥塞控制都要你自己决定要不要补。
如果要用 UDP 实现类似 TCP 的能力,最关键不是 API,而是这几个机制:
- 序号
seq - 确认
ack - 超时重传
- 滑动窗口
- 按序交付
- 流控 / 拥塞控制
把这些拆开理解之后,“UDP 实现 TCP”就不再神秘,本质上就是:
- 在用户态自己维护传输状态机
参考
- Linux网络基础——传输层协议 UDP 与 TCP
- RFC 768: UDP
- RFC 793: TCP