RTCP 只有五种包,靠第二个字节的 PT 区分:200 到 204。 length 字段是「32 位字的个数减一」,不是字节数——这个坑最经典。
RTP 负责搬数据,RTCP 负责「对账」。这一节先把五种包认全。 RTCP 的包都很小(8 到 几十字节),但信息密度极高——丢包率、抖动、对时、身份,全在里面。 下一节 01.07.02 拿 SR 单独拆一遍。
一、五种包,各管一件事
PT 是第二个字节的低七位,200 到 204 就是这五种。
| PT | 名字 | 做什么 | 谁发 |
|---|---|---|---|
| 200 | SR | 发送者报告:我发了多少、现在几点 | 发数据的一端 |
| 201 | RR | 接收者报告:我收了多少、丢了多少 | 只收的一端 |
| 202 | SDES | 源描述:这个 SSRC 是谁 | 双方 |
| 203 | BYE | 我要走了 | 退出的一端 |
| 204 | APP | 厂商自定义 | 看实现 |
SR 和 RR 是互补的:发数据的发 SR(我发了多少),只收的发 RR(我收得怎么样)。 两个都收也发?那就发 SR——SR 里也能带接收报告块,信息更全。
二、length 字段的坑
这个字段最容易理解错,必须说清楚:
| 包总长 | length 字段写 | 误读成字节会怎样 |
|---|---|---|
| 8 字节(RR) | 1 | 以为只有 1 字节,读不全 |
| 28 字节(SR) | 6 | 以为 6 字节,后面的全乱 |
| 32 字节(SR+一块) | 7 | 同上 |
公式是:length = 包总长 ÷ 4 − 1。 反推就是:包总长 =(length + 1)× 4。
为什么这么设计:RTCP 要求包长必须是 4 的倍数(32 位对齐),所以长度只需要存「几个 4 字节」。 减一是为了让最短的包(8 字节)也能用一个小数表示。
这个坑的后果:把它当字节数读,第一个包就读错长度,后面的包全部错位,解析出来全是垃圾。 表现是「RTCP 解析总是失败」,而且看不出哪里错。
三、什么时候发
RTCP 是周期发的,不是每包都发。
| 包 | 发送时机 | 大致频率 |
|---|---|---|
| SR | 发过 RTP 的一端定期发 | 几秒一次 |
| RR | 只收的一端定期发 | 几秒一次 |
| SDES | 跟在 SR/RR 后面一起发 | 同上 |
| BYE | 会话结束时发一次 | 一次 |
| APP | 看实现 | 不定 |
为什么不能更频繁:RTCP 是控制流量,RFC 3550 建议它占总带宽的 5% 左右。 发太勤会挤占媒体带宽。
这个 5% 是自适应的:人越多,每路发得越稀疏。 一千人看同一路,每人发的间隔就被拉得很长——否则光是 RTCP 就能把带宽吃光。
四、SDES 里的 CNAME
SDES 最主要的作用是携带 CNAME(规范名字)。 它是唯一能跨会话稳定标识「同一个发送者」的东西。
为什么需要它:SSRC 是随机生成的,一次会话里冲突了会换号; 而 CNAME 不变,所以能用它把音视频两路关联起来。
| 标识 | 会变吗 | 用途 |
|---|---|---|
| SSRC | 冲突时会换 | 区分同一会话内的不同流 |
| CNAME | 整个会话期间不变 | 关联音视频、跨会话识别 |
| SSRC + CNAME | — | 完整识别 |
典型 CNAME 格式:user@host,比如 [email protected]。
五、动手 Demo:RTCP 只有五种包,PT 是认包的唯一依据
这段把五种包的最小形态各造一个,打印头和 length 字段。 重点验证「length = 总长/4 - 1」这条公式。
kinds = [(200, u"SR", 28), (201, u"RR", 8), (202, u"SDES", 12),
(203, u"BYE", 8), (204, u"APP", 12)]
for pt, name, size in kinds:
b0 = (2 << 6) | 0 # V=2, P=0, RC=0
h = struct.pack(u"!BBHI", b0, pt, size // 4 - 1, ssrc)
T([name, u"%d" % pt, u"%d" % (size // 4 - 1), hexdump(h)], [0, 0, 0, 0])
实跑输出:
--------------------------------------------------------------------
RTCP 只有五种包,PT 是认包的唯一依据
--------------------------------------------------------------------
**RTCP 的头也是 8 字节起步,但比 RTP 简单:版本、填充、计数、PT、长度。**
靠 PT 区分是哪一种包,200 到 204。
PT 名字 做什么 最小长度 length 字段
----------------------------------------------------------------------------
200 SR Sender Report 发送者报告 28 字节 6
201 RR Receiver Report 接收者报告 8 字节 1
202 SDES Source Description 源描述 12 字节 2
203 BYE Goodbye 结束 8 字节 1
204 APP Application 自定义 12 字节 2
**length 字段不是字节数,是「32 位字的个数减 1」**:
28 字节的 SR,28/4-1 = 6,所以字段里写 6。
这个坑很经典——把它当字节数读,包就永远解析不完。
**真造这五个包,看头两个 32 位字长什么样:**
包 PT 长度字段 头 8 字节(十六进制)
---------------------------------------------------------
SR 200 6 80 C8 00 06 1A 2B 3C 4D
RR 201 1 80 C9 00 01 1A 2B 3C 4D
SDES 202 2 80 CA 00 02 1A 2B 3C 4D
BYE 203 1 80 CB 00 01 1A 2B 3C 4D
APP 204 2 80 CC 00 02 1A 2B 3C 4D
**第二个字节就是 PT**,一眼能认出是哪种包。
SR 和 RR 的区别:SR 是「我发过数据,这是我的统计」,RR 是「我只收,这是我的统计」。
只收不发的客户端(比如只观看)只发 RR。
**几种包什么时候发:**
包 什么时候
-----------------------------------------------------
SR 发过 RTP 的一端,周期发(默认 5 秒量级)
RR 只收的一端,周期发
SDES 跟 SR/RR 一起发,带 CNAME
BYE 会话结束时发一次
APP 厂商自定义,标准里不定
五个包的 header 第一字节全是 0x80,和 RTP 一样(版本 2)。 第二个字节就是 PT:0xC8 是 200、0xC9 是 201、0xCA 是 202、0xCB 是 203、0xCC 是 204。 记住 0xC8 是 SR——抓包里看到第二字节 0xC8,就找到了发送者报告。
第三、四字节是 length:SR 是 6(表示 7 个 32 位字,共 28 字节),RR 是 1(2 个字,8 字节)。 这两个数看着小得离谱,但它们是按「字数」算的,不是字节。
最后一个字段是 SSRC,四种包都有(BYE 也有)。它告诉你「这个报告说的是哪一路」。 多路流的时候,靠它把报告对应到具体的流——没有它,收到一堆 RR 也不知道说的是谁。
30 秒实验
- 抓一次播放,在 Wireshark 里过滤 rtcp,看有几种 PT 出现。
- 把 SR 包的 length 字段从 6 改成 28,看解析器会报什么。
- 算一下 SR 包 28 字节里,几个字是头、(28/4-1) 为什么是 6。
常见误区
- 以为 length 是字节数。 是 32 位字的个数减一。28 字节的 SR 写的是 6。
- 以为 RTCP 每包都发。 是周期性发的,RFC 建议只占总带宽 5% 左右。
- 以为 SSRC 能长期标识一个发送者。 它随机生成、冲突会换,长期识别要靠 SDES 里的 CNAME。
自测三问(点开对答案)
1. RTCP 的 length 字段怎么算? —— length = 包总长 ÷ 4 − 1。反推:总长 =(length + 1)× 4。因为 RTCP 包必须 4 字节对齐。 2. SR 和 RR 的分工是什么? —— SR 是发数据的一端发的,报告「我发了多少、现在几点」; RR 是只收的一端发的,报告「我收得怎么样」。 3. 为什么需要 CNAME 而不只用 SSRC? —— SSRC 随机生成、冲突会换号,无法长期标识。 CNAME 在整个会话期间不变,能用来关联音视频两路和跨会话识别。下一节拆 SR:28 个字节里装着 NTP 时间、包数、字节数,还有那个 1900 年的纪元。
- ☐ (本节完,下一节 01.07.02 RTCP包结构)