强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

RTMP 协议精讲 / 02 - 握手过程

握手过程(Handshake)

2.1 握手概述

RTMP 连接建立的第一步是 握手(Handshake)。与 TCP 三次握手不同,RTMP 握手是在 TCP 连接之上进行的 应用层协商,目的是:

  1. 验证协议版本兼容性
  2. 同步时间戳
  3. 交换随机数据(用于加密握手时生成密钥)
  4. 建立通信信任关系
客户端 (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 + randomtime + 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当前标准所有主流实现使用此版本
6FP9 handshakeFlash 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

注意事项

  1. C0+C1 合并发送:规范允许将 C0 和 C1 合并为一个 TCP 包(1537 字节),大多数实现都这样做
  2. 阻塞读取:S0+S1+S2 可能分多次 TCP 包到达,实现时需要循环读取到足够长度
  3. 超时处理:握手应在 5 秒内完成,否则应断开连接
  4. 复杂握手兼容性:如果收到复杂格式的 C1 但不支持,应回退到简单握手
  5. RTMPE 安全性:RTMPE 的 RC4 加密已不安全,生产环境应使用 RTMPS(TLS)

扩展阅读


上一章01 - RTMP 协议概述 下一章03 - 块流机制 — 了解 RTMP 的消息分块传输