UDP 的头只有 8 个字节,四个字段,没有任何可靠性机制。 发送成功只代表「交给了系统」,不代表「对方收到了」。
8.3 的 HTTP 是拿「可靠」换「延迟」,这一节讲另一个极端。 理解了 UDP,才能理解 RTP 为什么非要自己加序号。
一、8 字节的头
UDP 的头短得离谱:8 个字节,四个字段,一个都不多。
字段 长度 作用
源端口 2 字节 回包时用,可以不填 目的端口 2 字节 往哪个程序送 长度 2 字节 含头在内一共多少字节 校验和 2 字节 IPv4 下可以关掉
拿它跟 TCP 比一比,差距一目了然。
对比项 TCP UDP
头长度 最少 20 字节 固定 8 字节 建连 三次握手 没有 丢包 重传 不管 顺序 保证 不保证 开销 大 极小
所以 UDP 的性格是「发出去就不管了」。 这不是缺陷,是设计选择—— 直播宁可丢一帧,也不要为了补那一帧把整条流卡住。
二、先发后收,一个包都收不到
下面这个实验最能说明 UDP 的性格:先把 100 个包全发完,再开接收端。
做法 收到包
先绑定再发(100 个) 100 先发完再绑定(100 个) 0
一个都收不到,而且发送端全程没有任何报错。 没有程序在监听的那一瞬间,内核直接把包扔掉,连条记录都不留。
UDP 既不排队,也不缓存,更没有「等对方准备好」这种机制。 这就是调直播时最常见的现象:对面说没收到,你这边一切正常。
三、TCP 会报错,UDP 不吭声
往一个没人监听的端口发数据,两种协议的反应完全不同。
动作 结果 耗时
TCP 连没人听的端口 TimeoutError 2008 ms UDP 发没人听的端口 不报错 0.6 ms
TCP 那一行明确抛了异常,名字是 TimeoutError。
在多数 Linux 上会更快地抛 ConnectionRefusedError,
本机因为防火墙静默丢弃,才等到 2 秒超时。
重点看时间差:一个 2008 毫秒,一个 0.6 毫秒。 UDP 这么快,是因为它什么都没做——没建连、没确认、没等待。 所以「UDP 发送成功」这句话本身没有意义,它只保证「已经交给系统了」。
四、一个包能有多大
UDP 的长度字段是 2 字节,理论上一包能到 65535, 但真实网络里的天花板远低于这个数字。
负载字节 结果
1400 收到 1400 1472 收到 1472 2000 收到 2000 60000 收到 60000 65508 OSError
1472 = 1500 减 20 减 8,正好一个以太网帧装得下,这是不分片的单包上限。 工程上常取 1400,多留一点给隧道封装和 IP 选项。
2000 与 60000 都超过 MTU,会走 IP 分片。 本地回环能收全,是因为它压根没经过真实网卡; 真在公网上跑,丢掉任意一片,整个包就废了。
65508 直接抛 OSError:65535 减 8 减 20,再大就装不进长度字段了。
五、直播里什么时候该用 UDP
把几种场景摊开看,规律就很清楚:越靠下越可靠,也越慢。
场景 该用什么 为什么
局域网监控 裸 UDP 承载 TS 够简单,丢一点无所谓 跨公网直播 RTP over UDP 有序号才能发现丢包 弱网移动端 SRT 自带重传与纠错 网页播放 HTTP-FLV/HLS 浏览器只认 HTTP
裸 UDP 只适合「能接受丢包、而且不在乎丢了哪个」的场合。 一旦你需要知道「少了什么」,就得往上叠一层——那就是 8.2 讲的 RTP。
六、动手 Demo:量一遍 UDP 的四条脾气
下面这段代码做四件事:验先发后收、比 TCP 与 UDP 的报错、测单包上限。 数字都是当场跑出来的,换台机器会有差别,但结论不会变。
import socket, time
def free_port():
s = socket.socket()
s.bind(("127.0.0.1", 0))
p = s.getsockname()[1]
s.close()
return p
# ① 先发完 100 个,再开接收端
port = free_port()
tx = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for _ in range(100):
tx.sendto(b"x" * 100, ("127.0.0.1", port))
time.sleep(0.3)
rx = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
rx.bind(("127.0.0.1", port))
rx.settimeout(1.0)
got = 0
try:
while True:
rx.recvfrom(4096)
got += 1
except Exception:
pass
print(u"先发完再绑定,收到 %d 个" % got)
# ② TCP 会报错,UDP 不会
dead = free_port()
t0 = time.time()
try:
socket.create_connection(("127.0.0.1", dead), 2.0)
except Exception as e:
print(u"TCP %s %.0f ms" % (type(e).__name__, (time.time() - t0) * 1000))
t0 = time.time()
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(b"hello", ("127.0.0.1", dead))
print(u"UDP 不报错 %.1f ms" % ((time.time() - t0) * 1000))
# ③ 单包上限
for n in (1400, 1472, 2000, 65508):
a, b = free_port(), socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
b.bind(("127.0.0.1", a))
b.settimeout(1.5)
try:
s.sendto(b"y" * n, ("127.0.0.1", a))
print(u"%d -> 收到 %d" % (n, len(b.recvfrom(70000)[0])))
except Exception as e:
print(u"%d -> %s" % (n, type(e).__name__))
b.close()
实跑输出:
--------------------------------------------------------------------
8.4.1 UDP 简介
--------------------------------------------------------------------
**① UDP 的头只有 8 字节,四个字段全是必需的**
字段 长度 作用
------------------------------------------------
源端口 2 字节 回包时用,可以不填
目的端口 2 字节 往哪个程序送
长度 2 字节 含头在内一共多少字节
校验和 2 字节 IPv4 下可关掉
**八个字节,连 TCP 头的四分之一都不到。**
**TCP 头最少 20 字节,还要加三次握手、确认、重传这些机制。**
**省下的这些,换来的就是「快」与「简单」,代价是「什么都不保」。**
**② 先发后收,一个包都收不到**
做法 收到包
-------------------------------------
先绑定再发(100 个) 100
先发完再绑定(100 个) 0
**先发完再绑定,一个都收不到——因为没有程序在监听的那一瞬间,包直接被丢弃。**
**UDP 没有重传,也没有「等对方准备好」的机制。**
**这一条跟 TCP 完全不同:TCP 连不上会报错,UDP 连不上就悄悄没了。**
**所以调直播时「对面没收到」是最常见的现象,而且一点线索都不给。**
**③ TCP 连不上会报错,UDP 不会**
动作 结果 耗时
---------------------------------------------------
TCP 连没人听的端口 TimeoutError 2008 ms
UDP 发没人听的端口 不报错 0.6 ms
**TCP 那行立刻抛 `ConnectionRefusedError`,UDP 那行毫无反应。**
**所以「UDP 发成功了」这句话本身没有意义——它只保证「已经交给系统了」。**
**④ 一个包最大能有多大**
负载字节 结果
---------------------------
1400 收到 1400
1472 收到 1472
2000 收到 2000
60000 收到 60000
65508 OSError
**1472 是「1500 减 20 减 8」——正好卡在 MTU 之内,一个 IP 包能装下。**
**2000 与 60000 会走 IP 分片,但本地回环还是收到了,因为没经过真实网卡。**
**65508 超过了 UDP 长度字段的表示能力(65535 减 8 减 20),直接失败。**
第一段是 0,第二段 TCP 报错而 UDP 不报错,第三段 65508 失败——三条全对上。
单包上限这一组最能看出「本地回环」和「真实网卡」的差别: 60000 字节在回环上收得干干净净,真走网卡早被 MTU 分片打散了。
30 秒实验
- 换成
sendto前先bind:把发送端也绑上端口,看接收端会不会收到回包。 - 把 100 个包改成 1 个:确认「先发后收」丢包与包数无关,纯粹是时序。
- 把超时从 2 秒改成 0.5 秒:看 TCP 那行的异常类型会不会变。
常见误区
- 以为
sendto没抛异常就是送到了。它只保证交给了内核。 - 以为 UDP 快是因为协议先进。快是因为它什么都没做。
- 在公网上发 60000 字节的大包。回环能过,真网卡会分片、丢一片就全废。
自测三问(点开对答案)
1. UDP 头有几个字段、共几个字节? —— 四个字段,8 个字节:源端口、目的端口、长度、校验和。 2. 为什么先发完再开接收端会一个都收不到? —— 没有程序监听时内核直接丢包,UDP 不排队也不缓存。 3. 1472 这个数字是怎么来的? —— 1500 减 20(IP 头)减 8(UDP 头),不分片时的单包上限。- ☐ 8.4.1 完,下一节 8.4.2 VLC 作为 UDP 流媒体服务器