☰
【Linux笔记】UDP协议
2026/10/7 10:04:03 网站建设 项目流程

一、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 不保证报文间的顺序,但对于单个报文内部的数据,字节顺序是严格保持的。

你不会收到一个内容被打乱的报文。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询