握手是六包定长字节,一共 4610 个,一个业务数据都不传。 本机没有服务器,那就自己扮一次,把这两边的字节数清楚。
13.1 说了第五段不归 FFmpeg 管,这一节就去看那一段在干什么。 看完之后 13.3 再来推流,就知道自己在推给谁。
一、本机没有服务器
先把「装没装」这件事查清。 端口 用途 结果
1935 RTMP 默认端口 没有(TimeoutError) 8080 常给 HTTP-FLV 没有(TimeoutError)
所以这一节不讲「怎么装服务器」,讲「服务器在握手这一层干什么」。 这一段能真跑,而且跑完能看懂所有 RTMP 资料里的那几张图。 注意这里用的是裸 socket,不是 FFmpeg——因为握手是传输层的事,用 socket 看反而最清楚。
二、握手就是六包定长字节
RTMP 连接的第一件事不是发命令,是交换固定长度的块。 包 谁发 长度 装什么
C0 客户端 1 版本号,现在都是 3 C1 客户端 1536 时间戳、零、随机数 S0 服务端 1 版本号 S1 服务端 1536 时间戳、零、随机数 C2 客户端 1536 把 S1 原样回抄 S2 服务端 1536 把 C1 原样回抄
加起来 1+1536 乘 3 再加 1,正好 4610 字节。 握手不传任何业务数据,只证明「双方都能收发定长块」。 为什么非要定长: 定长的东西最好校验——收到 1536 个字节就是 1536 个,少一个都说明连接有问题。 后面真正的数据才用变长的 chunk 分块。 真实 RTMP 还有复杂握手的 digest 校验(把一段密钥混进 C1 的随机区),这一节只做简单握手, 因为要讲清的是「交换固定长度块」这个骨架,不是那几行哈希。
三、真跑一遍两端握手
服务端与客户端都自己写,两个线程对着跑。 项 值
客户端发的 C0 1 字节(版本 3) 客户端发的 C1 1536 字节 服务端收到 C0/C1 1/1536 服务端回的字节 3073 客户端收到 S0 3 服务端收到 C2 1536 S2 与 C1 一样吗 True 全程耗时 不到 1 毫秒
3073 = 1 + 1536 + 1536,就是 S0 加 S1 加 S2,一个不多一个不少。 可见握手这一层完全没有额外开销。 S2 与 C1 一模一样,这就是握手的设计:服务端把客户端给的随机块原样回抄, 客户端看到回抄正确,就知道对面真收到了。 这种「原样回抄」是最省事的连通性证明。 耗时不到 1 毫秒也说明一件事:握手本身不慢。 直播的延迟不是被握手吃掉的,是被后面的缓冲吃掉的(13.1 那张表)。
四、真服务器怎么选
三个常见的,都开源、都能跑在 Linux 上。 服务器 特点 最小配置难点
SRS 国产、文档全、功能多 conf 里开 rtmp 段 nginx-rtmp 挂在 nginx 上,最常见 要自己编模块 MediaMTX 单文件、零依赖 改一个 yml
三者的共同点是「监听 1935、收 RTMP、转给播放端」,差别只在配置形态与附带功能(录制、 鉴权、转 HLS、转 WebRTC)。 自己练手推荐 MediaMTX:一个二进制文件、一个 yml,解压就能跑,不折腾编译。 真上生产再考虑 SRS 或 nginx-rtmp,因为它们能挂到已有的运维体系里。
五、对接服务器只要会三件事
握手之后还有 chunk 分块、AMF 命令、发布点鉴权三层, 但都不是「FFmpeg 的 API」能控制的——那是服务器的事。 要会的 例子
地址格式 rtmp://域名/应用名/流名 流名与鉴权 stream key 或带 token 的查询串 失败怎么分 先分网络、再分鉴权、最后看编码
第一件事最容易错:应用名与流名是两段,中间的斜杠不能少。
写成 rtmp://host/live 是把流名也省了,服务器多半直接拒。
第三件事是排障的顺序:
先 ping 或 socket 试端口(网络),再用带鉴权的地址试一次(鉴权),最后才怀疑编码参数。
反过来查,最常见的结局是改了半天码率,其实是 stream key 过期了。
六、动手 Demo:自己扮一次 RTMP 握手
两个线程:一个扮服务端收 C0+C1 回 S0+S1+S2,一个扮客户端发 C0+C1 再回 C2。
import socket
import struct
import threading
def server(srv, out):
srv.settimeout(3.0)
c, _ = srv.accept()
c0 = c.recv(1) # 版本号
c1 = b""
while len(c1) < 1536: # 定长 1536
c1 += c.recv(1536 - len(c1))
s1 = struct.pack(">II", 0, 0) + bytes(range(256)) * 5 + bytes(248)
c.sendall(bytes([3]) + s1 + c1[:1536]) # S0 + S1 + S2
c2 = b""
while len(c2) < 1536:
c2 += c.recv(1536 - len(c2))
out["c0"], out["c2"] = len(c0), len(c2)
c.close()
srv = socket.socket()
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", 0))
srv.listen(1)
out = {}
threading.Thread(target=server, args=(srv, out), daemon=True).start()
cli = socket.create_connection(("127.0.0.1", srv.getsockname()[1]))
c1 = struct.pack(">II", 0, 0) + bytes(range(256)) * 5 + bytes(248)
cli.sendall(bytes([3]) + c1) # C0 + C1
buf = b""
while len(buf) < 1 + 1536 + 1536: # 等 S0 + S1 + S2
buf += cli.recv(65536)
cli.sendall(buf[1:1537]) # C2 回抄 S1
print(out, buf[0], buf[1537:3073] == c1[:1536])
实跑输出:
--------------------------------------------------------------------
自己扮一次 RTMP 握手
--------------------------------------------------------------------
**① 本机没有流媒体服务器**
端口 结果
---------------------------------
1935 没有(TimeoutError)
8080 没有(TimeoutError)
**1935 是 RTMP 的默认端口,8080 常被拿去做 HTTP-FLV。两个都没人听。**
**所以这一节不讲「怎么装服务器」,讲「服务器在握手这一层干什么」。**
**② 握手就是四包定长字节**
包 谁发 长度 装什么
-------------------------------------------------
C0 客户端 1 版本号,现在都是 3
C1 客户端 1536 时间戳、零、随机数
S0 服务端 1 版本号
S1 服务端 1536 时间戳、零、随机数
S2 服务端 1536 把 C1 原样回抄
C2 客户端 1536 把 S1 原样回抄
**加起来 1+1536 乘 3 再加 1,正好 4610 字节。**
**握手不传任何业务数据,只证明「双方都能收发定长块」。**
**③ 真跑一遍**
项 值
------------------------------------
客户端发的 C0 1(版本 3)
客户端发的 C1 1536 字节
服务端收到 C0/C1 1/1536
服务端回的字节 3073
客户端收到 S0 3
服务端收到 C2 1536
S2 与 C1 一样吗 True
全程耗时 1.17 毫秒
**S2 与 C1 一模一样,这就是握手的设计**:**服务端把客户端给的随机块原样回抄**,
客户端看到回抄正确,就知道对面真收到了。**这个「原样回抄」是一种最省事的连通性证明。**
**④ 真服务器怎么选**
服务器 特点 最小配置难点
------------------------------------------------------------------
SRS 国产、文档全、功能多 conf 文件里开 rtmp 段
nginx-rtmp 挂在 nginx 上,最常见 要自己编模块
MediaMTX 单文件、零依赖 改一个 yml 就行
**三者的共同点是「监听 1935、收 RTMP、转给播放端」**,
**差别只在配置形态与附带功能(录制、鉴权、转 HLS)。**
**⑤ 为什么这一段只能讲到这**
**RTMP 握手之后还有 chunk 分块、AMF 命令、发布点鉴权三层**,
**每一层都有细节,但都不是「FFmpeg 的 API」能控制的**——**那是服务器的事。**
**对接服务器时真正要会的只有三件**:
**地址格式(`rtmp://域名/应用名/流名`)、流名与鉴权参数、以及推流失败时先分清是网络还是鉴权。**
服务端回 3073 字节 = 1+1536+1536,握手一个字节的额外开销都没有。 S2 与 C1 完全相同,这就是「原样回抄」的连通性证明。
30 秒实验
- 把 C0 的版本号从 3 改成 6,看服务端会不会拒(提示:这一节的服务端没校验)。
- 把 C1 只发 1000 字节,看服务端会不会一直等(提示:定长循环卡住)。
- 改一下 S1 的内容,看客户端回抄的 C2 跟着变不变。
常见误区
- 以为 RTMP 一上来就发业务命令。 先交换 4610 字节的定长块。
- 以为握手慢。 它不到 1 毫秒,慢的是后面的缓冲。
- 以为地址里流名可以省。
rtmp://host/app缺流名,多数服务器直接拒。
自测三问(点开对答案)
1. RTMP 握手一共传多少字节? —— 4610:C0+C1 各 1 与 1536,S0+S1+S2 各 1 与 1536 两个,再加 C2 的 1536。 2. S2 里装的是什么? —— C1 的原样回抄,用来证明双方真的收到了对方的块。 3. 为什么握手要定长? —— 定长最好校验,少一个字节就说明连接有问题。- ☐ (本节完,下一节 13.3 FFmpeg 进行 RTMP 直播推流)