rtmp協議詳解 (二)Chunking 組塊

語言: CN / TW / HK

上文我們經歷了rtmp的握手環節,那麼接下來我們就可以執行rtmp操作了,也就是本文的主體(訊息Message)

rtmp handshark握手

名詞解釋

    Payload (有效載荷):包含於一個數據包中的資料,例如音訊取樣或者壓縮的影片資料。payload 的格式和解釋,超出了本文件的範圍。
    Packet (資料包):一個數據包由一個固定頭和有效載荷資料構成。一些個底層協議可能會要求對資料包定義封裝。
    Port (埠):"傳輸協議用以區分開指定一臺主機的不同目的地的一個抽象。TCP/IP 使用小的正整數對埠進行標識。" OSI 傳輸層使用的運輸選擇器 (TSEL) 相當於埠。
    Transport address (傳輸地址):用以識別傳輸層端點的網路地址和埠的組合,例如一個 IP 地址和一個 TCP 埠。資料包由一個源傳輸地址傳送到一個目的傳輸地址。
    Message stream (訊息流):通訊中訊息流通的一個邏輯通道。
    Message stream ID (訊息流 ID):每個訊息有一個關聯的 ID,使用 ID 可以識別出流通中的訊息流。
    Chunk (塊):訊息的一段。訊息在網路傳送之前被拆分成很多小的部分。塊可以確保端到端交付所有訊息有序 timestamp,即使有很多不同的流。
    Chunk stream (塊流):通訊中允許塊流向一個特定方向的邏輯通道。塊流可以從客戶端流向伺服器,也可以從伺服器流向客戶端。
    Chunk stream ID (塊流 ID):每個塊有一個關聯的 ID,使用 ID 可以識別出流通中的塊流。
    Multiplexing (合成):將獨立的音訊/影片資料合成為一個連續的音訊/影片流的加工,這樣可以同時傳送幾個影片和音訊。
    DeMultiplexing (分解):Multiplexing 的逆向處理,將交叉的音訊和影片資料還原成原始音訊和影片資料的格式。
    Remote Procedure Call (RPC 遠端方法呼叫):允許客戶端或伺服器呼叫對端的一個子程式或者程式的請求。
    Metadata (元資料):關於資料的一個描述。一個電影的 metadata 包括電影標題、持續時間、建立時間等等。
    Application Instance (應用例項):伺服器上應用的例項,客戶端可以連線這個例項併發送連線請求。
    Action Message Format (AMF 動作訊息格式協議):一個用於序列化 ActionScript 物件圖的緊湊的二進位制格式。AMF 有兩個版本:AMF 0 [AMF0] 和 AMF 3 [AMF3]。

基礎結構

RTMP 塊流包括其自身的帶內協議控制資訊,並且提供機制為上層協議植入使用者控制訊息。

可以被分割為塊以支援組合的訊息的格式取決於上層協議。訊息格式必須包含以下建立塊所需的欄位。

  1. Timestamp:訊息的 timestamp。這個欄位可以傳輸四個位元組。
  2. Length:訊息的有效負載長度。如果不能省略掉訊息頭,那它也被包括進這個長度。這個欄位佔用了塊頭的三個位元組。
  3. Type Id:一些型別 ID 保留給協議控制訊息使用。這些傳播資訊的訊息由 RTMP 塊流協議和上層協議共同處理。其他的所有型別 ID 可用於上層協議,它們被 RTMP 塊流處理為不透明值。事實上,RTMP 塊流中沒有任何地方要把這些值當做型別使用;所有訊息必須是同一型別,或者應用使用這一欄位來區分同步跟蹤,而不是型別。這一欄位佔用了塊頭的一個位元組。
  4. Message Stream ID:message stream (訊息流) ID 可以使任意值。合併到同一個塊流的不同的訊息流是根據各自的訊息流 ID 進行分解。除此之外,對 RTMP 塊流而言,這是一個不透明的值。這個欄位以小端格式佔用了塊頭的四個位元組。

Chunking (組塊)

首先,我們來了解下資料塊。

握手之後,連線開始對一個或多個塊流進行合併。建立的每個塊都有一個唯一 ID 對其進行關聯,這個 ID 叫做 chunk stream ID (塊流 ID)。這些塊通過網路進行傳輸。傳遞時,每個塊必須被完全傳送才可以傳送下一塊。在接收端,這些塊被根據塊流 ID 被組裝成訊息。

分塊允許上層協議將大的訊息分解為更小的訊息,例如,防止體積大的但優先順序小的訊息 (比如影片) 阻礙體積較小但優先順序高的訊息 (比如音訊或者控制命令)。

分塊也讓我們能夠使用較小開銷傳送小訊息,因為塊頭包含包含在訊息內部的資訊壓縮提示。

塊的大小是可以配置的,下面會詳細分析。

更大的塊大小可以降低 CPU 開銷,但在低頻寬連線時因為它的大量的寫入也會延遲其他內容的傳遞。
更小的塊不利於高位元率的流化。所以塊的大小設定取決於具體情況。

訊息結構Message

塊格式

每個塊包含一個頭和資料體,如下:

+--------------+----------------+--------------------+--------------+
 | Basic Header | Message Header | Extended Timestamp | Chunk Data |
 +--------------+----------------+--------------------+--------------+
 | 						      |
 |<------------------- Chunk Header ----------------->|

塊頭包含三個部分(Chunk Format):

  1. Basic Header (基本頭,1 到 3 個位元組):這個欄位對塊流 ID 和塊型別進行編碼。塊型別決定了訊息頭的編碼格式。(這一欄位的) 長度完全取決於塊流 ID,因為塊流 ID 是一個可變長度的欄位。
  2. Message Header (訊息頭,0,3,7,或者 11 個位元組):這一欄位對正在傳送的訊息 (不管是整個訊息,還是隻是一小部分) 的資訊進行編碼。這一欄位的長度可以使用塊頭中定義的塊型別進行決定。
  3. Extended Timestamp (擴充套件 timestamp,0 或 4 位元組):這一欄位是否出現取決於塊訊息頭中的 timestamp 或者 timestamp delta 欄位。更多資訊參考 5.3.1.3 節。

而Chunk Data (有效大小):當前塊的有效負載,相當於定義的最大塊大小。

塊基本頭(Basic Header)

塊基本頭對塊流 ID 和塊型別 (由下圖中的 fmt 欄位表示) 進行編碼[每個訊息命令都會有基本頭]。

塊基本頭欄位可能會有 1,2 或者 3 個位元組,取決於塊流 ID。

一個 (RTMP) 實現應該使用能夠容納這個 ID 的最小的容量進行表示。RTMP 協議最多支援 65597 個流,流 ID 範圍 3 - 65599,ID 0、1、2 被保留。

0 值表示二位元組形式,並且 ID 範圍 64 - 319 (第二個位元組 + 64)。
1 值表示三位元組形式,並且 ID 範圍為 64 - 65599 ((第三個位元組) * 256 + 第二個位元組 + 64)。
3 - 63 範圍內的值表示整個流 ID。
帶有 2 值的塊流 ID 被保留,用於下層協議控制訊息和命令。

塊基本頭中的 0 - 5 位 (最低有效) 代表塊流 ID。塊流 ID 2 - 63 可以編進這一欄位的一位元組版本中。

 0 1 2 3 4 5 6 7
 +-+-+-+-+-+-+-+-+
 |fmt| cs id     |
 +-+-+-+-+-+-+-+-+

 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |fmt| 0       | cs id - 64      |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |fmt| 1      | cs id - 64                       |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

訊息的頭資訊(Message Header)

訊息的頭資訊包含了要傳送的實際資訊(可能是完整的,也可能是一部分)的描述資訊。Message Header的格式和長度取決於Basic Header 的chunk type,即fmt,共有四種不同的格式。

其中第一種格式可以表示其他三種表示的所有資料,但由於其他三種格式是基於對之前chunk的差量化的表示,因此可以更簡潔地表示相同的資料,實際使用的時候還是應該採用儘量少的位元組表示相同意義的資料。

在 chunk 中會有時間戳 timestamp 和時間戳差 timestamp delta(下面的圖表會展示),並且它們不會同時存在,只有這兩者之一大於3位元組能表示的最大數值 0xFFFFFF = 16777215 時,才會用這個欄位來表示真正的時間戳,否則這個欄位為 0。擴充套件時間戳佔 4 個位元組,能表示的最大數值就是 0xFFFFFFFF = 4294967295。當擴充套件時間戳啟用時,timestamp欄位或者timestamp delta要全置為1,而不是減去時間戳或者時間戳差的值。

fmt = 0

當fmt為0是,佔據11個位元組,其他三種能表示的資料它都能表示,是最通用的結構(但在chunk stream 的開始第一個chunk和頭資訊

當fmt為0是,佔據11個位元組,其他三種能表示的資料它都能表示,是最通用的結構(但在chunk stream 的開始第一個chunk和頭資訊

中的時間戳後退(即值與上一個chunk相比減小,通常在回退播放的時候會出現這種中的時間戳後退(即值與上一個chunk相比減小,通常在回退播放的時候會出現這種情況)的時候必須採用這種格式)。

     0               1               2               3
     0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                    timestamp                  |message length |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |    message length (coutinue)  |message type id| msg stream id |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                  msg stream id                |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
timestamp(時間戳):佔用3個位元組,因此它最多能表示到16777215=0xFFFFFF=2^24-1,當它的值超過這個最大值時,這三個位元組都置為1,這樣實際的timestamp會轉存到 ExtendedTimestamp 欄位中,接收端在判斷timestamp欄位24個位都為1時就會去Extended Timestamp中解析實際的時間戳。
message length(訊息資料長度):佔用3個位元組,表示實際傳送的訊息的資料如音訊幀、影片幀等資料的長度,單位是位元組。注意這裡是Message的長度,也就是chunk屬於的Message的總長度,而不是chunk本身data的長度。
message type id(訊息的型別id):1個位元組,表示實際傳送的資料的型別,如8代表音訊資料,9代表影片資料。
message stream id(訊息的流id):4個位元組,表示該chunk所在的流的ID,和Basic Header的CSID一樣,它採用小端儲存方式。
fmt = 1

type為1時佔用7個位元組,省去了表示message stream id的4個位元組,表示此chunk和上一次發的 chunk 所在的流相同,如果在傳送端和對端有一個流連結的時候可以儘量採取這種格式。

     0               1               2               3
     0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |               timestamp delta                 |message length |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |    message length (coutinue)  |message type id|
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

要注意的是:

這裡的timestamp delta:3 bytes,這裡和type=0時不同,儲存的是和上一個chunk的時間差。類似上面提到的timestamp,當它的值超過3個位元組所能表示的最大值時,三個位元組都置為1,實際的時間戳差值就會轉存到Extended Timestamp欄位中,接收端在判斷timestamp delta欄位24個bit都為1時就會去Extended Timestamp 中解析實際的與上次時間戳的差值。

fmt = 2
     0               1               2
     0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |               timestamp delta                 |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

type 為 2 時只需佔用 3 個位元組,相對於 type = 1 格式又省去了表示訊息長度的3個位元組和表示訊息型別的1個位元組,表示此 chunk和上一次傳送的 chunk 所在的流、訊息的長度和訊息的型別都相同。餘下的這三個位元組表示 timestamp delta,使用同type=1。

fmt = 3

type=3時,為0位元組,表示這個chunk的Message Header和上一個是完全相同的。

當它跟在type=0的chunk後面時,表示和前一個 chunk 的時間戳都是相同。 什麼時候連時間戳都是相同呢?

就是一個 Message 拆分成多個 chunk,這個 chunk 和上一個 chunk 同屬於一個 Message。

而當它跟在 type = 1或 type = 2 的chunk後面時的chunk後面時,表示和前一個 chunk的時間戳的差是相同的。

比如第一個 chunk 的 type = 0,timestamp = 100,第二個 chunk 的 type = 2,timestamp delta = 20,表示時間戳為 100 + 20 = 120,第三個 chunk 的 type = 3,表示 timestamp delta = 20,時間戳為 120 + 20 = 140。

chunk分析(adobo官網示例)

demo 1

本示例展示了一個音訊訊息流。流中包含有冗餘資訊。

    +---------+-----------------+-----------------+-----------------+
    |         |Message Stream ID| Message Type ID | Time  | Length  |
    +---------+-----------------+-----------------+-------+---------+
    | Msg # 1 |    12345        |         8       | 1000  |   32    |
    +---------+-----------------+-----------------+-------+---------+
    | Msg # 2 |    12345        |         8       | 1020  |   32    |
    +---------+-----------------+-----------------+-------+---------+
    | Msg # 3 |    12345        |         8       | 1040  |   32    |
    +---------+-----------------+-----------------+-------+---------+
    | Msg # 4 |    12345        |         8       | 1060  |   32    |
    +---------+-----------------+-----------------+-------+---------+
              Sample audio messages to be made into chunks

最後實際傳送的chunk如下面表格所示,該表格展示了由此音訊流產生的塊資訊。從第 3 條資訊開始,資料傳輸達到最大優化。每條訊息的頭部只增加了 1 位元組長度。

    +--------+---------+-----+------------+------- ---+------------+
    |        | Chunk   |Chunk|Header Data |No.of Bytes|Total No.of |
    |        |Stream ID|Type |            |  After    |Bytes in the|
    |        |         |     |            |Header     |Chunk       |
    +--------+---------+-----+------------+-----------+------------+
    |Chunk#1 |    3    |  0  | delta: 1000|   32      |    44      |
    |        |         |     | length: 32,|           |            |
    |        |         |     | type: 8,   |           |            |
    |        |         |     | stream ID: |           |            |
    |        |         |     | 12345 (11  |           |            |
    |        |         |     | bytes)     |           |            |
    +--------+---------+-----+------------+-----------+------------+
    |Chunk#2 |    3    |  2  | 20 (3      |   32      |    36      |
    |        |         |     | bytes)     |           |            |
    +--------+---------+-----+----+-------+-----------+------------+
    |Chunk#3 |    3    |  3  | none (0    |   32      |    33      |
    |        |         |     | bytes)     |           |            |
    +--------+---------+-----+------------+-----------+------------+
    |Chunk#4 |    3    |  3  | none (0    |   32      |    33      |
    |        |         |     | bytes)     |           |            |
    +--------+---------+-----+------------+-----------+------------+
            Format of each of the chunks of audio messages

分析:

  1. 上面我們看協議規定了,第一條和最後一條fmt必須為0。

    因此,分片1的資料由 fmt+ header(11 bytes) +32 =44 bytes

  2. 看第二個chunk,由於 chunk 的 cs id 和 chunk type id和第一個都一直,因此是fmt=2.

    分片2的資料由 fmt+ header(3 bytes) +32 =36 bytes.

  3. 第三個chunk 和第二個 chunk 的 cs id ,chunk type id,以及 data 的長度和時間戳的差值都相同,因此採用 fmt=3.

    分片3的資料由 fmt+32 =36 bytes.

  4. 第四個同比第三個一致。

demo2

本示例展示了一條長訊息,由於訊息的長度超過了塊的最大長度(128bytes),此訊息在傳輸時將被分割成若干個塊。

    +-----------+-------------------+-----------------+-----------------+
    |           | Message Stream ID | Message Type ID | Time  | Length  |
    +-----------+-------------------+-----------------+-----------------+
    | Msg # 1   |       12346       |    9 (video)    | 1000  |   307   |
    +-----------+-------------------+-----------------+-----------------+
                    Sample Message to be broken to chunks

這個 Message 要分割成三個 chunk 傳送:

  1. 第一個 chunk:fmt = 0,timestamp = 1000,承擔 128 個bytes的 data.

    因此,分片1的資料由 fmt+ header(11 bytes) + 128 =140 bytes

  2. 第二個 chunk:同樣要傳送 128 bytes,其他欄位(即Message Header 中的幾個欄位)都與第一個相同,fmt=3.

    因此,分片2的資料由 fmt+ 128 =129 bytes

  3. 第三個 chunk:要傳送的 data 的長度為 307 - 128 - 128 = 51 bytes,fmt=3.

    因此,分片2的資料由 fmt+ 51 =129 bytes

    +-------+------+-----+-------------+-----------+------------+
    |       |Chunk |Chunk|Header       |No. of     |Total No. of|
    |       |Stream| Type|Data         |Bytes after| bytes in   |
    |       | ID   |     |             | Header    | the chunk  |
    +-------+------+-----+-------------+-----------+------------+
    |Chunk#1|  4   |  0  | delta: 1000 |  128      |   140      |
    |       |      |     | length: 307 |           |            |
    |       |      |     | type: 9,    |           |            |
    |       |      |     | stream ID:  |           |            |
    |       |      |     | 12346 (11   |           |            |
    |       |      |     | bytes)      |           |            |
    +-------+------+-----+-------------+-----------+------------+
    |Chunk#2|  4   |  3  | none (0     |  128      |   129      |
    |       |      |     | bytes)      |           |            |
    +-------+------+-----+-------------+-----------+------------+
    |Chunk#3|  4   |  3  | none (0     |  51       |   52       |
    |       |      |     | bytes)      |           |            |
    +-------+------+-----+-------------+-----------+------------+
                    Format of each of the chunks
分享到: