当前位置: 首页 > news >正文

SRS流媒体服务器(5)源码分析之RTMP握手

1.概述 

学习 RTMP 握手逻辑前,需明确两个核心问题:

  1. rtmp协议连接流程阶段
  2. rtmp简单握手和复杂握手区别

具体可以学习往期博客:

RTMP协议分析_rtmp与264的关系-CSDN博客

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 c1c1s1 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 s1c1s1 s1;if ((err = s1.s1_create(&c1)) != srs_success) {return srs_error_wrap(err, "create s1 from c1");}// 验证s1if ((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");}// 验证s2if ((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;// 读取客户端的C0C1if ((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]);}// 创建S0S1S2if ((err = hs_bytes->create_s0s1s2(hs_bytes->c0c1 + 1)) != srs_success) {return srs_error_wrap(err, "create s0s1s2");}// 向客户端发送S0S1S2if ((err = io->write(hs_bytes->s0s1s2, 3073, &nsize)) != srs_success) {return srs_error_wrap(err, "write s0s1s2");}// 读取客户端的C2if ((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 c1if (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/46if (c1) {memcpy(s0s1s2 + 1537, c1, 1536);}return err;
}

学习资料分享

0voice · GitHub

相关文章:

  • 关于 TCP 端口 445 的用途以及如何在 Windows 10 或 11 上禁用它
  • 课设:基于swin_transformer的植物中草药分类识别系统(包含数据集+UI界面+系统代码)
  • 基于51单片机和8X8点阵屏、矩阵按键的记忆类小游戏
  • Windows系统功能管控指南 | 一键隐藏关机键/禁用任务管理器
  • 二层交换机、三层交换机与路由器三者的详细对比
  • 一文讲透面向对象编程OOP特点及应用场景
  • 高压单端探头共模干扰问题分析及应对措施
  • java -jar命令运行 jar包时如何运行外部依赖jar包
  • 物联网中的WiFi模式解析:AP、STA与混合模式
  • 电平匹配电路
  • Flink运维要点
  • Python字符串常用内置函数详解
  • 车道线检测----Lane-ATT
  • 在vue3中使用Cesium的保姆教程
  • C# NX二次开发-实体离散成点
  • 5G-A和未来6G技术下的操作系统与移动设备变革:云端化与轻量化的发展趋势
  • Qwen3技术报告
  • 【Opencv】canny边缘检测提取中心坐标
  • 利用 Amazon Bedrock Data Automation(BDA)对视频数据进行自动化处理与检索
  • 用PHP轻松调用RESTful API:详解请求与响应
  • 湖南4个县市区被确定为野生蘑菇中毒高风险区:中毒尚无特效解毒药
  • 李伟任山东省委常委、省纪委书记
  • 美官方将使用华为芯片视作违反美出口管制行为,外交部回应
  • 俄乌官员即将在土耳其会谈,外交部:支持俄乌开启直接对话
  • 四川甘孜炉霍县觉日寺管委会主任呷玛降泽被查
  • 远洋集团:今年前4个月销售80.9亿元,同比增加13.62%