美文网首页janus
RTMP协议(三)分块与块包装

RTMP协议(三)分块与块包装

作者: Seacen_Liu | 来源:发表于2020-07-03 09:07 被阅读0次

    RTMP 是一个多媒体数据传播协议,相比于HTTP这些超文本协议,多媒体传输的音频和视频信息都相对较大。对于单个较大的信息可能会阻塞连接,导致优先级更高的信息无法传递,因此较大的信息一般都需要分包发送

    为了满足分包发送需求,RTMP 在传输的时候会对较大的数据进行分块(chunking操作,简而言之,就是将较大的信息分割成一个个的子信息,且不会超过配置的固定大小。发送端需要保证当前完整的“分包”信息发送完成再发送下一个“分包”,那么接收端就可以利用TCP有序到达的优势在对端进行重组。

    “分包”由于是都来自同一个大信息,因此在包头的描述处会存在大量的冗余数据。为了解决传输过程中的冗余数据,RTMP设计出一个特殊的数据格式——块(chunk,对需要传输的信息进行了块包装,在有序可靠的TCP的基础下利用不同类型的块尽可能消除冗余数据。从而可以令RTMP用较小的开支发送更多的小消息。

    块大小利弊

    • 更大的块可以降低 CPU 开销, 但在低带宽连接时会因其大量的写入导致延迟其他内容的传递;
    • 更小的块确不利于高比特率的流化。

    虽然RTMP提供了分块(chunking操作与块(chunk包装的设计,然而在块大小的选择上我们需要根据实际情况权衡利弊

    块流与信息流

    • 块流(Chunk Stream:指块组成的数据流,是 RTMP 协议传输的数据流。
    • 信息流(Message Stream:指信息组成的数据流,是需要传输的主要信息体构成的数据流。

    RTMP 握手完成之后,连接开始对一个或多个块流进行多路传输。通过块流ID(Chunk Stream ID可以将同一个块流(Chunk Stream块(Chunk整理在一起,然后将块(Chunk信息(message根据信息流ID(Messsage Stream ID重新连接在一起。当一个完整的音视频信息接收完成后,接收端就可以继续做解码等一系列工作了。

    块格式

    每个块包含一个块头和块数据体,其中块头包含三个部分:

    +--------------+----------------+--------------------+--------------+
    | Basic Header | Message Header | Extended Timestamp |  Chunk Data  |
    +--------------+----------------+--------------------+--------------+
    |                                                    |
    |<------------------- Chunk Header ----------------->|
                          Chunk Format
    
    • Basic Header(基本头,1~3字节):包含chunk stream ID(块流ID)和chunk type(块类型),长度由chunk stream ID决定。
    • Message Header(消息头,0,3,7,11字节):包含被发送的消息信息(全部或部分),长度由块头中的chunk type来决定。
    • Extended Timestamp(扩展时间戳,0,4字节):这个字段是否存在取决于块消息头中的timestamp或者timestamp delta字段。
    • Chunk Data(块数据,可变大小):当前块的有效数据,即信息的主体部分,上限为配置的最大块大小减去块头。

    Basic Header

    Basic Header(基本头信息)用于存放整个块头的基本信息,包括chunk stream ID(块流ID)和chunk type(块类型)。同时,Basic Header 应该使用最小的容量存放chunk typechunk stream ID,进而减少引入 Header 增加的数据量。

    • chunk type(块类型):决定消息头的后续的编码格式
      • 一般使用fmt表示,用来标识一个chunk的类型
      • chunk type的长度固定为2 bits
    • chunk stream ID(块流ID):决定 Basic Header 的字节数
      • 一般简写为CSID,用来标识一个特定的块流通道
      • chunk stream ID的长度由其真实值确定,取值为 6、14、22 bits
      • RTMP 最多支持 65597 个流,CSID的范围为3 ~ 65599,其中0、1、2被保留用作 Basic Header 的版本标记:
        • 0:表示二字节版本
        • 1:表示三字节版本
        • 2:保留值,用于下层协议控制消息和命令
        • 3 ~ 65599:用于表示不同的块流

    Basic Header 字节数形式与CSID的表示范围:

    • 一字节形式CSID范围为3~63 (63 = 2^6 - 1)
    • 二字节形式CSID范围为64~319 (319 = (2^8 - 1) + 64 = 255 + 64)
    • 三字节形式CSID范围为64~65599 (65599 = (2^16 - 1) + 64 = 65535 + 64)

    一字节版本 - CSID范围为3~63

    一字节中没有辅助的字段,而是直接将一字节剩下的6bits用作存放CSID

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

    CSID = <第一个字节的值(fmt=0)>

    二字节版本 - CSID范围为64~319

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

    条件: <第一个字节的值(fmt=0)> == 0

    CSID = <第二个字节的值> + 64

    三字节版本 - CSID范围为64~65599

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

    条件: <第一个字节的值(fmt=0)> == 1

    CSID = <第三个字节的值> * 256 + <第二个字节的值> + 64

    64~319 既可以用二字节形式也可用三字节形式表示,为了数据大小最好用二字节,不过具体还是看实际情况

    Message Header

    Message Header(块信息头)用于存放实际信息的描述信息。它有四种不同的格式,由 Basic Header 中的chunk typefmt进行区分。其中,第一种格式可以表示其他三种表示的所有数据,但由于其他三种格式是基于对之前chunk的差量化的表示,因此可以更简洁地表示相同的数据,实际使用的时候还是应该采用尽量少的字节表示相同意义的数据。根据chunk typefmt取值为0123,我们将其命名为类型 0、类型 1、类型 2、类型 3。

    类型 0(fmt = 0, 11 bytes)

    类型 0 的块头信息长度为 11 个字节,它能表示其他三种类型的数据,并且该类型必须用在chunk stream的起始位置和流时间戳回退或重置的时候。

     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 |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |          message stream id (coutinue)         |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    
    • timestamp(时间戳):占用 3 个字节,因此它最多能表示到16777215=0xFFFFFF=2^24-1,当它的值超过这个最大值时,这三个字节都置为1,这样实际的timestamp会转存到Extended Timestamp字段中,接收端在判断timestamp字段 24 bits 都为1时就会去Extended Timestamp
      中解析实际的时间戳。
    • message length(消息数据长度):占用 3 个字节,表示实际发送的消息的数据(如音频帧、视频帧等数据)的长度,单位是字节。注意这里是整个chunk的长度,而不是chunk本身data的长度。
    • message type id(消息的类型id):占用 1 个字节,表示实际发送的数据的类型
      • 12356:用于协议控制信息
      • 8:代表音频数据
      • 9:代表视频数据
    • message stream id(消息的流id):占用 4 个字节,表示该chunk所搭载的信息所在的信息流的ID(Basic Header的CSID一样,采用小端存储方式)。

    类型 1(fmt = 1, 7 bytes)

    类型 1 的块头信息长度为 7 个字节,它省去了message stream id的 4 个字节,表示此chunkd的信息和上一次发的chunk所在的信息流相同。该块通常跟随在类型 0 的块后面,表示其信息流不变,但是其信息长度是可变的(例如一些视频格式数据)。

     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 个字节,存储的是和上一个chunk的时间差。类似上面提到的timestamp,当它的值超过 3 个字节所能表示的最大值时,都会像timestamp采取一致的方式转存该值到Extended Timestamp
    • 其他字段与上面的解释相同

    类型 2(fmt = 2, 3 bytes)

    类型 2 的块头信息长度为 3 个字节,它又省去了message length的 3 个字节和message type id的 1 个字节,只使用 3 个字节表示timestamp delta。该块通常用于类型 0 或类型 1 的块后面,表示其所在的信息流、信息长度和信息类型都是不变的(例如一些音频和数据格式)。

     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                 |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    

    类型 3(fmt = 3, 0 byte)

    类型 3 的块头信息长度为 0 个字节,它把所有描述字段都省略了,表示这个chunk的 Message Header 和上一个chunk的完全相同。下面举例说明类型 3 的使用场景:

    场景一:相同信息流中,信息的间隔相同

    • 第一个chunk是类型 0 的,其timestamp为100;
    • 第二个chunk是类型 2 的,其timestamp delta为20,即timestamp为 100 + 20 = 120;
    • 第三个chunk是类型 3 的,其timestamp就为 120 + 20 = 140。

    列子二:较大信息被分块后,每一个块中的信息处于同一时间戳

    • 第一个chunk是类型 0 的,其timestamp为200;
    • 第二个chunk是类型 3 的,即timestamp也为200;
    • 第三个chunk是类型 3 的,即timestamp也为200;

    Extended Timestamp

    Extended Timestamp(扩展时间戳)用于扩展 Message Header 中时间戳的表示。

    • 该字段占4个字节(即32bits),最大能表示的数值为 4294967295(0xFFFFFFFF)。
    • 该字段用于对大于3个字节能表示的16777215 (0xFFFFFF) ,即对不适合于在 24 bit 的类型 0、1 和 2 的chunktimestamp或者timestamp delta进行编码。
    • 在时间戳表示正常的时候,这个字段的数值全为0;当该字段启用时,timestamp或者timestamp delta必须全置为1
    • 当最近的具有同􏰍一块流的类型 0􏰋、1 或 2 chunk指示 Extended Timestamp 字段出现时,这一􏰩段才会在类型 3 的chunk中出现。

    举例

    上面的介绍相对比较枯燥,通过下面的例子,我们就可以切身体会到块与分块的好处。

    例子 1:块包装信息流的效果 - 解决信息冗余问题

    本例展示的一个简单的音频信息流的信息冗余情况:

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

    Message Stream ID假设用 4 个字节表示,Message Type ID假设用 1 个字节表示,Time假设用 3 个字节表示,那么信息流分成的每一个信息的估算长度为 40 个字节。并且用这中信息表示方式,后续的信息中会越来越多冗余的信息。

    下图表示RTMP中对这个信息流进行块包装的效果:

        +--------+---------+-----+------------+------- ---+------------+
        |        | 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
    

    由于Chunk Stream ID为 3,小于 64,因此 Basic Header 只占用 1 个字节。由于timestamptimestamp detal只为 1000 和 20,因此不需要使用 Extended Timestamp 字段。综上,我们在计算 Chunk Header 大小的时候只需要计算 Message Header 的大小加 1 即可。以下的字节计算式均为:Basic Header + Message Header + Extended Timestamp + Chunk Data。

    • Chunk#1:作为整个块流的第一个,该chunk必须为类型 0 的块,因此该块的大小为 44 字节。
      • 44 = 1 + 11+ 0 + 32
    • Chunk#2:由Chunk#1message type8所知,该信息为一个音频信息流。由于音频流信息分割后都是等分的,因此这里能不使用类型 1 直接采用类型 2 省略相同的信息类型、信息流ID和信息长度。可知 1020 与 1000 相差 20,因此timestamp的位置直接存储timestamp detal时间差。这样能避免时间戳太大,因此块的时间戳的字节也只采用 3 个字节,这样能有效压缩数据,也不会影响使用。因此该块大小为 36 字节。
      • 36 = 1 + 3 + 0 + 32
    • Chunk#3:由于Chunk#3的时间差与Chunk#2是一致的,因此可以在类型 2 的基础上再省略掉timestamp detal直接使用类型 3,即省略掉整个 Message Header 部分。因此该快的大小只为 33 字节。
      • 33 = 1 + 0 + 0 + 32
    • Chunk#4:由于后续的信息类型、信息流ID、信息长度和时间差都没有变化,因此依然能完全省略 Message Header,该快大小也为 33 字节

    综上情况,从第 3 个chunk开始,数据传输达到最优化,只用一个字节就能标识信息。

    例子 2:信息流分块效果 - 避免单个包过大

    本例展示了一条长消息,该信息流单独封包相对比较大,可能会因为单独传输这个包而延后了优先级更高的信息

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

    由于消息的长度超过了块的最大长度(128字节),此消息传输时需要对其做分块操作,即将大的消息分割成若干个块,效果如下图所示:

        +-------+------+-----+-------------+-----------+------------+
        |       |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
    

    由于Chunk Stream ID为 4,小于 64,因此 Basic Header 也只占用 1 个字节。由于timestamp为 1000,因此也不需要使用 Extended Timestamp 字段。因此该例的块字节大小计算与上一例相同。

    • Chunk#1:作为块流第一个,它的类型必须是 0,因此该块的大小为 140 字节。
      • 140 = 1 + 11 + 0 + 128
    • Chunk#2:这个块是从同一个大消息中分割出来的,因此他的时间戳和上一个块一致,所以这里能直接使用类型 3 把所有 Message Header 都省略。因此该块的大小为 129 字节。
      • 129 = 1 + 0 + 0 + 128
    • Chunk#3:假设这个视频格式编码出来的信息长度是固定的,这里把剩余的信息封块,因此可以直接使用类型 3 表示。因此块的大小为 52 字节。
      • 52 = 1 + 0 + 0 + 51

    综上两个例子,类型 3 的块有两种使用方式:

    • 说明消息的继续
    • 说明新消息的头信息可以由前面存在的消息中推导出来

    相关文章

      网友评论

        本文标题:RTMP协议(三)分块与块包装

        本文链接:https://www.haomeiwen.com/subject/luujqktx.html