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

发布于:2025-05-17 ⋅ 阅读:(21) ⋅ 点赞:(0)

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 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;
}

学习资料分享

0voice · GitHub


网站公告

今日签到

点亮在社区的每一天
去签到