【Linux笔记】UDP协议

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

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

相关推荐
Felven11 小时前
stress-ng 与 fio 性能测试工具使用指南
linux·测试工具·fio·stress-ng
huainingning12 小时前
RJ SW Console口忘记密码处理方法
linux·运维·服务器
布裘12 小时前
【银河麒麟】桌面系统循环登录,无法进入桌面?
linux·运维·服务器
慧都小项12 小时前
程序到了Linux才出错?用CLion把调试接到目标环境
linux·运维·服务器
Ruiery14 小时前
Linux 6.6内核内存管理深度解析(一):物理内存初始化 — memblock 怎么把内存交给 buddy
linux·运维·服务器
天蓝蓝的本我15 小时前
conda日常用到的命令积累1-登录linux,安装pycharm
linux·pycharm·conda
Android系统攻城狮15 小时前
Linux Gstreamer深度解析之gst_audio_encoder_get_frame_max调用流程与实战(五十九)
linux·运维·服务器·gstreamer音视频·音视频进阶·gstreamer音视频进阶
忆挽篱笙歌16 小时前
makefil
linux·运维·服务器
菜_小_白17 小时前
codex
linux·vscode·ai