块流就是把一条消息切碎,让多路数据能在一条 TCP 上交错。 默认块大小 128 字节,调大它省的其实不是带宽,而是块数与解析开销。
4.2 讲的是「命令层」,从这一节起进入「传输层」。 4.4.2 讲块的格式,4.4.3 到 4.4.5 逐段拆头,4.4.6 讲交错重组。
一、为什么要切
一条 TCP 连接要同时送四类东西:命令、音频、视频、控制。 如果每条消息都整块发,那么一条 20 KB 的视频帧会独占连接,音频就只能等它写完。
等多久? 按 2 Mbps 算,20 KB 大约要 80 毫秒。 音频的帧间隔是 20 毫秒左右,所以一次视频大帧就能让声音断掉四拍。 这就是必须切块的理由。
切成 128 字节之后,音频就能在视频块之间插队。 代价是每个块要带一个头部,但那只有 1 字节(后续块),便宜得可以忽略。
二、块大小影响的是块数
很多人以为「块调大能省带宽」,这个说法是错的。 实测一条 20 KB 的消息: 按 128 切是 160 块、块头共 171 字节,占 0.83%;按 4096 切只有 5 块、块头 16 字节,占 0.08%。 两者差十倍,但绝对值都很小。
真正的差别在块数:160 块对 5 块,差了 32 倍。 接收端每处理一个块,都要走一次「查 csid、拼缓冲、更新状态」的流程。 块数多就是纯粹的 CPU 与调度开销,跟带宽没关系。
所以实际推流都会在 connect 之后发一条「设置块大小」,常见值是 4096 或 60000。 但调太大也有代价:交错粒度变粗,音频要等更久才能插进来。
三、块大小是单向的
这一点特别容易误解:我发给你 4096,只约束「我发给你的块」。 你要发给我,还得自己再设一次。
所以抓包时常见「两个方向的块大小不一样」,这不是配错。 推流端设 4096(因为视频帧大),服务器回推流端可能还是 128(命令都很小)。
还有一条硬规定:设置块大小这条消息本身,必须用「旧块大小」切。 因为它只有 4 字节消息体,一条块就装完了,实际上不会触发切换——但规则要记住,否则自己实现时容易写成「先改再发」。 正确顺序是「按旧尺寸发完这条,之后才按新尺寸」。
四、块流 ID 是车道
每条块流用 csid 标识,取值 2 到 65599。 实作的默认分工是:2 走协议控制与用户控制、3 走 AMF 命令、4 走音频、6 走视频。
csid 的作用等价于「车道」: 同一条 csid 上的块必须按顺序发(因为消息头的压缩依赖「上一次的值」);不同 csid 之间可以任意交错。 这就是为什么音频能插在视频大帧中间——它们走的是不同车道。
一句话概括:csid 是车道、块是车厢、消息是货。 多车道可以并行跑,但同一车道上的车厢不能超车。
五、动手 Demo:128 字节一块的老规矩
先看一条 300 字节消息怎么切成 3 块,再算块大小对块数的影响。 重点看「块头占比」与「块数」哪个才是真正的成本。
msg = bytes(range(256)) * 80 # 凑一条 20480 字节的大消息
csize = 128
n = (len(msg) + csize - 1) // csize
print("消息 %d 字节,按 %d 切,%d 块" % (len(msg), csize, n))
parts = [len(msg[i:i + csize]) for i in range(0, len(msg), csize)]
print("前 3 块载荷:%s 末块 %d" % (parts[:3], parts[-1]))
print("")
for cs in (128, 512, 1024, 4096, 60000):
cnt = (len(msg) + cs - 1) // cs
over = 12 + (cnt - 1) * 1 # 首块 12 字节头,后续各 1 字节
print("块大小 %-6d 块数 %-4d 块头 %-4d 字节 占 %.2f%%"
% (cs, cnt, over, over * 100.0 / len(msg)))
print("")
print("块数从 %d 降到 %d,少了 %.0f 倍"
% (160, 5, 160 / 5.0))
实跑输出:
--------------------------------------------------------------------
块流——128 字节一块的老规矩
--------------------------------------------------------------------
**① 默认块大小是 128 字节**
一条 300 字节的消息,按 128 切,变成 3 块
各块载荷:128、128、44
**为什么要切?因为 RTMP 要在一条 TCP 连接上「同时」送多路数据。**
**如果一条大消息不切,它就会独占整条连接,音频要等视频发完才能走。**
**切块之后可以交错发送,语音就不会被视频大帧堵住。**
**② 块大小真正影响的是「块数」**
块大小 块数 块头总量 占消息
-------------------------------------------
128 160 171 字节 0.83%
512 40 51 字节 0.25%
1024 20 31 字节 0.15%
4096 5 16 字节 0.08%
60000 1 12 字节 0.06%
**先看清一件事:块头总量只占百分之零点几,所以「调大块省带宽」是错的。**
**块头本身太便宜了——后续块只有 1 字节。**
**真正的收益在块数:一条 20 KB 的视频消息,按 128 切是 160 块。**
**按 4096 切只有 5 块,少了 32 倍。**
**接收端每处理一个块,都要走一次「查 csid、拼缓冲、更新状态」的流程。**
**块数多就是纯粹的 CPU 与调度开销,跟带宽没关系。**
**所以实际推流会在 connect 之后发一条「设置块大小」的消息把它调大,常见值是 4096 或 60000。**
**但调得太大也有代价:交错粒度变粗,音频要等更久才能插进来。**
**③ 一个容易误解的地方**
**块大小是「单向生效」的:我发给你 4096,只约束我发给你的块;你发给我还要单独设一次。**
**所以抓包时两边的块大小常常不一样,别以为是配错了。**
**还有:改块大小这条消息本身必须用小尺寸发。**
**标准规定它自己要用「旧块大小」切**——**否则接收方还不知道新尺寸,就会解错。**
20 KB 的消息按 128 切是 160 块、按 4096 切是 5 块。 块头占比从 0.83% 降到 0.08%——看着差十倍,但绝对值都小到可以忽略。 所以「调大块」的真实收益是块数少 32 倍,省的是 CPU 与调度,不是带宽。
30 秒实验
- 把消息改成 300 字节,看各个块大小下块数怎么变——512 以上都只有 1 块,这就是「小块无所谓」的边界。
- 把首块的 12 字节头改成 1 字节(想象成格式 3),再算占比——会低到几乎为零,说明块头真的不是瓶颈。
- 把块大小设成 1,算块数——20480 块,这时块头就占到 50% 了,这是理论上的下限。
常见误区
- 「调大块能省带宽」——块头只占 0.8%,省的是解析开销。
- 「块大小一设,两个方向都生效」——单向的,各设各的。
- 「块越大越好」——交错粒度变粗,音频要等更久。
自测三问(点开对答案)
1. 为什么要切块? —— **为了让多路数据在一条 TCP 上交错**,避免音频被视频大帧堵住。 2. 调大块大小真正的收益是什么? —— **块数大幅减少**(160 → 5),省的是 CPU 与调度,不是带宽。 3. 块大小是单向还是双向生效? —— **单向。**我设的只约束我发出的块。- ☐ (本节完,下一节 4.4.2 消息块格式)