握手之后第一件事是发 connect 命令,它用 AMF0 编码,带 app 与 tcUrl 两个关键字段。 服务器回 _result,里面写 NetConnection.Connect.Success,连接才算建立。
4.2.1 的握手只证明「双方懂协议」,这一节才真正建立一条可用的连接。 4.2.3 在这条连接上开流,4.2.4 才开始播。
一、connect 命令的三个部分
一条 RTMP 命令永远是「命令名 + 事务号 + 若干参数」:
① 命令名:字符串,connect。
② 事务号:数字,从 1 开始,响应会带同一个号回来。
它是「请求-响应」的唯一配对手段。
③ 命令对象:一个 AMF0 对象,装参数。
对象里有三个字段最关键:
① app:应用名,对应 URL 的第一段路径。
rtmp://host/live/test 里 app 就是 live。
② tcUrl:完整地址,服务器拿它做鉴权与路由。
有的服务器只看这个、不看 app,所以两者要一致。
③ flashVer:客户端版本串,服务器据此决定「用哪套兼容策略」。
实测这条 connect 命令编出来 87 字节——比握手的 1536 小得多,但信息密度高得多。
二、AMF0 的编码规律
AMF0 是「先一个字节说类型,再跟内容」的自描述格式。
常用五种:
① 0x00 数字:8 字节 IEEE 双精度,大端。
所以一个数字占 9 字节,比整数多得多。
② 0x01 布尔:1 字节,0 或 1。
③ 0x02 字符串:2 字节长度 + UTF-8 字节。
④ 0x03 对象:一串「键长度 + 键 + 值」,以 00 00 09 结尾。
⑤ 0x05 空值:什么都不跟。
对象结尾那三个字节是最容易漏的: 先两个 0 表示「键名长度为 0」(即没有更多字段),再一个 0x09 是对象结束标记。 漏掉它就解析不出后面的参数,而且报错会指向完全无关的地方。
为什么数字用双精度? 因为 AMF0 是从 ActionScript 的 Number 类型继承来的,那个类型本来就是双精度。 所以封包时别按「整数大概 4 字节」估,一估就少一半。
三、响应与错误码
服务器回的也是命令,但名字分两种:
① _result:成功。
事务号与请求相同,后面跟一个信息对象。
② _error:失败。
同样带事务号,信息对象里有 code 与 description。
connect 的成功标志是 NetConnection.Connect.Success。
它出现在信息对象的 code 字段里。
失败常见三种:
① NetConnection.Connect.Rejected:app 不对或被拒。
② NetConnection.Connect.Failed:服务器内部错误。
③ NetConnection.Connect.AppShutdown:app 被停了。
注意 connect 的响应里还会带几个能力字段:比如 fmsVer、capabilities、level。
它们不影响流程,但抓包时能据此判断对端是哪个服务器软件——不同服务器的 fmsVer 前缀不一样,这是个实用的小技巧。
四、为什么很多人卡在这一步
connect 是整条链路里第一个「带业务语义」的环节,所以坑最多:
① app 与 tcUrl 不一致:服务器按 tcUrl 路由、用 app 找目录,对不上就 404。
② 用错端口:1935 是明文,443 常是 RTMPS,把 RTMPS 的地址填成 rtmp:// 会卡在握手。
③ 推流密钥拼错位置:有的平台要求 rtmp://host/live/stream?key=xxx,整段都算流名,切错了就鉴权失败。
排查顺序建议是:先抓握手是否完成 → 再看 connect 有没有发出去 → 最后看响应里的 code。 这三步能把问题范围缩到一层,比反复改地址有效。
五、动手 Demo:编一条 connect 命令再解回来
手工按 AMF0 编码 connect,再逐字段解析,验证字节与语义对得上。 重点是看清「命令名 + 事务号 + 命令对象」这三段。
import struct
def amf_str(s):
b = s.encode("utf-8")
return b"\x02" + struct.pack("!H", len(b)) + b
def amf_num(v):
return b"\x00" + struct.pack("!d", float(v))
def amf_obj(d):
out = b"\x03"
for k, v in d.items():
kb = k.encode("utf-8")
out += struct.pack("!H", len(kb)) + kb
out += amf_str(v)
return out + b"\x00\x00\x09" # 结尾:两个 0 + 0x09
body = amf_str("connect") + amf_num(1) + amf_obj({
"app": "live",
"tcUrl": "rtmp://127.0.0.1/live",
"flashVer": "FMLE/3.0",
})
print("编码后 %d 字节" % len(body))
print("前 16 字节 %s" % body[:16].hex())
# 解回来
def rd(b, off):
t = b[off]; off += 1
if t == 0x00:
return struct.unpack("!d", b[off:off + 8])[0], off + 8
if t == 0x02:
n = struct.unpack("!H", b[off:off + 2])[0]
off += 2
return b[off:off + n].decode(), off + n
if t == 0x03:
d = {}
while True:
n = struct.unpack("!H", b[off:off + 2])[0]; off += 2
if n == 0:
return d, off + 1
k = b[off:off + n].decode(); off += n
v, off = rd(b, off)
d[k] = v
name, o = rd(body, 0)
tid, o = rd(body, o)
obj, o = rd(body, o)
print("命令名 = %s 事务号 = %.0f" % (name, tid))
print("app = %s tcUrl = %s" % (obj["app"], obj["tcUrl"]))
实跑输出:
--------------------------------------------------------------------
建立连接——connect 命令长什么样
--------------------------------------------------------------------
**① 真编一条 connect 命令**
编码后 87 字节
前 16 字节:020007636f6e6e656374003ff0000000
**三个字段依次是:命令名、事务号、命令对象。**
**事务号 1 表示「这是第一条命令」,响应会带同一个号回来。**
**② 解回来验证**
命令名 = connect
事务号 = 1
app = live
tcUrl = rtmp://127.0.0.1/live
flashVer = FMLE/3.0
**③ AMF0 的类型标记**
标记 类型 后面跟什么
------------------------------------------------
0x00 数字 8 字节 IEEE 双精度,大端
0x01 布尔 1 字节,0 或 1
0x02 字符串 2 字节长度 + UTF-8 字节
0x03 对象 键值对,以 00 00 09 结尾
0x05 空值 什么都不跟
**数字用双精度而不是整数,是因为 AMF0 要从 ActionScript 的 Number 兼容过来。**
**所以一个布尔值占 2 字节、一个数字占 9 字节,封包时别按「大概多少」估。**
**对象结尾那三个字节 `00 00 09` 是这个格式最容易漏的地方:**
**先两个 0 表示「键名长度为 0」(即结束),再一个 0x09 表示对象结束标记。**
**漏掉它就解析不出后面的字段。**
87 字节里,字符串和对象占了绝大部分——三个数字各 9 字节,反而不是大头。
前 16 字节是 02 0007 connect 加事务号的类型标记与开头,一眼能看出「这是字符串、长度 7、内容是 connect」。
解析回来三段完全对上——说明 AMF0 的自描述格式只要按类型标记走,不需要提前约定字段顺序。
30 秒实验
- 把
app改成live2,看编码长度变不变——字符串多一个字节,整体多一个字节。 - 把事务号从 1 改成 2,看前 16 字节里哪个位置变了——双精度的低位在后面,肉眼看不出来,得按字节 diff。
- 删掉对象结尾的
00 00 09,看解析器卡在哪——它会一直读到越界,这是「漏了结束标记」的典型表现。
常见误区
- 「connect 之后就能推流了」——还要 createStream 与 publish。
- 「app 与 tcUrl 填一个就行」——服务器可能两个都看,必须一致。
- 「AMF0 的数字占 4 字节」——是 8 字节双精度,加类型标记共 9 字节。
自测三问(点开对答案)
1. connect 命令的三个部分是什么? —— **命令名、事务号、命令对象。**事务号用于配对请求与响应。 2. AMF0 对象靠什么结尾? —— **`00 00 09`**:两个 0 表示没有更多字段,0x09 是对象结束标记。 3. connect 成功的标志是什么? —— **信息对象里的 code 等于 `NetConnection.Connect.Success`。**- ☐ (本节完,下一节 4.2.3 RTMP建立流)