一、RTMP 握手
客户端 服务器
│ │
│──── C0 (版本=3) ──────────────→│
│──── C1 (1536 字节随机数据) ────→│
│←─── S0 (版本=3) ──────────────│
│←─── S1 (1536 字节随机数据) ────│
│←─── S2 (C1 的回应) ───────────│
│──── C2 (S1 的回应) ───────────→│
│ │
│ 握手完成,建立连接 │
│ │
│──── connect("live") ──────────→│
│←─── _result (成功) ───────────│
│──── createStream ─────────────→│
│←─── _result (stream_id=1) ────│
│──── publish("live") ──────────→│
│←─── onStatus("正在发布") ──────│
│ │
│ 开始推送音视频数据 │
│──── Audio/Video Tag ──────────→│
│──── Audio/Video Tag ──────────→│
│ ... │
二、RTMP Chunk 分片
RTMP 的核心设计:把大的消息切成小的 chunk,交错发送,避免大消息阻塞后面的小消息。
消息(Message):
├── Header (1-18 字节) │ 消息体(可变大小,最大可达 128MB)
消息被切成 Chunk(每个 128 字节):
Chunk 1: [基本头][消息头][数据 128 字节]
Chunk 2: [基本头 ][数据 128 字节] ← 后面的 chunk 可以省略消息头
Chunk 3: [基本头 ][数据 剩余字节]
为什么需要分片?
假设你同时推送:
视频包:500KB 的 H.264 关键帧
音频包:200 字节的 AAC 帧
如果不分片:
[完整视频包 500KB] → 发完才轮到 [音频包 200 字节]
客户端听到 500KB 的传输时间里没有声音 → 卡顿
分片后:
[视频 ch1][视频 ch2][音频 ch1][视频 ch3][视频 ch4][音频 ch2]...
音频每 128 字节穿插在视频中 → 客户端持续收到声音
三、RTMP URL 的结构
rtmp://server-address:port/app/streamName
例子:
rtmp://live.twitch.tv/app/live_123456
↑协议 ↑服务器 ↑应用 ↑流名称
当你推流:
push("rtmp://server/live/stream1")
服务器收到:publish "stream1" in app "live"
当你拉流:
play("rtmp://server/live/stream1")
服务器找到:app "live" 中名为 "stream1" 的流,转发给你
四、不用第三方库的 RTMP 握手(极简版)
真实的 RTMP 握手复杂(包含 digest),这里展示握手的第一步——验证版本号:
#include <cstdint>
#include <cstring>
#include <sys/socket.h>
#include <netdb.h>
// 极简 RTMP 握手(只做连接建立 + 版本交换)
// 真实的 RTMP 还需要做 digest 验证,这里只展示流程骨架
bool rtmp_handshake(int sockfd) {
// C0: 客户端声明 RTMP 版本(目前版本 = 3)
uint8_t c0 = 3;
if (send(sockfd, &c0, 1, 0) != 1) return false;
// C1: 1536 字节随机数据(实际包含 time + version + random)
uint8_t c1[1536] = {};
uint32_t ts = htonl(0); // 时间戳
memcpy(c1, &ts, 4); // 前 4 字节 = 时间戳
memcpy(c1 + 4, &ts, 4); // 接着 4 字节 = 版本(0 = 握手类型)
// 剩余 1528 字节随机填充
for (int i = 8; i < 1536; i++) c1[i] = rand() & 0xFF;
if (send(sockfd, c1, 1536, 0) != 1536) return false;
// 读取 S0: 服务器版本
uint8_t s0;
if (recv(sockfd, &s0, 1, 0) != 1) return false;
if (s0 != 3) return false; // 版本不匹配
// 读取 S1 + S2
uint8_t buf[3072];
if (recv(sockfd, buf, 3072, MSG_WAITALL) != 3072) return false;
// C2: 回显 S1 的数据
if (send(sockfd, buf, 1536, 0) != 1536) return false;
return true; // 握手完成
}