動態

詳情 返回 返回

深入理解rtmp(三)之手把手實現握手協議 - 動態 詳情

深入理解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

攜帶上內容的流程圖:
image.png

複雜握手

介紹複雜模式前,先介紹一個哈希簽名算法,即hmac-sha256算法。複雜模式會使用它做一些簽名運算和驗證。
簡單來説,這個算法的輸入為一個key(長度可以為任意)和一個input字符串(長度可以為任意),經過hmac-sha256運算後得到一個32字節的簽名串。

key和input固定時,hmac-sha256運算結果也是固定唯一的。
相對於簡單握手,複雜握手增加了嚴格的驗證,主要是 random 字段上進行更細化的劃分

1528Bytes隨機數的部分平均分成兩部分,一部分764Bytes存儲public key(公共密鑰),另一部分764Bytes存儲digest(密文,32字節)。
image.png

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.hpp
class 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
user avatar uwatechnologies 頭像 niewj 頭像 tangqingfeng 頭像 abelethan 頭像 jingmingdewudongmian_dscnyw 頭像 yuezhang_5e5e7da0beeea 頭像 iex365 頭像 mrbone11 頭像 duwenlong 頭像 saltymilk 頭像 Reimual 頭像 lradian 頭像
點贊 12 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.