🎬 第八章 FFmpeg命令行实现流媒体功能及直播应用
📄 一句话概括

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 秒实验

  1. 换成 sendto 前先 bind:把发送端也绑上端口,看接收端会不会收到回包。
  2. 把 100 个包改成 1 个:确认「先发后收」丢包与包数无关,纯粹是时序。
  3. 把超时从 2 秒改成 0.5 秒:看 TCP 那行的异常类型会不会变。

常见误区

  1. 以为 sendto 没抛异常就是送到了。它只保证交给了内核。
  2. 以为 UDP 快是因为协议先进。快是因为它什么都没做。
  3. 在公网上发 60000 字节的大包。回环能过,真网卡会分片、丢一片就全废。
自测三问(点开对答案) 1. UDP 头有几个字段、共几个字节? —— 四个字段,8 个字节:源端口、目的端口、长度、校验和。 2. 为什么先发完再开接收端会一个都收不到? —— 没有程序监听时内核直接丢包,UDP 不排队也不缓存。 3. 1472 这个数字是怎么来的? —— 1500 减 20(IP 头)减 8(UDP 头),不分片时的单包上限。

相关笔记