RTMP 握手:一场客户端与服务端的“暗号”对决
如果把 RTMP 连接比作两个特工接头那握手阶段就是他们互相确认身份、对暗号的过程。今天我们就顺着这段代码把这个“接头故事”讲给你听。第一第一次碰面 —— C0 与 C1故事的主角是两个角色客户端想推流/拉流的那个服务端Monibuca也就是这段代码的主人客户端先开口。它发来一个C0 C1 的组合包一共1537 字节C01 字节 C11536 字节C0一句话表明来意C0 只有 1 个字节意思是我用的是 RTMP 第 3 版协议代码里这么写if C0C1[0] ! RTMP_HANDSHAKE_VERSION { // 0x03 return errors.New(C0 Error) }如果不是0x03服务端直接翻脸——不认识你走开C1一张复杂的名片C1 有 1536 字节结构是这样的字段大小含义Time4 字节当前时间戳Zero4 字节关键字段Random1528 字节随机填充Zero 字段是整段故事的分岔路口var zero int util.GetBE(C1[4:8], zero) if zero 0 { return nc.simple_handshake(C1, checkC2) } return nc.complex_handshake(C1)zero 0 → 简单握手大家都是老实人zero ! 0 → 复杂握手来对暗号第二简单模式 —— 老实人的互信如果客户端是个老实人Zero 字段全为 0那双方就走简单握手。服务端做的事情很简单1. 回一个 S0版本号 3 2. 回一个 S1时间戳 Monibuca 字符串 3. 把客户端的 C1 原封不动当 S2 发回去 4. 读客户端的 C2验证一下S0S1[0] RTMP_HANDSHAKE_VERSION util.PutBE(S0S1[1:5], time.Now().Unix()0xFFFFFFFF) copy(S0S1[5:], Monibuca) nc.Write(S0S1) // S0 S1 nc.Write(C1) // S2 C1原样返回客户端那边也差不多它构造 C0C1 发出去然后等 S0S1再把 S0S1 当 C2 发回去// 客户端构造 C1 util.PutBE(C1[0:4], time.Now().Unix()0xFFFFFFFF) util.PutBE(C1[4:8], 0) // Zero 0声明简单握手 // 填充随机数据...简单握手就像两个人见面握个手你好你好再见再见——完事。第三复杂模式 —— 特工的暗号对决如果 Zero 不为 0说明客户端想玩点高级的。这就是RTMP 复杂握手Digest Handshake。3.1 两套暗号方案RTMP 规范定义了两种排布方式叫Scheme 0 和Scheme 1。区别在于在 1536 字节里密钥key和摘要digest的位置不同。两种方案的结构对比Scheme 0: [Time][Version][Digest区域][Key区域] Scheme 1: [Time][Version][Key区域][Digest区域]每个区域里偏移量都是由数据本身的前 4 个字节算出来的相当于藏宝图上的坐标。3.2 偏移量怎么算以 Scheme 0 的 Digest 偏移为例func scheme0_Digest_Offset(C1S1 []byte) int { // 取偏移字段的 4 个字节 scheme0_digest_offset : int(C1S1[8]) int(C1S1[9]) int(C1S1[10]) int(C1S1[11]) // 取模 基础偏移 scheme0_digest_offset (scheme0_digest_offset % 764) 4 4 4 return scheme0_digest_offset }本质就是从几个字节里算出一个数模一下最大值再加个基础偏移就得到了 digest 藏在哪。密钥key的偏移也是同理只是读的位置不同。3.3 验证客户端身份服务端拿到 C1 后要做的事是分别用 Scheme 0 和 Scheme 1 尝试解析对每种方案找到 digest 的位置把 digest 前后的数据拼起来相当于去掉 digest 后的纯净数据用FP_KEY 的前 30 字节做 HMAC-SHA256 密钥对纯净数据算哈希拿算出来的哈希和 C1 里的 digest 比对tmp_Hash, _ : HMAC_SHA256(c1_Part1_Part2, FP_KEY[:30]) ok bytes.Equal(digest, tmp_Hash)如果匹配上了 → 客户端身份确认如果两种方案都失败 → 你不是自己人scheme, challenge, digest, ok, err clientScheme(C1, 1) if ok { return ... } scheme, challenge, digest, ok, err clientScheme(C1, 0) if ok { return ... } return ... errors.New(Client scheme error)3.4 服务端回敬 S1 S2验证通过后服务端要回两个东西S1 和S2。构造 S1S1 : create_S1() // 随机生成 1536 字节然后找到 S1 里放 digest 的位置把去掉 digest 区域的纯净数据用FMS_KEY 前 36 字节做 HMAC-SHA256算出来的结果填回 digest 位置。tmp_Hash, _ HMAC_SHA256(S1_Part1_Part2, FMS_KEY[:36]) copy(S1[S1_Digest_Offset:], tmp_Hash)构造 S2S2 分两部分S2 随机数据(1504字节) digest(32字节)digest 的生成链条是digest HMAC-SHA256(随机数据, HMAC-SHA256(客户端digest, FMS_KEY[:68]))tmp_Hash, _ HMAC_SHA256(digest, FMS_KEY[:68]) // 先用客户端digest S2_Digest, _ HMAC_SHA256(S2_Random, tmp_Hash) // 再对随机数据算最后一把发出去buffer : net.Buffers{[]byte{RTMP_HANDSHAKE_VERSION}, S1, S2_Random, S2_Digest} buffer.WriteTo(nc) nc.Skip(1536) // 跳过客户端可能发来的 C2复杂握手不需要 C2第四故事总结阶段简单握手复杂握手标志C1.Zero 0C1.Zero ! 0验证方式原样返回数据HMAC-SHA256 签名验证密钥无FP_KEY / FMS_KEY安全等级低谁都能连高需要暗号典型场景老客户端、调试Flash Player、正规推流第五核心工具函数一览函数作用Handshake()入口根据 Zero 字段分流simple_handshake()简单握手实现complex_handshake()复杂握手实现validateClient()尝试验证两种 SchemeclientScheme()按指定 Scheme 解析并验证schemeX_Digest_Offset()计算 digest 偏移schemeX_Key_Offset()计算 key 偏移HMAC_SHA256()HMAC-SHA256 封装create_S1()生成随机 S1cerate_S2()生成随机 S2尾声RTMP 握手说到底就是一句话简单握手是你好我好大家好复杂握手是对不上暗号就别想进门。这段代码完整实现了 RTMP 规范里的两种握手方式是 Monibuca 流媒体服务器能和各种 RTMP 客户端OBS、FFmpeg、Flash Player 等顺利对接的关键所在。