一、UDP协议
UDP:是 TCP/IP 协议栈中传输层的一个无连接、不可靠、面向报文的轻量级协议。
它运行在 IP 层之上(即应用层),为应用程序提供最基本的多路复用与多路分用能力,但不提供可靠传输保障。
1.1 UDP报文结构

Linux中UDP报头结构体:
struct udp_header
{
uint16_t src_port;
uint16_t dst_port;
uint16_t length;
uint16_t checksum;
};
1.2 UDP 工作原理
1.2.1 标准问题
A. UDP 报头和有效载荷分离
UDP:基于固定首部 + 显式长度字段
Udp的报头:固定8字节长度
Udp的长度字段:明确指出了整个数据报的字节数。
Udp的有效载荷:Udp的长度 - 8
固定偏移量
-
接收端收到 UDP 数据报后,直接读取前 8 个字节作为首部。
-
字节 0-1:源端口
-
字节 2-3:目的端口
-
字节 4-5:UDP 总长度(首部 + 载荷)
-
字节 6-7:校验和
-
从第 9 个字节开始,即为有效载荷。
长度字段的校验作用
-
UDP Length字段明确指出了整个数据报的字节数。 -
计算公式:
Payload Size=UDP Length−8=UDP Length−8 -
如果 IP 层交付上来的数据长度与 UDP Length 字段不一致,接收端会直接丢弃该数据报或报错。这既是分离依据,也是完整性校验手段。
边界确定性:UDP 是面向报文的协议,IP 层交付给 UDP 的是一个完整的数据报单元。
UDP 不需要在载荷内部寻找边界,一次 IP 交付 = 一个完整的 UDP 报头 + 一个完整的载荷。
B. UDP向上交付
UDP 的交付(无连接):
⑥ 应用层:recvfrom() / recvmsg() 系统调用
→ 从 Socket 接收队列中取出 **一个完整的数据报**
→ 剥离 UDP 头,将纯数据拷贝到用户态缓冲区
▲
│
|
⑤ 传输层:udp_rcv() 被调用
→ 做 UDP 校验和(若开启)
→ 根据 {目标IP, 目标端口} 在 UDP 哈希表里查找对应的 Socket
→ 找到后调用 udp_queue_rcv_skb(),将 **完整的数据报**(含 UDP 头)挂入 Socket 的接收队列
→ 若队列满,直接丢弃(不通知发送方)
▲
│
|
④ 网络层:ip_rcv() → 校验 IP 头,剥离 IP 头,根据协议号(17 = UDP)分发给 udp_rcv()
▲
│
|
③ 链路层:netif_receive_skb() → 剥离 Ethernet 头,根据 ethertype(0x0800)送给 IPv4
▲
│
|
② 驱动层:中断处理 → 从 Ring Buffer 取出 skb,记录各层偏移,交给网络层
▲
│
|
① 网卡 DMA:硬件收到以太网帧,通过 DMA 写入 Ring Buffer,产生硬中断
C. UDP向下封装
⑥ 应用层 `sendto()`:指定目标 IP + 端口,将数据拷贝入内核h
│
▼
⑤ `udp_sendmsg()` 被调用:
│ → 检查数据长度是否超过 MTU(若超过,IP层会负责分片)
│ → 构造 UDP 头部,填充校验和
│
▼
④ 调用 `ip_append_data()` 或 `ip_push_pending_frames()`
│ → 将数据打包成 IP 分片(若需要),或直接交给 IP 层
│
▼
③ 网络层 `ip_output()`:添加 IP 头,查路由表决定出口网卡
│
▼
② 链路层 `dev_queue_xmit()`:邻居子系统填充 MAC 头,压入发送队列
│
▼
① 驱动/网卡 DMA:网卡取走数据发送
1.3 UDP的特点
无连接
/ \
/ \
不可靠 面向数据报
| |
+----------+---------+
|
三者共同决定了 UDP 的定位:
轻量、快速、简单、灵活
- 因为无连接,所以不需要维护状态 -> 不可靠(没有状态来做可靠性)
- 因为不可靠,所以不需要复杂的流控和重组 -> 面向数据报(无需字节流重组)
- 因为面向数据报,所以每个报文独立自包含 -> 无连接(不需要跨报文的上下文)
1.3.1 无连接
"无连接"的理解:
-
UDP 在通信双方之间不存在任何状态关联。
-
发送端在发送数据之前,不需要与接收端进行任何形式的协商、握手或状态同步。
-
每一个 UDP 报文都是完全独立的实体,内核不为这对通信维护任何会话上下文。
A. "无连接"的具体表现
a. 不关心接收端成功接收
发送端调用 sendto() 时:
- 不检查接收端是否启动
- 不检查接收端端口是否监听
- 不检查接收端缓冲区是否已满
- 只要本地 IP 层能路由出去,就认为"发送成功"
注意:sendto() 返回成功 ≠ 对方收到了数据
它仅仅表示数据已成功交给内核的网络协议栈
b. 没有连接生命周期
TCP socket 连接有明确的状态: ESTABLISHED、FIN_WAIT、TIME_WAIT 等状态迁移。
UDP socket 只有两种状态:已绑定(bound)和未绑定(unbound)。
不存在"正在建立""正在关闭"等中间态。
c. 一个 socket 对接多个对端
// 同一个 UDP socket 可以向完全不同的目标发送数据
sendto(sockfd, data1, len1, 0, (struct sockaddr *)&addr_A, sizeof(addr_A));
sendto(sockfd, data2, len2, 0, (struct sockaddr *)&addr_B, sizeof(addr_B));
sendto(sockfd, data3, len3, 0, (struct sockaddr *)&addr_C, sizeof(addr_C));
// 也可以接收来自任意对端的数据
recvfrom(sockfd, buf, size, 0, &peer_addr, &addr_len);
// peer_addr 每次可能都不同
这在 TCP 中是不可能的------TCP 的一个 socket 严格绑定一对(源IP:源端口, 目的IP:目的端口)。
1.3.2 不可靠
"不可靠"的理解:
-
不是指 UDP "质量差",而是指 UDP 协议本身不提供任何交付保证。
-
它把"可靠性"这个责任完全推给了上层应用。
A."不可靠"的具体表现
a. 不保证送达
发送端 网络端 接收端
| | |
|--- UDP Datagram -----------> | |
| |--- (Packet lost in network) |
| | |
| sendto() returns success | |
| | |
| | |
| Sender is completely | |
| unaware of the loss | |
报文可能在传输途中被路由器丢弃(队列满、TTL 耗尽、校验失败)
可能被防火墙/ACL 过滤
可能因接收端缓冲区满而被内核静默丢弃
发送端不会收到任何否定反馈(除非使用了 connected UDP 且触发了 ICMP 错误)
b. 不保证顺序
发送端按序发送: [Seq=1] [Seq=2] [Seq=3] [Seq=4]
网络路径A: [Seq=1] ---------> 先到达
网络路径B: [Seq=2] ----> 后到达(走了更短的路径)
网络路径C: [Seq=3] -----------> 最后到达
网络路径D: [Seq=4] ---> 最先到达(ECMP 负载均衡到快路径)
接收端实际收到: [Seq=4] [Seq=1] [Seq=2] [Seq=3]
IP 层的 ECMP 负载均衡可能导致同一流的报文走不同路径
路由器队列调度也可能改变报文顺序
c. 不重传
TCP 有超时重传(RTO)和快速重传(Fast Retransmit)
UDP 没有任何重传机制,丢了一个包就是丢了,协议层不会尝试补救
即 UDP 不会阻塞socket分配的发送缓冲区,而是临时存储在发送缓冲区中,然后直接向下传输到网络层直接发送。
d. 不确认
TCP 的 ACK 机制让发送端确切知道哪些数据已被接收
UDP 没有 ACK 字段,发送端处于完全的"盲发"状态,不能确定是否发送成功
e. 无流量控制
TCP 通过滑动窗口告知发送端"我还能接收多少数据"
UDP 没有窗口概念,发送端可以以任意速率发送,即使接收端处理能力远低于发送速率
结果:接收端内核缓冲区迅速填满 -> 新报文被丢弃 -> 应用层看到大量丢包
f. 无拥塞控制
TCP 通过慢启动、拥塞避免、快恢复等算法自适应调整发送速率
UDP 完全不感知网络拥塞,在网络已经拥塞时继续高速发送,会加剧拥塞,甚至挤占 TCP 流量
1.3.3 面向数据报
"面向数据报"的理解:
-
UDP 严格保留应用层数据的边界。
-
应用层交给 UDP 一个完整的数据块,UDP 就将其作为一个不可分割的整体进行封装、传输和交付。
TCP 面向字节流的表现:
=== TCP 面向字节流 ===
发送端两次 send():
send("Hello") -> 5 字节进入发送缓冲区
send("World") -> 5 字节追加到发送缓冲区
TCP 发送缓冲区内容: [H][e][l][l][o][W][o][r][l][d]
^ ^ ^ ^ ^ ^ ^ ^ ^ ^
纯粹的字节序列,无任何边界标记
接收端可能的 recv() 结果(全部合法):
情况A: recv() => "HelloWorld" (一次读完)
情况B: recv() => "Hel" + "loWorld" (拆成两次)
情况C: recv() => "HelloWor" + "ld" (在任意位置拆分)
情况D: recv() => "H" + "e" + "lloWorld" (逐字节读)
TCP 不关心你的逻辑消息边界,它只看到一条连续的字节河。
UDP 面向数据报的表现:
=== UDP 面向数据报 ===
发送端两次 sendto():
sendto("Hello") -> 封装为 UDP 报文 #1
sendto("World") -> 封装为 UDP 报文 #2
网络上传输的是两个独立的报文:
[UDP Header | H e l l o] [UDP Header | W o r l d]
^^^ ^^^^^^ ^ ^ ^ ^ ^ ^^^ ^^^^^^ ^ ^ ^ ^ ^
报文#1 (Length=13) 报文#2 (Length=13)
接收端 recvfrom() 的结果(只有一种可能):
第1次 recvfrom() => "Hello" (恰好是报文#1的完整载荷)
第2次 recvfrom() => "World" (恰好是报文#2的完整载荷)
不可能出现 "Hel" + "loWorld" 的情况!
也不可能出现 "HelloWorld" 合并的情况!
A. "面向数据报"的三个核心性质
a. 性质一:不拆分
应用层交给 UDP 多大的数据,UDP 就封装成多大的报文。
即使数据长达 60000 字节,UDP 也不会将其拆分为多个小报文(但 IP 层可能会分片,这是另一回事)。
// 发送 5000 字节
sendto(sockfd, big_buf, 5000, 0, &addr, sizeof(addr));
// 接收端一定是一次 recvfrom 拿到完整的 5000 字节
n = recvfrom(sockfd, buf, sizeof(buf), 0, &peer, &len);
// n == 5000(假设没有丢包)
b. 性质二:不出现粘包
发送端连续发送多个 UDP 报文,接收端不会将它们合并为一个。
// 发送端连续发 3 个小报文
sendto(sockfd, "AAA", 3, 0, &addr, sizeof(addr));
sendto(sockfd, "BBB", 3, 0, &addr, sizeof(addr));
sendto(sockfd, "CCC", 3, 0, &addr, sizeof(addr));
// 接收端一定是 3 次 recvfrom,每次恰好 3 字节
// 绝不会一次 recvfrom 拿到 "AAABBBCCC"
c. 性质三:单条报文内顺序确定
虽然 UDP 不保证报文间的顺序,但对于单个报文内部的数据,字节顺序是严格保持的。
你不会收到一个内容被打乱的报文。