1.概述
学习 RTMP 握手逻辑前,需明确两个核心问题:
- rtmp协议连接流程阶段
- rtmp简单握手和复杂握手区别
具体可以学习往期博客:
2.rtmp握手源码分析
2.1 握手入口
根据SRS流媒体服务器(4)可知,服务启动SrsServer → 初始化 SrsBufferListener → 每个 SrsBufferListener 管理一个 SrsTcpListener → SrsTcpListener 通过协程循环接受新连接 → on_tcp_client 回调到上层SrsServer
→ SrsServer::accept_client 接收新 TCP 连接 → 创建SrsRtmpConn
连接对象→SrsRtmpConn::do_cycle()协程驱动cycle()
主循环→完成握手、应用连接、媒体流传输→连接断开清理。
srs_error_t SrsRtmpConn::do_cycle()
{
srs_error_t err = srs_success;
// 打印RTMP客户端的IP地址和端口
srs_trace("RTMP client ip=%s:%d, fd=%d", ip.c_str(), port, srs_netfd_fileno(stfd));
// 设置RTMP的接收和发送超时时间
rtmp->set_recv_timeout(SRS_CONSTS_RTMP_TIMEOUT);
rtmp->set_send_timeout(SRS_CONSTS_RTMP_TIMEOUT);
// 执行RTMP握手
if ((err = rtmp->handshake()) != srs_success) {
return srs_error_wrap(err, "rtmp handshake");
}
// 获取RTMP代理的真实客户端IP地址
uint32_t rip = rtmp->proxy_real_ip();
// 获取请求信息
SrsRequest* req = info->req;
if ((err = rtmp->connect_app(req)) != srs_success) {
return srs_error_wrap(err, "rtmp connect tcUrl");
}
// 执行服务循环
if ((err = service_cycle()) != srs_success) {
err = srs_error_wrap(err, "service cycle");
}
srs_error_t r0 = srs_success;
if ((r0 = on_disconnect()) != srs_success) {
err = srs_error_wrap(err, "on disconnect %s", srs_error_desc(r0).c_str());
srs_freep(r0);
}
// 如果客户端被重定向到其他服务器,则已经记录了该事件
// If client is redirect to other servers, we already logged the event.
if (srs_error_code(err) == ERROR_CONTROL_REDIRECT) {
srs_error_reset(err);
}
return err;
}
2.2 简单和复杂握手
主要是优先尝试复杂握手,随后解析客户端发来的C0C1(并解析是否是代理,Schema1模式等)并返回S0S1S2给客户端,最后再接收C2。
Schema0是一种特殊的握手验证方式,主要为了兼容Adobe Flash Player。在 Schema0 中,Digest 固定位于 C1/S1 的第 8-71 字节(共 64 字节),剩余的 1464 字节为随机数据。这种固定位置的设计简化了验证逻辑,但安全性较低。
Schema1是更安全的握手验证方式,主要用于现代客户端(如 OBS、FFmpeg)Schema1 中,Digest 的位置由 C1 的前 4 字节(时间戳)计算得出,这种方式使得 Digest 位置不固定,提高了安全性。公式为:
digest_offset = (timestamp[0] + timestamp[1] + timestamp[2] + timestamp[3]) % 728 + 12
2.2.1 复杂握手代码示例
SrsRtmpServer::handshake() 复杂握手或简单握手
SrsComplexHandshake::handshake_with_client 读取客户端发送的c0c1数据,解析c1,
生成并发送s0s1s2数据,然后接收客户端发送的c2数据。
c1s1::parse(char* _c1s1, int size, srs_schema_type schema) 根据握手消息的schema类型,解析c1s1握手消息
c1s1_strategy_schema1::parse(char* _c1s1, int size) Schema1解密
/**
* @brief 与客户端进行 RTMP 握手
*
* 此函数用于与 RTMP 客户端进行握手,以建立连接。首先尝试复杂握手,如果失败且错误码为 ERROR_RTMP_TRY_SIMPLE_HS,则尝试简单握手。
*
* @return srs_error_t 握手结果,成功返回 srs_success,失败返回错误码并附加中文注释。
*/
srs_error_t SrsRtmpServer::handshake()
{
srs_error_t err = srs_success;
srs_assert(hs_bytes);
SrsComplexHandshake complex_hs;
// 尝试与客户端进行复杂握手,如果握手失败 则尝试简单握手
//SrsRtmpConn(xxx) -> skt = new SrsTcpConnection(c); -> io = skt;
if ((err = complex_hs.handshake_with_client(hs_bytes, io)) != srs_success) {
if (srs_error_code(err) == ERROR_RTMP_TRY_SIMPLE_HS) {
srs_freep(err);
SrsSimpleHandshake simple_hs;
if ((err = simple_hs.handshake_with_client(hs_bytes, io)) != srs_success) {
// 如果简单握手失败,返回错误并添加中文注释
return srs_error_wrap(err, "simple handshake");
}
} else {
// 如果复杂握手失败且错误码不是 ERROR_RTMP_TRY_SIMPLE_HS,返回错误并添加中文注释
return srs_error_wrap(err, "complex handshake");
}
}
hs_bytes->dispose(); // 释放 hs_bytes 占用的资源
return err; // 返回错误码
}
/**
* @brief 与客户端进行复杂握手
*
* 该函数用于与客户端进行复杂握手协议。握手过程包括读取客户端发送的c0c1数据,解析c1,
* 生成并发送s0s1s2数据,然后接收客户端发送的c2数据。
*
* @param hs_bytes 存储握手字节数据的对象指针
* @param io 读写接口指针
*
* @return 错误码,成功时返回srs_success
*/
srs_error_t SrsComplexHandshake::handshake_with_client(SrsHandshakeBytes* hs_bytes, ISrsProtocolReadWriter* io)
{
srs_error_t err = srs_success;
ssize_t nsize;
// 读取客户端发送的c0c1数据
if ((err = hs_bytes->read_c0c1(io)) != srs_success) {
return srs_error_wrap(err, "read c0c1");
}
// decode c1
c1s1 c1;
// 尝试使用schema0进行解析
// @remark, 使用schema0是为了让Flash播放器满意
if ((err = c1.parse(hs_bytes->c0c1 + 1, 1536, srs_schema0)) != srs_success) {
return srs_error_wrap(err, "parse c1, schema=%d", srs_schema0);
}
// 尝试使用schema1进行解析
if ((err = c1.c1_validate_digest(is_valid)) != srs_success || !is_valid) {
}
// encode s1
c1s1 s1;
if ((err = s1.s1_create(&c1)) != srs_success) {
return srs_error_wrap(err, "create s1 from c1");
}
// 验证s1
if ((err = s1.s1_validate_digest(is_valid)) != srs_success || !is_valid) {
srs_freep(err);
return srs_error_new(ERROR_RTMP_TRY_SIMPLE_HS, "verify s1 failed, try simple handshake");
}
c2s2 s2;
if ((err = s2.s2_create(&c1)) != srs_success) {
return srs_error_wrap(err, "create s2 from c1");
}
// 验证s2
if ((err = s2.s2_validate(&c1, is_valid)) != srs_success || !is_valid) {
srs_freep(err);
return srs_error_new(ERROR_RTMP_TRY_SIMPLE_HS, "verify s2 failed, try simple handshake");
}
// 发送s0s1s2数据
if ((err = hs_bytes->create_s0s1s2()) != srs_success) {
return srs_error_wrap(err, "create s0s1s2");
}
if ((err = s1.dump(hs_bytes->s0s1s2 + 1, 1536)) != srs_success) {
return srs_error_wrap(err, "dump s1");
}
if ((err = s2.dump(hs_bytes->s0s1s2 + 1537, 1536)) != srs_success) {
return srs_error_wrap(err, "dump s2");
}
if ((err = io->write(hs_bytes->s0s1s2, 3073, &nsize)) != srs_success) {
return srs_error_wrap(err, "write s0s1s2");
}
// 接收客户端发送的c2数据
if ((err = hs_bytes->read_c2(io)) != srs_success) {
return srs_error_wrap(err, "read c2");
}
c2s2 c2;
if ((err = c2.parse(hs_bytes->c2, 1536)) != srs_success) {
return srs_error_wrap(err, "parse c2");
}
// verify c2
// 不验证c2,因为ffmpeg会失败
// Flash播放器可以正常工作
srs_trace("complex handshake success");
return err;
}
/**
* @brief 读取RTMP握手过程中的C0C1包
*
* 该函数负责从给定的协议读取器中读取C0C1包数据,并进行rtmp代理处理。
*/
srs_error_t SrsHandshakeBytes::read_c0c1(ISrsProtocolReader* io)
{
c0c1 = new char[1537];
if ((err = io->read_fully(c0c1, 1537, &nsize)) != srs_success) {
return srs_error_wrap(err, "read c0c1");
}
// Whether RTMP proxy, @see https://github.com/ossrs/go-oryx/wiki/RtmpProxy
//如果是一个通过 RTMP 代理传输的数据包。
if (uint8_t(c0c1[0]) == 0xF3) {
//表示代理数据头部之后额外数据的长度。
uint16_t nn = uint16_t(c0c1[1])<<8 | uint16_t(c0c1[2]);
ssize_t nn_consumed = 3 + nn;
// 4B client real IP.
if (nn >= 4) {
//提取出客户端的真实 IP 地址。
proxy_real_ip = uint32_t(c0c1[3])<<24 | uint32_t(c0c1[4])<<16 | uint32_t(c0c1[5])<<8 | uint32_t(c0c1[6]);
nn -= 4;
}
// 移除代理头部,确保后续处理时只考虑原始的 RTMP 数据。
memmove(c0c1, c0c1 + nn_consumed, 1537 - nn_consumed);
//从 io 中读取被移除部分的数据,填补到 c0c1 缓冲区的末尾,确保总长度仍为 1537 字节。
if ((err = io->read_fully(c0c1 + 1537 - nn_consumed, nn_consumed, &nsize)) != srs_success) {
return srs_error_wrap(err, "read c0c1");
}
}
return err;
}
/**
* @brief 解析c1s1握手消息
*
* 该函数用于解析c1s1握手消息,并根据指定的schema类型选择相应的解析策略。
*
* @param _c1s1 指向握手消息的指针
* @param size 握手消息的大小,应为1536字节
* @param schema 握手消息的schema类型,应为srs_schema0或srs_schema1
*
* @return 如果解析成功,返回srs_success;否则返回相应的错误码和错误信息
*/
srs_error_t c1s1::parse(char* _c1s1, int size, srs_schema_type schema)
{
srs_assert(size == 1536);
// 检查schema类型是否有效
if (schema != srs_schema0 && schema != srs_schema1) {
return srs_error_new(ERROR_RTMP_CH_SCHEMA, "parse c1 failed. invalid schema=%d", schema);
}
// 创建SrsBuffer对象,用于读取数据
SrsBuffer stream(_c1s1, size);
// 读取时间戳
time = stream.read_4bytes();
// 读取版本号
version = stream.read_4bytes(); // client c1 version
// 释放旧的payload指针
srs_freep(payload);
// 根据schema类型选择不同的解析策略
if (schema == srs_schema0) {
//schema0 是一种特定的解析方式,它针对旧版 Flash 播放器的特性进行了优化。
payload = new c1s1_strategy_schema0();
} else {
//Schema1是更安全的握手验证方式,主要用于现代客户端(如 OBS、FFmpeg)
payload = new c1s1_strategy_schema1();
}
// 复杂握手解析明文和密文 传入原始数据和解析后的数据大小
return payload->parse(_c1s1, size);
}
/**
* @brief 解析c1s1策略模式schema1
*
* 该函数用于解析c1s1策略模式schema1的数据结构。
*
* @param _c1s1 输入的c1s1数据指针
* @param size 输入数据的大小,必须为1536字节
*
* @return srs_error_t 类型的错误码。成功时返回 srs_success,失败时返回相应的错误码。
*/
srs_error_t c1s1_strategy_schema1::parse(char* _c1s1, int size)
{
srs_error_t err = srs_success;
srs_assert(size == 1536);
if (true) {
SrsBuffer stream(_c1s1 + 8, 764);
//密文
if ((err = digest.parse(&stream)) != srs_success) {
return srs_error_wrap(err, "parse c1 digest");
}
}
if (true) {
SrsBuffer stream(_c1s1 + 8 + 764, 764);
//明文
if ((err = key.parse(&stream)) != srs_success) {
return srs_error_wrap(err, "parse c1 key");
}
}
return err;
}
2.2.2 简单握手代码示例
简单握手中C1和S1从第9个字节开始都是随机数。S2是C1的复制。C2是S1的复制。S0是空包,S012回复包组成是参考C1和S2独立数据包。
/**
* @brief 与客户端进行简单握手
*
* 该函数用于与RTMP客户端进行简单握手。
*
* @param hs_bytes 握手字节数据
* @param io 读写接口
*
* @return 返回握手结果的状态码,如果成功则返回srs_success,否则返回相应的错误状态码。
*/
srs_error_t SrsSimpleHandshake::handshake_with_client(SrsHandshakeBytes* hs_bytes, ISrsProtocolReadWriter* io)
{
srs_error_t err = srs_success;
ssize_t nsize;
// 读取客户端的C0C1
if ((err = hs_bytes->read_c0c1(io)) != srs_success) {
return srs_error_wrap(err, "read c0c1");
}
// 检查版本号,
if (hs_bytes->c0c1[0] != 0x03) {
return srs_error_new(ERROR_RTMP_PLAIN_REQUIRED, "only support rtmp plain text, version=%X", (uint8_t)hs_bytes->c0c1[0]);
}
// 创建S0S1S2
if ((err = hs_bytes->create_s0s1s2(hs_bytes->c0c1 + 1)) != srs_success) {
return srs_error_wrap(err, "create s0s1s2");
}
// 向客户端发送S0S1S2
if ((err = io->write(hs_bytes->s0s1s2, 3073, &nsize)) != srs_success) {
return srs_error_wrap(err, "write s0s1s2");
}
// 读取客户端的C2
if ((err = hs_bytes->read_c2(io)) != srs_success) {
return srs_error_wrap(err, "read c2");
}
// 打印握手成功日志
srs_trace("simple handshake success.");
return err;
}
/**
* @brief 创建S0S1S2握手字节
*
* 该函数创建一个长度为3073字节的握手字节数组,并将其赋值给成员变量s0s1s2。
*
* @param c1 用于生成S2部分的输入字符串
* @return srs_error_t 成功时返回srs_success,失败时返回相应的错误码
*/
srs_error_t SrsHandshakeBytes::create_s0s1s2(const char* c1)
{
srs_error_t err = srs_success;
// 如果s0s1s2已经存在,则直接返回成功
if (s0s1s2) {
return err;
}
// 为s0s1s2分配内存
s0s1s2 = new char[3073];
srs_random_generate(s0s1s2, 3073);
// 创建一个缓冲区,用于写入s0s1s2的前9个字节
// plain text required.
SrsBuffer stream(s0s1s2, 9);
// 向缓冲区写入第一个字节
stream.write_1bytes(0x03);
// 向缓冲区写入当前时间戳(4个字节)
stream.write_4bytes((int32_t)::time(NULL));
// 如果c0c1存在,则将c0c1的后4个字节写入缓冲区
// s1 time2 copy from c1
if (c0c1) {
stream.write_bytes(c0c1 + 1, 4);
}
// 如果c1存在,则将c1复制到s0s1s2的1537到3072字节位置
// if c1 specified, copy c1 to s2.
// @see: https://github.com/ossrs/srs/issues/46
if (c1) {
memcpy(s0s1s2 + 1537, c1, 1536);
}
return err;
}
学习资料分享