写点什么

深入理解 rtmp(三) 之手把手实现握手协议

用户头像
轻口味
关注
发布于: 4 小时前
深入理解rtmp(三)之手把手实现握手协议

RTMP 是基于 TCP 协议的应用层协议,默认通信端口 1935.实现握手协议前先了解一下 rtmp 握手协议吧!!!


握手过程


要建立一个有效的 RTMP Connection 链接,首先要“握手”:客户端要向服务器发送 C0,C1,C2(按序)三个 chunk,服务器向客户端发送 S0,S1,S2(按序)三个 chunk,然后才能进行有效的信息传输。RTMP 协议本身并没有规定这 6 个 Message 的具体传输顺序,但 RTMP 协议的实现者需要保证这几点如下:


  1. 客户端要等收到 S1 之后才能发送 C2

  2. 客户端要等收到 S2 之后才能发送其他信息(控制信息和真实音视频等数据)

  3. 服务端要等到收到 C0 之后发送 S1

  4. 服务端必须等到收到 C1 之后才能发送 S2

  5. 服务端必须等到收到 C2 之后才能发送其他信息(控制信息和真实音视频等数据)


用图形可以表示为:


+-------------+                            +-------------+|   Client    |      TCP/IP Network        |     Server  |+-------------+             |              +-------------+       |                    |                     |Uninitialized               |                Uninitialized       |        C0          |                     |       |------------------->|           C0        |       |                    |-------------------->|       |        C1          |                     |       |------------------->|           S0        |       |                    |<--------------------|       |                    |           S1        |  Version sent              |<--------------------|       |        S0          |                     |       |<-------------------|                     |       |        S1          |                     |       |<-------------------|               Version sent       |                    |           C1        |       |                    |-------------------->|       |        C2          |                     |       |------------------->|           S2        |       |                    |<--------------------|    Ack sent                |                   Ack Sent       |        S2          |                     |       |<-------------------|                     |       |                    |           C2        |       |                    |-------------------->|Handshake Done              |               Handshake Done      |                     |                     |          Pictorial Representation of Handshake
复制代码


总结一下:


  • 握手开始于客户端发送 C0、C1 块。服务器收到 C0 或 C1 后发送 S0 和 S1。

  • 当客户端收齐 S0 和 S1 后,开始发送 C2。当服务器收齐 C0 和 C1 后,开始发送 S2。

  • 当客户端和服务器分别收到 S2 和 C2 后,握手完成。


注意事项: 在实际工程应用中,一般是客户端先将 C0, C1 块同时发出,服务器在收到 C1 之后同时将 S0, S1, S2 发给客户端。S2 的内容就是收到的 C1 块的内容。之后客户端收到 S1 块,并原样返回给服务器,简单握手完成。按照 RTMP 协议个要求,客户端需要校验 C1 块的内容和 S2 块的内容是否相同,相同的话才彻底完成握手过程,实际编写程序用一般都不去做校验。


RTMP 握手的这个过程就是完成了两件事:


  1. 校验客户端和服务器端 RTMP 协议版本号

  2. 发了一堆随机数据,校验网络状况。


握手包格式


简单握手


C0 和 S0:1 个字节,包含了 RTMP 版本, 当前 RTMP 协议的版本为 3


 0 1 2 3 4 5 6 7+-+-+-+-+-+-+-+-+|     version   |+-+-+-+-+-+-+-+-+ C0 and S0 bits
复制代码


C1 和 S1:4 字节时间戳,4 字节的 0,1528 字节的随机数


 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 bytes                        |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|                           random bytes                        ||                               (cont)                          ||                               ....                            |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                        C1 and S1 bits
复制代码


  • C1/S1 长度为 1536B。主要目的是确保握手的唯一性。

  • 格式为 time + zero + random

  • time 发送时间戳,长度 4 byte

  • zero 保留值 0,长度 4 byte

  • random 随机值,长度 1528 byte,保证此次握手的唯一性,确定握手的对象


C2 和 S2:4 字节时间戳,4 字节从对端读到的时间戳,1528 字节随机数


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)                       | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |                          time2 (4 bytes)                      | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |                          random echo                          | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |                          random echo                          | |                             (cont)                            | |                              ....                             | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                            C2 and S2 bits
复制代码


  • C2/S2 的长度也是 1536B。相当于就是 S1/C1 的响应值,对应 C1/S1 的 Copy 值,在于字段有点区别

  • time, C2/S2 发送的时间戳,长度 4 byte

  • time2, S1/C1 发送的时间戳,长度 4 byte

  • random,S1/C1 发送的随机数,长度为 1528B


携带上内容的流程图:


复杂握手


介绍复杂模式前,先介绍一个哈希签名算法,即 hmac-sha256 算法。复杂模式会使用它做一些签名运算和验证。简单来说,这个算法的输入为一个 key(长度可以为任意)和一个 input 字符串(长度可以为任意),经过 hmac-sha256 运算后得到一个 32 字节的签名串。


key 和 input 固定时,hmac-sha256 运算结果也是固定唯一的。相对于简单握手,复杂握手增加了严格的验证,主要是 random 字段上进行更细化的划分


1528Bytes 随机数的部分平均分成两部分,一部分 764Bytes 存储 public key(公共密钥),另一部分 764Bytes 存储 digest(密文,32 字节)。


c0


固定为 0x03


c1


格式如下:


| 4字节时间戳time | 4字节模式串 | 1528字节复杂二进制串 |
复制代码


time 字段参照简单模式下 time 的说明。


4 字节模式串, 使用的是[0x0C, 0x00, 0x0D, 0x0E]。


1528 字节复杂二进制串生成规则如下:步骤一,将 1528 字节复杂二进制串进行随机化处理。步骤二,在 1528 字节随机二进制串中写入 32 字节的 digest 签名。


digest 的位置先说明 digest 的位置如何确定。digest 的位置可以在前半部分,也可以在后半部分。


digest 在前半部分


当 digest 在前半部分时,digest 的位置信息(以下简称 offset)保存在前半部分的起始位置。


c1 格式展开如下:


| 4字节time | 4字节模式串 | 4字节offset | left[...] | 32字节digest | right[...] | 后半部分764字节 |
offset = (c1[8] + c1[9] + c1[10] + c1[11]) % 728 + 12
复制代码


几点说明


  • 计算出的 offset 是相对于整个 c1 的起始位置而言的。

  • 为什么要取余 728 呢,因为前半部分的 764 字节要减去 offset 字段的 4 字节,再减去 digest 的 32 字节。

  • 为什么要加 12 呢,是因为要跳过 4 字节 time+4 字节模式串+4 字节 offset。

  • offset 的取值范围为[12,740)。

  • 当 offset=12 时, left 部分就不存在,当 offset=739 时, right 部分就不存在。


digest 在后半部分


当 digest 在后半部分时,offset 保存在后半部分的起始位置。c1 格式展开如下:


| 4字节time | 4字节模式串 | 前半部分764字节 | 4字节offset | left[...] | 32字节digest | right[...] |
offset = (c1[8+764] + c1[8+764+1] + c1[8+764+2] + c1[8+764+3]) % 728 + 8 + 764 + 4
复制代码


几点说明:


  • 计算出的 offset 依赖是相对于 c1 的其实位置而言的。

  • 为什么要取余 728 呢,因为后半部分的 764 字节要减去 offset 字段的 4 字节,再减去 digest 的 32 字节。

  • 为什么加 8 加 764 加 4 呢,是因为要跳过 4 字节 time+4 字节模式串+前半部分 764 字节+4 字节 offset。

  • offset 的取值范围为[776,1504)。

  • 当 offset=776 时, left 部分就不存在,当 offset=1503 时, right 部分就不存在。


digest 如何生成


说完 digest 的位置,再说 digest 如何生成。


即将 c1 digest 左边部分拼接上 c1 digest 右边部分(如果右边部分存在的话)作为 hmac-sha256 的 input(整个大小是 1536-32),以下大小为 30 字节固定 key 作为 hmac-sha256 的 key,进过 hmac-sha256 计算得出 32 字节的 digest 填入 c1 中 digest 字段中。


'G', 'e', 'n', 'u', 'i', 'n', 'e', ' ', 'A', 'd', 'o', 'b', 'e', ' ','F', 'l', 'a', 's', 'h', ' ', 'P', 'l', 'a', 'y', 'e', 'r', ' ','0', '0', '1',
复制代码


服务端在收到 c1 后,首先通过 c1 中的模式串,初步判断是否为复杂模式,如果是复杂模式,则通过 c1 重新 digest,看计算得出的 digest 和 c1 中的包含的 digest 字段是否相同来确定握手是否为复杂模式。


注意,由于服务端无法直接得知客户端是将 digest 放在前半部分还是后半部分,所以服务端只能先验证其中一种,如果验证失败,再验证另外一种,如果都失败了,就考虑回退使用简单模式和客户端继续握手。


s0


固定为 0x03


s1


s1 的构造方法和 c1 相同。


只不过将模式串换成了 [0x0D, 0x0E, 0x0A, 0x0D]。


并且将 hmac-sha256 的 key 换成了如下 36 字节固定 key


'G', 'e', 'n', 'u', 'i', 'n', 'e', ' ', 'A', 'd', 'o', 'b', 'e', ' ','F', 'l', 'a', 's', 'h', ' ', 'M', 'e', 'd', 'i', 'a', ' ','S', 'e', 'r', 'v', 'e', 'r', ' ','0', '0', '1',
复制代码


s2


格式如下:


| 4字节时间戳time | 4字节time2 | 1528字节随机二进制串 |
复制代码


其中 time 和 time2 字段参考简单模式下 s2 的说明。


1528 字节随机二进制串中也需要填入 digest。


将 32 字节 digest 直接填入 s2 的尾部,也即没有设置相应的 offset ,digest 的计算方法是,使用 digest 的左边部分作为 hmac-sha256 的 input(大小是 1536-32), 使用 c1 中的 digest 作为 hmac-sha256 的 key ,通过 hmac-sha256 计算得出 digest。


c2


c2 的构造方法和 s2 相同。


只不过它是用 s2 中的 digest 作为 hmac-sha256 的 key。


握手实现


我们继续写代码,实现简单握手协议.协议实现相关的代码我们放到 protocol 文件夹下,我们先定义一个 rtmp_stack.hpp 文件,用来存放我们后面封装的 rtmp 相关数据结构,rtmp_stack.hpp 中,增加 HandshakeBytes 类来存放握手相关的数据


// store the handshake bytes,class HandshakeBytes{public:    // For RTMP proxy, the real IP.    uint32_t proxy_real_ip;    // [1+1536]    char* c0c1;    // [1+1536+1536]    char* s0s1s2;    // [1536]    char* c2;public:    HandshakeBytes();    virtual ~HandshakeBytes();public:    virtual void dispose();public:    virtual error_t read_c0c1(SimpleSocketStream* io);    virtual error_t read_s0s1s2(SimpleSocketStream* io);    virtual error_t read_c2(SimpleSocketStream* io);    virtual error_t create_c0c1();    virtual error_t create_s0s1s2(const char* c1 = NULL);    virtual error_t create_c2();};
复制代码


我们作为客户端只实现 c0,c1,c2 的生成发送和 s0,s1,s2 的读取即可:


HandshakeBytes::HandshakeBytes(){    c0c1 = s0s1s2 = c2 = NULL;    proxy_real_ip = 0;}
HandshakeBytes::~HandshakeBytes(){ dispose();}
void HandshakeBytes::dispose(){ freepa(c0c1); freepa(s0s1s2); freepa(c2);}

error_t HandshakeBytes::read_s0s1s2(SimpleSocketStream* io){ error_t err = srs_success; if (s0s1s2) { return err; } ssize_t nsize; s0s1s2 = new char[3073]; if ((err = io->read_fully(s0s1s2, 3073, &nsize)) != srs_success) { return error_wrap(err, "read s0s1s2"); } return err;}
error_t HandshakeBytes::create_c0c1(){ error_t err = srs_success; if (c0c1) { return err; } c0c1 = new char[1537]; random_generate(c0c1, 1537); // plain text required. SBuffer stream(c0c1, 9); stream.write_1bytes(0x03); stream.write_4bytes((int32_t)::time(NULL)); stream.write_4bytes(0x00); return err;}
error_t HandshakeBytes::create_c2(){ error_t err = srs_success; if (c2) { return err; } c2 = new char[1536]; srs_random_generate(c2, 1536); // time SBuffer stream(c2, 8); stream.write_4bytes((int32_t)::time(NULL)); // c2 time2 copy from s1 if (s0s1s2) { stream.write_bytes(s0s1s2 + 1, 4); } return err;}
复制代码


random_generate 实现:


//rand()随机数生成void random_generate(char* bytes, int size){    static bool _random_initialized = false;    if (!_random_initialized) {        srand(0);        _random_initialized = true;    }        for (int i = 0; i < size; i++) {        // the common value in [0x0f, 0xf0]        bytes[i] = 0x0f + (rand() % (256 - 0x0f - 0x0f));    }}
复制代码


最基本的客户端握手协议就实现了,服务端的实现也类似.


接下来我们把握手封装到一个类里面:


//rtmp_handshake.hppclass SimpleHandshake{public:    SimpleHandshake();    virtual ~SimpleHandshake();public:    // Simple handshake.    virtual srs_error_t handshake_with_client(HandshakeBytes* hs_bytes, SimpleSocketStream* io);    virtual srs_error_t handshake_with_server(HandshakeBytes* hs_bytes, SimpleSocketStream* io);};
复制代码


实现(同样的我们先只实现客户端连接服务端):


SimpleHandshake::SimpleHandshake(){}
SimpleHandshake::~SimpleHandshake(){}
error_t SimpleHandshake::handshake_with_server(HandshakeBytes* hs_bytes, SimpleSocketStream* io){ error_t err = srs_success; ssize_t nsize; // simple handshake if ((err = hs_bytes->create_c0c1()) != success) { return error_wrap(err, "create c0c1"); } if ((err = io->write(hs_bytes->c0c1, 1537, &nsize)) != srs_success) { return error_wrap(err, "write c0c1"); } if ((err = hs_bytes->read_s0s1s2(io)) != srs_success) { return error_wrap(err, "read s0s1s2"); } // plain text required. if (hs_bytes->s0s1s2[0] != 0x03) { return error_new(ERROR_RTMP_HANDSHAKE, "handshake failed, plain text required, version=%X", (uint8_t)hs_bytes->s0s1s2[0]); } if ((err = hs_bytes->create_c2()) != success) { return srs_error_wrap(err, "create c2"); }
memcpy(hs_bytes->c2, hs_bytes->s0s1s2 + 1, 1536); if ((err = io->write(hs_bytes->c2, 1536, &nsize)) != success) { return error_wrap(err, "write c2"); } std::cout << "simple handshake success." << std::endl; return err;}
复制代码


接口封装及测试


我们在实现 rtmpsdk.hpp 对外暴露接口前,先封装一个上下文环境的 Context:


struct Context{    // The original RTMP url.    std::string url;        // Parse from url.    std::string tcUrl;    std::string host;    std::string vhost;    std::string app;    std::string stream;    std::string param;        // Parse ip:port from host.    std::string ip;    int port;
SimpleSocketStream* skt; HandshakeBytes* hhb; // user set timeout, in ms. int64_t stimeout; int64_t rtimeout; Context() : port(0) { skt = NULL; } virtual ~Context() { srs_freep(skt); }};
复制代码


下面我们按前文步骤深入理解 rtmp(二)之 C++脚手架搭建封装接口步骤在 rtmpsdk.hhp 统一封装统一对外暴露接口


1.实现 rtmp_create


rtmp_t rtmp_create(const char* url){    int ret = ERROR_SUCCESS;        Context* context = new Context();    context->url = url;        // create socket    freep(context->skt);    context->skt = new SimpleSocketStream();        if ((ret = context->skt->create_socket(context->url)) != ERROR_SUCCESS) {//调用SimpleSocketStream的create_socket方法        printf("Create socket failed, ret=%d", ret);                // free the context and return NULL        freep(context);        return NULL;    }        return context;}
复制代码


2.封装 rtmp_handshake


int rtmp_handshake(rtmp_t rtmp){    int ret = ERROR_SUCCESS;        if ((ret = rtmp_dns_resolve(rtmp)) != ERROR_SUCCESS) {        return ret;    }        if ((ret = rtmp_connect_server(rtmp)) != ERROR_SUCCESS) {        return ret;    }        if ((ret = rtmp_do_simple_handshake(rtmp)) != ERROR_SUCCESS) {        return ret;    }        return ret;}
复制代码


握手我们分三步执行:


  1. dns 解析

  2. 连接服务

  3. 进行握手


rtmp_dns_resolve


rtmp_dns_resolve 我们又拆分成了解析 uri 和解析 host:


int rtmp_dns_resolve(rtmp_t rtmp){    int ret = ERROR_SUCCESS;        assert(rtmp != NULL);    Context* context = (Context*)rtmp;        // parse uri    if ((ret = librtmp_context_parse_uri(context)) != ERROR_SUCCESS) {        return ret;    }    // resolve host    if ((ret = librtmp_context_resolve_host(context)) != ERROR_SUCCESS) {        return ret;    }        return ret;}
复制代码


解析 uri:


int librtmp_context_parse_uri(Context* context){    int ret = ERROR_SUCCESS;        std::string schema;
//1.通过最后边的斜线"/"将url拆分成tcUrl和stream两部分 parse_rtmp_url(context->url, context->tcUrl, context->stream); // when connect, we only need to parse the tcUrl //2.将tcUrl拆分成scheme, host, 虚拟host,app,stream和端口 srs_discovery_tc_url(context->tcUrl, schema, context->host, context->vhost, context->app, context->stream, context->port, context->param); return ret;}
复制代码


解析 host:


int librtmp_context_resolve_host(Context* context){    int ret = ERROR_SUCCESS;        // connect to server:port    int family = AF_UNSPEC;    进行dns解析,将host解析成ip    context->ip = dns_resolve(context->host, family);    if (context->ip.empty()) {        return ERROR_SYSTEM_DNS_RESOLVE;    }        return ret;}
复制代码


dns 解析:


string dns_resolve(string host, int& family){    addrinfo hints;    memset(&hints, 0, sizeof(hints));    hints.ai_family = family;        addrinfo* r = NULL;        if(getaddrinfo(host.c_str(), NULL, &hints, &r)) {        return "";    }        char shost[64];    memset(shost, 0, sizeof(shost));    if (getnameinfo(r->ai_addr, r->ai_addrlen, shost, sizeof(shost), NULL, 0, NI_NUMERICHOST)) {        return "";    }
family = r->ai_family; return string(shost);}
复制代码


rtmp_connect_server


int librtmp_context_connect(Context* context){    int ret = ERROR_SUCCESS;        srs_assert(context->skt);        std::string ip = context->ip;    if ((ret = context->skt->connect(ip.c_str(), context->port)) != ERROR_SUCCESS) {        return ret;    }        return ret;}
int rtmp_connect_server(rtmp_t rtmp){ int ret = ERROR_SUCCESS; assert(rtmp != NULL); Context* context = (Context*)rtmp; // set timeout if user not set. if (context->stimeout == SRS_UTIME_NO_TIMEOUT) { context->stimeout = SRS_SOCKET_DEFAULT_TMMS; context->skt->set_send_timeout(context->stimeout * SRS_UTIME_MILLISECONDS); } if (context->rtimeout == SRS_UTIME_NO_TIMEOUT) { context->rtimeout = SRS_SOCKET_DEFAULT_TMMS; context->skt->set_recv_timeout(context->rtimeout * SRS_UTIME_MILLISECONDS); } if ((ret = librtmp_context_connect(context)) != ERROR_SUCCESS) { return ret; } return ret;}
复制代码


设置完超时等参数后,调用 SimpleSocketStream 的 connect 连接服务器


rtmp_do_simple_handshake


调用我们上面封装的 handshake_with_server 与 rtmp server 进行握手


int rtmp_do_simple_handshake(rtmp_t rtmp){    int ret = ERROR_SUCCESS;    srs_error_t err = srs_success;        srs_assert(rtmp != NULL);    Context* context = (Context*)rtmp;        srs_assert(context->skt != NULL);        // simple handshake    srs_freep(context->hhb);    context->hhb = new HandshakeBytes();        srs_assert(context->hhb);        SimpleHandshake simple_hs;    if ((err = simple_hs.handshake_with_server(context->hhb, context->skt)) != srs_success) {        return -1;    }        context->hhb->dispose();        cout << "handshake success..." << endl;        return ret;}
复制代码


3.main 中测试


改造我们上一篇的 main 方法:


int main(int argc,char* argv[]){    std::cout << "Hello rtmp server!" << std::endl;        rtmp_t client = rtmp_create("rtmp://127.0.0.1:1935/live/livestream");    int ret = rtmp_handshake(client);    return 0;    }
复制代码


最终日志输出:


$ ./rtmpsdk Hello rtmp server!simple handshake success.handshake success...
复制代码


srs 服务端日志输出:


[2020-01-21 11:06:17.237][Trace][7503][531] RTMP client ip=172.17.0.1, fd=10[2020-01-21 11:06:17.240][Trace][7503][531] simple handshake success.[2020-01-21 11:06:17.240][Warn][7503][531][104] client disconnect peer. ret=1007
复制代码


发布于: 4 小时前阅读数: 4
用户头像

轻口味

关注

100位签约计划优质创作者 2017.10.17 加入

Android音视频、AI相关领域从业者

评论

发布
暂无评论
深入理解rtmp(三)之手把手实现握手协议