1. UDP协议基础:协议栈位置、报文结构、与TCP核心差异

做网络分析这么多年,我始终觉得一个道理很朴素:要分析丢包,先得懂协议本身。UDP 看起来简单,但恰恰是这种「简单」让很多人栽了跟头。今天咱们就把 UDP 的底裤扒干净。

1.1 UDP 在协议栈里的位置

先看一张图,把 UDP 在 TCP/IP 协议栈中的位置搞清楚。

UDP 在 TCP/IP 协议栈中的位置 应用层 HTTP / DNS / DHCP / SNMP / NTP / 视频流 传输层 UDP(无连接) | TCP(面向连接) 网络层 IP(IPv4 / IPv6) 数据链路层 以太网 / WiFi / PPP UDP 位于传输层,直接承载于 IP 之上,不建立连接,不维护状态

UDP 在传输层,直接坐在 IP 头上。它不跟你搞什么三次握手、四次挥手——发就发了,收就收了,丢了也不管。我刚开始做网络开发时觉得这太「不负责任」了,后来才明白,这正是它的魅力所在。

1.2 UDP 报文结构——8 个字节定乾坤

UDP 头部只有 8 个字节。你没看错,就 8 个。对比 TCP 动辄 20 字节的头部,UDP 简直是个「极简主义者」。

字段 长度 说明
源端口 2 字节 发送方端口号。可选,不需要时可以填 0
目的端口 2 字节 接收方端口号。这个必须填,否则数据不知道给谁
长度 2 字节 UDP 头部 + 数据的总长度。最小 8,最大 65535
校验和 2 字节 可选字段。IPv4 下可以全 0,IPv6 下必须计算

关键点:UDP 头部只有 4 个字段、8 个字节。没有序号、没有确认号、没有窗口大小、没有标志位。这就是为什么它「快但不可靠」。

我遇到过不少开发者,拿着 Wireshark 抓包,看到 UDP 报文就蒙了——「怎么就这么点信息?」对,就这么点。你想想看,TCP 头部有 10 多个字段,UDP 只有 4 个。少了那么多东西,自然就少了那些保证可靠性的机制。

1.3 UDP 与 TCP 核心差异——一张表说清楚

很多教材喜欢列一堆对比项,我直接给你一张实战用的对照表。这张表我用了快十年,面试新人时也经常拿它来考。

对比维度 UDP TCP
连接方式 无连接。发数据前不需要握手 面向连接。三次握手建立连接
可靠性 不可靠。不保证送达,不重传 可靠。确认重传、序号保证
有序性 不保证。后发的可能先到 保证。按序号重组数据
流量控制 无。发多少算多少 有。滑动窗口控制发送速率
拥塞控制 无。网络堵了照样发 有。慢启动、拥塞避免
头部开销 8 字节 20~60 字节
传输模式 报文边界保留 字节流,无边界
典型应用 DNS、DHCP、视频流、VoIP、游戏 HTTP、FTP、SMTP、SSH

实战经验:我做过一个视频监控项目,最初用 TCP 传输视频流。结果网络一波动,TCP 就开始重传、拥塞控制,画面卡得没法看。后来换成 UDP + 应用层丢包补偿,流畅度直接翻倍。说白了,实时性要求高的场景,UDP 是唯一选择。

1.4 为什么 UDP 会丢包?——先看根因

既然要分析丢包率,咱们得先搞清楚 UDP 丢包的根因。我把它归纳为三类:

  1. 网络层丢包——路由器缓冲区满了,直接把包扔了。这是最常见的。
  2. 接收端缓冲区溢出——应用程序读得慢,内核缓冲区满了,新来的包只能丢弃。
  3. 校验和错误——数据在传输过程中被损坏,接收端校验失败,直接丢弃。

注意:UDP 没有重传机制。一旦丢包,应用层永远不知道这个包曾经存在过。这就是为什么很多 UDP 应用需要自己实现 ACK 和超时重传——说白了,就是在应用层「山寨」TCP 的可靠性。

我记得有一次排查一个游戏服务器的丢包问题,抓包发现接收端缓冲区一直在涨,但应用程序就是读得慢。后来发现是业务线程里加了个耗时的数据库查询,把接收循环堵住了。嗯,这种问题在 UDP 场景下特别隐蔽,因为 UDP 不会告诉你「我丢了包」。

1.5 一个简单的 UDP 收发示例

光说不练假把式。我写个最简单的 Python 示例,帮你理解 UDP 的「无连接」特性。

# udp_sender.py — 发送端
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
message = b"Hello, UDP!"
sock.sendto(message, ("192.168.1.100", 8888))
sock.close()
# udp_receiver.py — 接收端
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("0.0.0.0", 8888))

data, addr = sock.recvfrom(1024)
print(f"收到来自 {addr} 的消息: {data}")
sock.close()

你看,发送端不需要 connect,直接 sendto 就发出去了。接收端 bind 一个端口,等着 recvfrom 就行。没有握手、没有确认、没有状态维护。这就是 UDP 的「无连接」本质。

核心结论:UDP 的丢包不是「Bug」,而是「Feature」。它把可靠性交给了应用层,自己只负责「尽力而为」地传输。分析丢包率,本质上就是在分析这个「尽力而为」到底做到了什么程度。