一 RTMP协议详解
1.总体介绍
RTMP协议是应⽤层协议,是要靠底层可靠的传输层协议(通常是TCP)来保证信息传输的可靠性的。在 基于传输层协议的链接建⽴完成后,RTMP协议也要客户端和服务器通过“握⼿”来建⽴基于传输层链接之 上的RTMP Connection链接,在Connection链接上会传输⼀些控制信息,如 SetChunkSize,SetACKWindowSize。其中CreateStream命令会创建⼀个Stream链接,⽤于传输具体的 ⾳视频数据和控制这些信息传输的命令信息。RTMP协议传输时会对数据做⾃⼰的格式化,这种格式的消 息我们称之为RTMP Message,⽽实际传输的时候为了更好地实现多路复⽤、分包和信息的公平性,发送 端会把Message划分为带有Message ID的Chunk,每个Chunk可能是⼀个单独的Message,也可能是 Message的⼀部分,在接受端会根据chunk中包含的data的⻓度,message id和message的⻓度把 chunk还原成完整的Message,从⽽实现信息的收发。
RTMP 是 Real Time Messaging Protocol( 实时消息传输协议) 的首字母缩写。该协议基于 TCP,是一个协议族,包括 RTMP 基本协议及 RTMPT/RTMPS/RTMPE 等多种变种。
RTMP 与 HTTP 一样, 都属于 TCP/IP 四层模型的应用层。
上述红色部分的含义如下图:
2. 握手
要建⽴⼀个有效的RTMP Connection链接,⾸先要“握⼿”:客户端要向服务器发送C0,C1,C2(按序)三个 chunk,服务器向客户端发送S0,S1,S2(按序)三个chunk,然后才能进⾏有效的信息传输。RTMP协议 本身并没有规定这6个Message的具体传输顺序,但RTMP协议的实现者需要保证这⼏点:
- 客户端要等收到S1之后才能发送C2
- 客户端要等收到S2之后才能发送其他信息(控制信息和真实⾳视频等数据)
- 服务端要等到收到C0之后发送S1
- 服务端必须等到收到C1之后才能发送S2
- 服务端必须等到收到C2之后才能发送其他信息(控制信息和真实⾳视频等数据)
如果每次发送⼀个握⼿chunk的话握⼿顺序会是这样:
理论上来讲只要满⾜以上条件,如何安排6个Message的顺序都是可以的,但实际实现中为了在保证握⼿ 的身份验证功能的基础上尽量减少通信的次数,⼀般的发送顺序是这样的,这⼀点可以通过wireshark抓 ffmpeg推流包进⾏验证:
|client|Server |
|----C0+C1---->|
|<----S0+S1+S2----|
|----C2---->|
握手完成后,就可以发送数据了,那么这个数据的形式是什么样子的呢? 是 RTMP Chunk Stream,那么这个RTMP Chunk Stream 具体是啥呢?
3. RTMP Chunk Stream (重点)
Chunk Stream是对传输RTMP Chunk的流的逻辑上的抽象,客户端和服务器之间有关RTMP的信息都在 这个流上通信。这个流上的操作也是我们关注RTMP协议的重点。
3.1 Message(消息)
这⾥的Message是指满⾜该协议格式的、可以切分成Chunk发送的消息,消息包含的字段如下:
- Timestamp(时间戳):消息的时间戳(但不⼀定是当前时间,后⾯会介绍),4个字节
- Length(⻓度):是指Message Payload(消息负载)即⾳视频等信息的数据的⻓度,3个字节
- TypeId(类型Id):消息的类型Id,1个字节
- Message Stream ID(消息的流ID):每个消息的唯⼀标识,划分成Chunk和还原Chunk为Message 的时候都是根据这个ID来辨识是否是同⼀个消息的Chunk的,4个字节,并且以⼩端格式存储 (Message Stream ID如何产⽣?audio和video使⽤不同的Message Stream ID)
3.2 Chunking(Message分块)
RTMP在收发数据的时候并不是以Message为单位的,⽽是把Message拆分成Chunk发送,⽽且必须 在⼀个Chunk发送完成之后才能开始发送下⼀个Chunk。每个Chunk中带有MessageID代表属于哪个 Message,接受端也会按照这个id来将chunk组装成Message。
为什么RTMP要将Message拆分成不同的Chunk呢?通过拆分,数据量较⼤的Message可以被拆分成 较⼩的“Message”,这样就可以避免优先级低的消息持续发送阻塞优先级⾼的数据,⽐如在视频的传输过 程中,会包括视频帧,⾳频帧和RTMP控制信息,如果持续发送⾳频数据或者控制数据的话可能就会造成 视频帧的阻塞,然后就会造成看视频时最烦⼈的卡顿现象。同时对于数据量较⼩的Message,可以通过对 Chunk Header的字段来压缩信息,从⽽减少信息的传输量。(具体的压缩⽅式会在后⾯介绍)
Chunk的默认⼤⼩是128字节,在传输过程中,通过⼀个叫做Set Chunk Size的控制信息(⻅ spec 5.4.1 )可以设置Chunk数据量的最⼤值,在发送端和接受端会各⾃维护⼀个Chunk Size(srs流媒 体服务器默认是60000),可以分别设置这个值来改变⾃⼰这⼀⽅发送的Chunk的最⼤⼤⼩。⼤⼀点的 Chunk减少了计算每个chunk的时间从⽽减少了CPU的占⽤率,但是它会占⽤更多的时间在发送上,尤其 是在低带宽的⽹络情况下,很可能会阻塞后⾯更重要信息的传输。⼩⼀点的Chunk可以减少这种阻塞问 题,但⼩的Chunk会引⼊过多额外的信息(Chunk中的Header),少量多次的传输也可能会造成发送的间 断导致不能充分利⽤⾼带宽的优势,因此并不适合在⾼⽐特率的流中传输。在实际发送时应对要发送的数 据⽤不同的Chunk Size去尝试,通过抓包分析等⼿段得出合适的Chunk⼤⼩,并且在传输过程中可以根据 当前的带宽信息和实际信息的⼤⼩动态调整Chunk的⼤⼩,从⽽尽量提⾼CPU的利⽤率并减少信息的阻塞 机率。
3.3 Chunk Format(块格式)Chunk的默认⼤⼩是128字节
3.3.1 Basic Header(基本的头信息):
Basic Header是变⻓的,Basic Header的⻓度可能是1,2,或3个字节。也就是8/16/24位.
包含了 chunk type(chunk的类型)和 chunk stream ID(流通道Id,也叫做CSID)
CSID 为0,1,2,
当CSID 为0时,表示了Basic Header占⽤2个字节
当CSID 为1时,表示了Basic Header占⽤3个字节
当CSID 为2时,表示了Basic Header占⽤1个字节 ,代表该chunk是控制信息和⼀些命令信息,后⾯会有详细的介绍。(这块不确定,还需要再往后看)
当Basic Header为1个字节时,CSID占6位,6位最多可以表示64个数,因此这种情况下CSID在 [0, 63] 之间,其中⽤户可⾃定义的范围为 [3,63] ,实际是可以⽤2开始⽤。
当Basic Header为2个字节时,CSID占只占8位,第⼀个字节除chunk type占⽤的bit都置为0,第⼆ 个字节⽤来表示CSID-64,8位可以表示 [0, 255] 共256个数,ID的计算⽅法为(第⼆个字节+64),范围为 [64,319]。
注意如果不加64,那么就和 Basic Header为1个字节 时表示的数据重复了,因此规定了要加64
当Basic Header为3个字节时,以在此字段⽤3字节版本编码。ID的计算⽅法为(第三字节 *256+第⼆字节+64)(Basic Header是采⽤⼩端存储的⽅式),范围为 [64,65599]
可以看到2个字节和3个字节的Basic Header所能表示的CSID是有交集的 [64,319],但实际实现时还 是应该秉着最少字节的原则使⽤2个字节的表示⽅式来表示 [64,319] 的CSID。
3.3.2 Message Header(消息的头信息)
包含了要发送的实际信息的描述信息。(这个实际信息 可能是完整的,也可能是⼀部分)
Basic Header的chunk type 决定了Message Header的格式和 ⻓度 。
共有4种不同的格式。
由上⾯所提到的Basic Header中的fmt 字段控制。
其中第⼀种格式可以表示其他三种表示的所有数据,
但由于其他三种格式是基于对之前chunk 的差量化的表示,因此可以更简洁地表示相同的数据,实际使⽤的时候还是应该采⽤尽量少的字节表示相 同意义的数据。以下按照字节数从多到少的顺序分别介绍这4种格式的Message Header。
Type=0: 占⽤11个字节
type=0时Message Header占⽤11个字节,其他三种能表示的数据它都能表示,但在chunk stream的 开始的第⼀个chunk和头信息中的时间戳后退(即值与上⼀个chunk相⽐减⼩,通常在回退播放的时候会出 现这种情况)的时候必须采⽤这种格式。
- timestamp(时间戳):占⽤3个字节,因此它最多能表示到16777215=0xFFFFFF=2 -1, 当它的值超过这个最⼤值时,这三个字节都置为1,这样实际的timestamp会转存到Extended Timestamp字段中,接受端在判断timestamp字段24个位都为1时就会去Extended timestamp中解析 实际的时间戳。
- message length(消息数据的⻓度):占⽤3个字节,表示实际发送的消息的数据如⾳频帧、视频帧 等数据的⻓度,单位是字节。注意这⾥是Message的⻓度,也就是chunk属于的Message的总数据⻓ 度,⽽不是chunk本身Data的数据的⻓度。 message type id(消息的类型id):占⽤1个字节,表示实际发送的数据的类型,如8代表⾳频数据、9 代表视频数据。
- msg stream id(消息的流id):占⽤4个字节,表示该chunk所在的流的ID,和Basic Header的CSID ⼀样,它采⽤⼩端存储的⽅式,
Type = 1:占⽤7个字节
type=1时Message Header占⽤7个字节,省去了表示msg stream id的4个字节,表示此chunk和上 ⼀次发的chunk所在的流相同,如果在发送端只和对端有⼀个流链接的时候可以尽量去采取这种格式。
- timestamp delta:占⽤3个字节,注意这⾥和type=0时不同,存储的是和上⼀个chunk的时间差。类 似上⾯提到的timestamp,当它的值超过3个字节所能表示的最⼤值时,三个字节都置为1,实际的时间戳差值就会转存到Extended Timestamp字段中,接受端在判断timestamp delta字段24个位都为1时 就会去Extended timestamp中解析时机的与上次时间戳的差值。
Type = 2:占⽤3个字节
type=2时Message Header占⽤3个字节,相对于type=1格式⼜省去了表示消息⻓度的3个字节和表示消 息类型的1个字节,表示此chunk和上⼀次发送的chunk所在的流、消息的⻓度和消息的类型都相同。余下 的这三个字节表示timestamp delta,使⽤同type=1。
Type = 3:占⽤0字节
0字节!!!好吧,它表示这个chunk的Message Header和上⼀个是完全相同的,⾃然就不⽤再传输 ⼀遍了。当它跟在Type=0的chunk后⾯时,表示和前⼀个chunk的时间戳都是相同的。什么时候连时间戳 都相同呢?就是⼀个Message拆分成了多个chunk,这个chunk和上⼀个chunk同属于⼀个Message。⽽ 当它跟在Type=1或者Type=2的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
4种type对⽐
(type 3⽆字段就不画上去了)
3.3.3 Extended Timestamp(扩展时间戳)
上⾯我们提到在chunk中会有时间戳timestamp和时间戳差timestamp delta,并且它们不会同时存 在,只有这两者之⼀⼤于3个字节能表示的最⼤数值0xFFFFFF=16777215时,才会⽤这个字段来表示真正 的时间戳,否则这个字段为0。扩展时间戳占4个字节,能表示的最⼤数值就是0xFFFFFFFF=4294967295。当扩展时间戳启⽤时,timestamp字段或者timestamp delta要全置为0xFFFFFF,表示 应该去扩展时间戳字段来提取真正的时间戳或者时间戳差。注意扩展时间戳存储的是完整值,⽽不是减去 时间戳或者时间戳差的值。
3.3.4 Chunk Data(块数据):
⽤户层⾯上真正想要发送的与协议⽆关的数据,⻓度在(0,chunkSize]之间。
3.3.5 chunk表示例1
这个例⼦显示了⼀个简单的⾳频信息流。这个例⼦演示了信息的冗余。
⾸先包含第⼀个Message的chunk的Chunk Type为0,因为它没有前⾯可参考的chunk,timestamp 为1000,表示时间戳。type为0的header占⽤11个字节,假定chunkstreamId为3<127,因此Basic Header占⽤1个字节,再加上Data的32个字节,因此第⼀个chunk共44=11+1+32个字节。
第⼆个chunk和第⼀个chunk的CSID,TypeId,Data的⻓度都相同,因此采⽤Chunk Type=2, timestamp delta=1020-1000=20,因此第⼆个chunk占⽤36=3+1+32个字节。
第三个chunk和第⼆个chunk的CSID,TypeId,Data的⻓度和时间戳差都相同,因此采⽤Chunk Type =3省去全部Message Header的信息,占⽤33=1+32个字节。
第四个chunk和第三个chunk情况相同,也占⽤33=1+32个字节。 最后实际发送的chunk如下:
3.3.6 chunk表示例2
此示例说明了⼀个消息因为太⻓,以⾄于⽆法适⽤⼀个128字节的chunk,从⽽被分解成多个 chunk。
注意到Data的Length=307>128,因此这个Message要切分成⼏个chunk发送,第⼀个chunk的Type= 0,Timestamp=1000,承担128个字节的Data,因此共占⽤140=11+1+128个字节。
第⼆个chunk也要发送128个字节,其他字段也同第⼀个chunk,因此采⽤Chunk Type=3,此时时间 戳也为1000,共占⽤129=1+128个字节。
第三个chunk要发送的Data的⻓度为307-128-128=51个字节,还是采⽤Type=3,共占⽤1+51=52 个字节。 最后实际发送的chunk如下:
从两个例⼦中注意到,类型3的chunk可以⽤在两种不同的⽅式中。第⼀种是指定消息的继 续。第⼆种是指定⼀个新的消息的开始,它的头可以来⾃于现有的状态数据。