RTMP 协议精讲 / 02 - 握手过程
握手过程(Handshake)
2.1 握手概述
RTMP 连接建立的第一步是 握手(Handshake)。与 TCP 三次握手不同,RTMP 握手是在 TCP 连接之上进行的 应用层协商,目的是:
- 验证协议版本兼容性
- 同步时间戳
- 交换随机数据(用于加密握手时生成密钥)
- 建立通信信任关系
客户端 (Client) 服务端 (Server)
│ │
│──── TCP 三次握手 ──────────────→│ ← TCP 层
│ │
│──── C0 + C1 ──────────────────→│ ← RTMP 握手开始
│←─── S0 + S1 + S2 ─────────────│
│──── C2 ───────────────────────→│ ← RTMP 握手完成
│ │
│ 正式通信开始 │
│════ connect() ═══════════════→│ ← AMF 命令
2.2 握手数据包结构
握手由 6 个数据包 组成:C0、C1、C2(客户端)、S0、S1、S2(服务端)。
2.2.1 C0 / S0 — 版本字节
C0 和 S0 各 1 字节,表示 RTMP 协议版本号。
0 1
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
│ Version │ C0 / S0
+-+-+-+-+-+-+-+-+
字段说明:
┌──────────┬───────────────────────────────────────────────┐
│ 字段 │ 说明 │
├──────────┼───────────────────────────────────────────────┤
│ Version │ RTMP 版本号,当前规范定义为 3 │
│ │ 0-2: 早期私有版本 │
│ │ 3: 当前标准版本 │
│ │ 4-255: 保留 │
└──────────┴───────────────────────────────────────────────┘
2.2.2 C1 / S1 — 时间戳与随机数据
C1 和 S1 各 1536 字节(12288 bits),结构如下:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
│ time (4 bytes) │
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
│ zero (4 bytes) │
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
│ │
│ random data (1528 bytes) │
│ ... │
│ │
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
字段说明:
┌──────────────┬────────────────────────────────────────────────────┐
│ 字段 │ 说明 │
├──────────────┼────────────────────────────────────────────────────┤
│ time │ 4 字节时间戳。C1 = 客户端发送时间,S1 = 服务端发送时间│
│ │ 可以是 0 或从 epoch 开始的毫秒数 │
├──────────────┼────────────────────────────────────────────────────┤
│ zero │ 4 字节,必须为 0(规范中保留字段) │
├──────────────┼────────────────────────────────────────────────────┤
│ random data │ 1528 字节随机数据 │
│ │ 用于加密握手时生成密钥 │
│ │ 普通握手中可为任意值 │
└──────────────┴────────────────────────────────────────────────────┘
2.2.3 C2 / S2 — 回声确认
C2 和 S2 各 1536 字节,结构与 C1/S1 相同,但含义不同:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
│ time (4 bytes) │ ← S2: 回显 C1.time
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ C2: 回显 S1.time
│ time2 (4 bytes) │ ← S2: 收到 C1 的时间
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ C2: 收到 S1 的时间
│ │
│ random data (1528 bytes) │ ← S2: 回显 C1.random
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ C2: 回显 S1.random
2.3 标准握手流程(RTMP)
完整交互时序
Client Server
│ │
│──── 1. C0 (1 byte: version=3) ────────→│
│──── C1 (1536 bytes: time+random) ──→│
│ │
│←─── 2. S0 (1 byte: version=3) ─────────│
│←─── S1 (1536 bytes: time+random) ───│
│←─── S2 (1536 bytes: echo C1) ───────│
│ │
│──── 3. C2 (1536 bytes: echo S1) ──────→│
│ │
│════ 4. 握手完成,开始正常通信 ═══════════│
│ │
详细步骤说明
Step 1: 客户端发送 C0 + C1
客户端在 TCP 连接建立后,立即发送 C0 和 C1(通常合并为一个 TCP 包,共 1537 字节)。
# Python 示例:构造 C0 + C1
import struct
import os
import time
def create_c0c1():
# C0: 版本号
c0 = struct.pack('B', 3) # version 3
# C1: 时间戳 + 零 + 随机数据
timestamp = int(time.time()) & 0xFFFFFFFF
zero = 0
random_data = os.urandom(1528)
c1 = struct.pack('>II', timestamp, zero) + random_data
return c0 + c1
# 发送
data = create_c0c1()
print(f"C0+C1 总长度: {len(data)} bytes") # 1537
Step 2: 服务端回 S0 + S1 + S2
服务端收到 C0+C1 后,验证版本号,然后构造并发送 S0、S1、S2(通常合并为一个 TCP 包,共 3073 字节)。
def create_s0s1s2(c1_data):
# S0: 版本号
s0 = struct.pack('B', 3)
# S1: 服务端时间戳 + 零 + 随机数据
server_timestamp = int(time.time()) & 0xFFFFFFFF
zero = 0
random_data = os.urandom(1528)
s1 = struct.pack('>II', server_timestamp, zero) + random_data
# S2: 回显 C1 的数据
c1_time = c1_data[0:4]
c1_time2 = struct.pack('>I', int(time.time()) & 0xFFFFFFFF) # 收到 C1 的时间
c1_random = c1_data[8:1536]
s2 = c1_time + c1_time2 + c1_random
return s0 + s1 + s2
Step 3: 客户端发送 C2
客户端收到 S0+S1+S2 后,构造 C2 回显 S1 的数据。
def create_c2(s1_data):
# C2: 回显 S1 的数据
s1_time = s1_data[0:4]
s1_time2 = struct.pack('>I', int(time.time()) & 0xFFFFFFFF) # 收到 S1 的时间
s1_random = s1_data[8:1536]
return s1_time + s1_time2 + s1_random
Step 4: 握手完成
服务端收到 C2 后,验证 C2 中回显的 S1 数据正确,握手完成,双方进入正常通信状态。
2.4 完整 Python 实现
以下是一个完整的 RTMP 握手客户端实现:
#!/usr/bin/env python3
"""
RTMP Handshake Client
用法: python3 rtmp_handshake.py <host> <port>
"""
import socket
import struct
import os
import time
import sys
class RTMPHandshake:
"""RTMP 握手处理器"""
RTMP_VERSION = 3
HANDSHAKE_SIZE = 1536
def __init__(self, host: str, port: int = 1935):
self.host = host
self.port = port
self.sock = None
def connect(self):
"""建立 TCP 连接"""
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(5.0)
self.sock.connect((self.host, self.port))
print(f"[TCP] 已连接 {self.host}:{self.port}")
def create_c0c1(self) -> bytes:
"""构造 C0+C1"""
# C0: 1 byte version
c0 = struct.pack('B', self.RTMP_VERSION)
# C1: 1536 bytes
timestamp = int(time.time()) & 0xFFFFFFFF
zero = 0
random_data = os.urandom(self.HANDSHAKE_SIZE - 8)
c1 = struct.pack('>II', timestamp, zero) + random_data
return c0 + c1
def create_c2(self, s1_data: bytes) -> bytes:
"""构造 C2(回显 S1)"""
# time: 回显 S1 的 time 字段
time_echo = s1_data[0:4]
# time2: 收到 S1 的时间
time2 = struct.pack('>I', int(time.time()) & 0xFFFFFFFF)
# random echo: 回显 S1 的随机数据
random_echo = s1_data[8:self.HANDSHAKE_SIZE]
return time_echo + time2 + random_echo
def parse_s0s1s2(self, data: bytes):
"""解析 S0+S1+S2"""
# S0: 版本
s0_version = data[0]
print(f"[S0] 版本: {s0_version}")
# S1: 1536 bytes
s1 = data[1:1 + self.HANDSHAKE_SIZE]
s1_time, s1_zero = struct.unpack('>II', s1[:8])
print(f"[S1] 时间戳: {s1_time}, 保留字段: {s1_zero}")
# S2: 1536 bytes
s2_start = 1 + self.HANDSHAKE_SIZE
s2 = data[s2_start:s2_start + self.HANDSHAKE_SIZE]
s2_time, s2_time2 = struct.unpack('>II', s2[:8])
print(f"[S2] 回显时间: {s2_time}, 收到时间: {s2_time2}")
return s1
def handshake(self) -> bool:
"""执行完整握手"""
try:
# Step 1: 发送 C0 + C1
c0c1 = self.create_c0c1()
self.sock.sendall(c0c1)
print(f"[发送] C0+C1 ({len(c0c1)} bytes)")
# Step 2: 接收 S0 + S1 + S2
s0s1s2 = b''
expected_len = 1 + self.HANDSHAKE_SIZE * 2 # 3073 bytes
while len(s0s1s2) < expected_len:
chunk = self.sock.recv(expected_len - len(s0s1s2))
if not chunk:
raise ConnectionError("连接被服务端关闭")
s0s1s2 += chunk
print(f"[接收] S0+S1+S2 ({len(s0s1s2)} bytes)")
s1 = self.parse_s0s1s2(s0s1s2)
# Step 3: 发送 C2
c2 = self.create_c2(s1)
self.sock.sendall(c2)
print(f"[发送] C2 ({len(c2)} bytes)")
print("[握手] 完成!可以开始正常通信")
return True
except Exception as e:
print(f"[握手] 失败: {e}")
return False
def close(self):
"""关闭连接"""
if self.sock:
self.sock.close()
print("[TCP] 连接已关闭")
def main():
if len(sys.argv) < 2:
print("用法: python3 rtmp_handshake.py <host> [port]")
print("示例: python3 rtmp_handshake.py localhost 1935")
sys.exit(1)
host = sys.argv[1]
port = int(sys.argv[2]) if len(sys.argv) > 2 else 1935
hs = RTMPHandshake(host, port)
try:
hs.connect()
hs.handshake()
finally:
hs.close()
if __name__ == '__main__':
main()
运行测试:
# 确保有 RTMP 服务运行(如 SRS)
docker run -d --name srs -p 1935:1935 ossrs/srs:5
# 运行握手测试
python3 rtmp_handshake.py localhost 1935
预期输出:
[TCP] 已连接 localhost:1935
[发送] C0+C1 (1537 bytes)
[接收] S0+S1+S2 (3073 bytes)
[S0] 版本: 3
[S1] 时间戳: 1715320800, 保留字段: 0
[S2] 回显时间: 1715320799, 收到时间: 1715320800
[发送] C2 (1536 bytes)
[握手] 完成!可以开始正常通信
[TCP] 连接已关闭
2.5 握手模式
RTMP 定义了三种握手模式:
2.5.1 简单握手(Simple Handshake)
最常用的模式,即上述标准流程。C1/S1/C2/S2 中的 random data 可以是任意值。
2.5.2 复杂握手(Complex Handshake)
部分 RTMP 实现(如 Adobe Media Server)使用复杂握手,在 C1/S1 中嵌入 数字签名:
C1/S1 结构(复杂握手):
┌─────────────────────────────────────────────────┐
│ time (4 bytes) │
│ version (4 bytes) — 通常为 0x00000000 │
├─────────────────────────────────────────────────┤
│ key (764 bytes) │
│ ┌─────────────────────────────────────────────┐│
│ │ offset (1 byte) ││
│ │ random data (offset bytes) ││
│ │ DH public key (128 bytes) ││
│ │ random data (764 - offset - 128 - 1 bytes) ││
│ └─────────────────────────────────────────────┘│
├─────────────────────────────────────────────────┤
│ digest (764 bytes) │
│ ┌─────────────────────────────────────────────┐│
│ │ offset (1 byte) ││
│ │ random data (offset bytes) ││
│ │ HMAC-SHA256 digest (32 bytes) ││
│ │ random data (764 - offset - 32 - 1 bytes) ││
│ └─────────────────────────────────────────────┘│
└─────────────────────────────────────────────────┘
复杂握手 vs 简单握手判断:
def is_complex_handshake(c1_data: bytes) -> bool:
"""判断 C1 是否为复杂握手"""
# 检查 key 字段末尾是否包含 "Genuine FP" 标记
fp_key = b"Genuine FP"
for i in range(764):
if c1_data[4 + i:4 + i + len(fp_key)] == fp_key:
return True
return False
注意:大多数开源 RTMP 服务器(如 SRS)默认使用简单握手,但对复杂握手有兼容支持。
2.5.3 握手模式对比
| 特性 | 简单握手 | 复杂握手 |
|---|---|---|
| C1/S1 结构 | time + zero + random | time + version + key + digest |
| 安全性 | 低(无验证) | 中(HMAC-SHA256 签名) |
| 兼容性 | 所有实现 | 仅 Adobe 实现 |
| 性能 | 高 | 略低 |
| 开源实现 | ✅ 默认 | ⚠️ 部分支持 |
2.6 加密握手(RTMPE)
RTMPE(RTMP Encrypted)是 Adobe 的私有加密方案,在标准握手基础上增加密钥协商:
RTMPE 握手流程:
1. 执行标准握手(C0C1/S0S1S2/C2)
2. 使用 DH(Diffie-Hellman)算法交换密钥
3. 基于共享密钥初始化 RC4 加密流
4. 后续所有数据使用 RC4 加密
密钥计算流程:
┌───────────┐ ┌──────────────┐ ┌─────────────┐
│ C1.random │ │ DH 共享密钥 │ │ HMAC 计算 │
│ S1.random │────→│ Key Agreement │────→│ 生成 RC4 Key │
└───────────┘ └──────────────┘ └─────────────┘
│
▼
┌─────────────────┐
│ RC4 加密流初始化 │
│ Client Key │
│ Server Key │
└─────────────────┘
RTMPS(TLS 加密)
RTMPS 是更安全的替代方案,在 TLS 握手之上运行标准 RTMP:
客户端 服务端
│ │
│──── TLS ClientHello ────────→│ ← TLS 握手
│←─── TLS ServerHello ────────│
│──── TLS Finished ───────────→│
│ │
│ TLS 加密通道建立 │
│════ RTMP C0C1 ════════════→│ ← 标准 RTMP 握手
│════ S0S1S2 ════════════════←│ (在 TLS 内部)
│════ C2 ════════════════════→│
│ │
│ 加密的 RTMP 通信 │
2.7 版本协商
RTMP 版本协商在 C0/S0 中完成:
| 版本号 | 含义 | 说明 |
|---|---|---|
| 0 | 非正式版本 | 早期私有实现 |
| 1 | 保留 | — |
| 2 | 保留 | — |
| 3 | 当前标准 | 所有主流实现使用此版本 |
| 6 | FP9 handshake | Flash Player 9 改进握手 |
| 7-255 | 保留 | 未来扩展 |
版本不匹配处理
def check_version(client_version: int, server_version: int) -> bool:
"""检查版本兼容性"""
if client_version == server_version:
return True
# 服务端可以选择回退到客户端版本
if server_version >= 3 and client_version >= 3:
return True
return False
2.8 Wireshark 抓包分析
使用 Wireshark 可以直观地观察 RTMP 握手过程:
抓包步骤
# 1. 启动抓包
sudo wireshark -i lo -f "tcp port 1935" &
# 2. 启动 RTMP 服务器
docker run --rm -p 1935:1935 ossrs/srs:5
# 3. 使用 FFmpeg 推流触发握手
ffmpeg -re -f lavfi -i testsrc=size=320x240:rate=15 \
-c:v libx264 -f flv rtmp://localhost:1935/live/test
过滤器
# 只看 RTMP 数据
tcp.port == 1935
# 只看握手阶段(前 3073 字节)
tcp.port == 1935 && tcp.len > 0 && tcp.stream == 0
# 查看特定连接的握手
tcp.stream eq 0
握手数据特征
Frame 1: C0+C1 (1537 bytes)
- Byte 0: Version = 0x03
- Byte 1-4: Timestamp
- Byte 5-8: Zero = 0x00000000
- Byte 9-1536: Random Data
Frame 2: S0+S1+S2 (3073 bytes)
- S0: Version = 0x03
- S1: Server timestamp + random
- S2: Echo of C1
Frame 3: C2 (1536 bytes)
- Echo of S1
注意事项
- C0+C1 合并发送:规范允许将 C0 和 C1 合并为一个 TCP 包(1537 字节),大多数实现都这样做
- 阻塞读取:S0+S1+S2 可能分多次 TCP 包到达,实现时需要循环读取到足够长度
- 超时处理:握手应在 5 秒内完成,否则应断开连接
- 复杂握手兼容性:如果收到复杂格式的 C1 但不支持,应回退到简单握手
- RTMPE 安全性:RTMPE 的 RC4 加密已不安全,生产环境应使用 RTMPS(TLS)
扩展阅读
上一章:01 - RTMP 协议概述 下一章:03 - 块流机制 — 了解 RTMP 的消息分块传输