rtmp协议分析(Message 消息,Chunk分块)

RTMP详细分析(三次握手)
librtmp分析(发送数据包处理)
librtmp分析(接收数据包处理)

目录


RTMP Chunk Stream
Chunk Stream是对传输RTMP Chunk的流的逻辑上的抽象,客户端和服务器之间有关RTMP的信息都在
这个流上通信。这个流上的操作也是我们关注RTMP协议的重点。

1、Message(消息)

这里的Message是指满足该协议格式的、可以切分成Chunk发送的消息,消息包含的字段如下:

Timestamp(时间戳):消息的时间戳(但不一定是当前时间),4个字节。

Length(长度):是指Message Payload(消息负载)即音视频等信息的数据的长度,3个字节。

TypeId(类型Id):消息的类型Id,1个字节。

Message Stream ID(消息的流ID,应该叫Chunk Stream ID更准确):每个消息的唯一标识,划分成Chunk和还原Chunk为Message的时候都是根据这个ID来辨识是否是同一个消息的Chunk的,4个字节,并且以小端格式存储。
(Message Stream ID如何产生?audio和video使用不同的Message Stream ID)

2、Chunking(Message 分块)

RTMP在收发数据的时候并不是以Message为单位的,而是把Message拆分成Chunk发送,而且必须
在一个Chunk发送完成之后才能开始发送下一个Chunk。每个Chunk中带有MessageID(Chunk Stream ID)代表属于哪个Message,接受端也会按照这个id来将chunk组装成Message。
为什么RTMP要将Message拆分成不同的Chunk呢?通过拆分,数据量较大的Message可以被拆分成较小的“Message”,这样就可以避免优先级低的消息持续发送阻塞优先级高的数据,比如在视频的传输过程中,会包括视频帧,音频帧和RTMP控制信息,如果持续发送音频数据或者控制数据的话可能就会造成视频帧的阻塞,然后就会造成看视频时最烦人的卡顿现象。同时对于数据量较大的Message,可以通过对
Chunk Header的字段来压缩信息,从而减少信息的传输量。
Chunk的默认大小是128字节,在传输过程中,通过一个叫做Set Chunk Size的控制信息可以设置Chunk数据量的最大值,在发送端和接受端会各自维护一个Chunk Size(srs流媒体服务器默认是60000),可以分别设置这个值来改变这一方发送的Chunk的最大值。大一点的Chunk减少了计算每个chunk的时间从而减少了CPU的占用率,但是它会占用更多的时间在发送上,尤其是在低带宽的网络情况下,很可能会阻塞后面更重要信息的传输。小一点的Chunk可以减少这种阻塞问题,但小的Chunk会引起过多额外的信息(Chunk中的Header),少量多次的传输也可能会造成发送的间断导致不能充分利用高带宽的优势,因此并不适合在高比特率的流中传输。在实际发送时应对要发送的数据用不同的Chunk Size去尝试,通过抓包分析等手段得出合适的Chunk大小,并且在传输过程中可以根据当前的带宽信息和实际信息的大小动态调Chunk的大小,从而尽量提高CPU的利用率并减少信息的阻塞机率。

Chunk Format(块格式):
在这里插入图片描述

2.1、 Basic Header(基本的头信息)

包含了chunk stream ID(流通道Id)和chunk type(chunk的类型),chunk stream id一般被简写为CSID,用来唯一标识一个特定的流通道,chunk type决定了后面Message Header的格式。Basic Header的长度可能是1,2,或3个字节,其中chunk type的长度是固定的(占2位,注意单位是位,bit),Basic Header是变长的,其长度取决于CSID的大小,在足够存储这两个字段的前提下最好用尽量少的字节从而减少由于引入Header增加的数据量。
RTMP协议最多支持65597个用户自定义chunk stream ID,范围为[3,65599] ,ID 0, 1, 2被协议规范直接使用,其中ID值为0, 1分表表示了Basic Header占用2个字节和3个字节:
ID值0:代表Basic Header占用2个字节,CSID在 [64,319] 之间;
ID值1:代表Basic Header占用3个字节,CSID在 [64,65599] 之间;
ID值2:代表该chunk是控制信息和一些命令信息,后面会有详细的介绍。

2.1.1、Basic Header为1个字节时

CSID占6位,6位最多可以表示64个数,因此这种情况下CSID在 [0,
63] 之间,其中用户可以定义的范围为 [3,63] 。
在这里插入图片描述

2.1.2、Basic Header为2个字节时

CSID占只占8位,第一个字节除chunk type占用的bit都置为0,第二
个字节用来表示CSID-64,8位可以表示 [0, 255] 共256个数,ID的计算方法为(第二个字节+64),范围为 [64,319]。
在这里插入图片描述

2.1.3、Basic Header为3个字节时

在此字段用3字节版本编码。ID的计算方法为(第三字节*256+第二字节+64)(Basic Header是采用小端存储的方式),范围为 [64,65599]。
在这里插入图片描述
可以看到2个字节和3个字节的Basic Header所能表示的CSID是有交集的 [64,319],但实际实现时还是应该秉着最少字节的原则使用2个字节的表示方式来表示 [64,319] 的CSID。

2.2、Message Header(消息的头信息)

包含了要发送的实际信息(可能是完整的,也可能是一部分)的描述信息。Message Header的格式和长度取决于Basic Header的chunk type,共有4种不同的格式,由上面所提到的Basic Header中的fmt 字段控制。其中第一种格式可以表示其他三种表示的所有数据,但由于其他三种格式是基于对之前chunk的差量化的表示,因此可以更简洁地表示相同的数据,实际使用的时候还是应该采样尽量少的字节表示相同意义的数据。

以下按照字节数从多到少的顺序分别介绍这4种格式的Message Header。

2.2.1、Type(fmt) = 0:占用11个字节

在这里插入图片描述
Type(fmg) = 0时,Message Header占用11个字节,其他三种能表示的数据它都能表示,但在chunk stream的开始的第一个chunk和头信息中的时间戳后退(即值与上一个chunk相比减小,通常在回退播放的时候会出现这种情况)的时候必须采用这种格式。

timestamp(时间戳):占用3个字节,因此它最多能表示到16777215=0xFFFFFF, 当它的值超过这个最大值时,这三个字节都置为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一样,它采用小端存储的方式。

2.2.2、Type(fmt) = 1:占用7个字节

在这里插入图片描述
Type(fmg) = 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中解析时机的与上次时间戳的差值。

2.2.3、Type(fmt) = 2:占用3个字节

在这里插入图片描述
Type(fmg) = 2时,Message Header占用3个字节,相对于type=1格式又省去了表示消息长度的3个字节和表示消息类型的1个字节,表示此chunk和上一次发送的chunk所在的流、消息的长度和消息的类型都相同。余下的这三个字节表示timestamp delta,使用同type=1。

2.2.4、Type(fmt) = 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。

以上几种Type的Message Header对比:
在这里插入图片描述
在这里插入图片描述

2.3、Extended Timestamp(扩展时间戳)

在chunk中会有时间戳timestamp和时间戳差timestamp delta,并且它们不会同时存在,只有这两者之一大于3个字节能表示的最大数值0xFFFFFF=16777215时,才会用这个字段来表示真正的时间戳,否则这个字段为0。扩展时间戳占4个字节,能表示的最大数值就是0xFFFFFFFF=4294967295。当扩展时间戳启用时,timestamp字段或者timestamp delta要全置为0xFFFFFF,表示应该去扩展时间戳字段来提取真正的时间戳或者时间戳差。

2.4、Chunk Data(块数据)

用户层面上真正想要发送的与协议无关的数据,长度在(0,chunkSize]之间。

3、协议控制消息(Protocol Control Message)

在RTMP的chunk流会有一些特殊的值来代表协议的控制消息,它们的Message Stream ID必须为0(代表控制流信息),CSID必须为2,Message Type ID可以为1,2,3,5,6,具体代表的消息会在下面依次说明。控制消息的接受端会忽略掉chunk中的时间戳,收到后立即生效。

3.1、Set Chunk Size(ID=1)

设置chunk中Data字段所能承载的最大字节数,默认为128B,通信过程中可以通过发送该消息来设置chunk Size的大小(不得小于128B),而且通信双方会各自维护一个chunkSize,两端的chunkSize是独立的。比如当A想向B发送一个200B的Message,但默认的chunkSize是128B,因此就要将该消息拆分为Data分别为128B和72B的两个chunk发送,如果此时先发送一个设置chunkSize为256B的消息,再发送Data为200B的chunk,本地不再划分
Message,B接受到Set Chunk Size的协议控制消息时会调整的接受chunk的Data的大小,也不用再将两个chunk组成为一个Message。在实际写代码的时候一般会把chunk size设置的很大,有的会设置为4096,FFMPEG推流的时候设置的是 60*1000,这样设置的好处是避免了频繁的拆包组包,占
用过多的CPU。

以下为代表Set Chunk Size消息的chunk的Data:
在这里插入图片描述
其中第一位必须为0,chunk Size占31个位,最大可代表2147483647=0x7FFFFFFF=2 -1,但实际上所有大于16777215=0xFFFFFF的值都用不上,因为chunk size不能大于Message的长度,表示Message的长度字段是用3个字节表示的,最大只能为0xFFFFFF。

3.2、Abort Message (ID=2)

当一个Message被切分为多个chunk,接受端只接收到了部分chunk时,发送该控制消息表示发送端不再传输同Message的chunk,接受端接收到这个消息后要丢弃这些不完整的chunk。Data数据中只需要一个CSID,表示丢弃该CSID的所有已接收到的chunk。
在这里插入图片描述

3.3、Acknowledgement (ID=3)、Window Acknowledgement Size (ID=5)

Window Acknowledgement Size用于设置窗口确认大小,Acknowledgement是窗口确认消息。
会话开始时,双方都要先对端发送Window Acknowledgement Size,用于指明期望获得确认的大小。当一端收到内容大小超过Window Acknowledgement Size,就要像对方发送Acknowledgement。
1、会话开始计算收到byte个数的时间点是收到Window Acknowledgement Size消息开始。
2、byte size不包括tcp包头,应该是chunk的大小,即从tcp 的recv函数中获得的内容大小。
3、双方都要向对方发送Window Acknowledgement Size和Acknowledgement。
4、发送端发送完Window Acknowledgement Size消息后,没有收到Acknowledgement是不再发送进行步的消息的——这样会容易引起错误,导致再也发送不出消息了。

Acknowledgement (ID=3):
客户端或者服务器在接收到等同于窗口大小的字节之后必须要发送给对端一个确认。窗
口大小是指发送者在没有收到接收者确认之前发送的最大数量的字节。这个消息定义了序列
号,也就是目前接收到的字节数。
在这里插入图片描述
Window Acknowledgement Size (ID=5):
‘客户端或者服务器端发送这条消息来通知对端发送和应答之间的窗口大小。发送者在发
送完窗口大小字节之后期望对端的确认。接收端在上次确认发送后接收到的指示数值后,或
者会话建立之后尚未发送确认,必须发送一个确认。
在这里插入图片描述
对于拉流端,一般在收到av_createStream后,接着play,然后发送Acknowledgement 以让服务器继续发送数据。

3.4、Set Peer Bandwidth (ID=6)

限制对端的输出带宽。接受端接收到该消息后会通过设置消息中的Window ACK Size来限制已发送但未接受到反馈的消息的大小来限制发送端的发送带宽。如果消息中的Window ACK Size与上一次发送给发送端的size不同的话要回馈一个Window Acknowledgement Size的控制消息。
在这里插入图片描述
1、Hard(Limit Type=0):接受端应该将Window Ack Size设置为消息中的值。

2、Soft(Limit Type=1):接受端可以讲Window Ack Size设为消息中的值,也可以保存原来的值(前提是原来的Size小与该控制消息中的Window Ack Size)。

3、Dynamic(Limit Type=2):如果上次的Set Peer Bandwidth消息中的Limit Type为0,本次也按Hard处理,否则忽略本消息,不去设置Window Ack Size。

4、用户控制消息(User Control Message Events ID=4)

(用户控制消息,Message Type ID=4):告知对方执行该信息中包含的用户控制事件,比如Stream Begin事件告知对方流信息开始传输。和前面提到的协议控制信息(Protocol Control Message)不同,这是在RTMP协议层的,而不是在RTMP chunk流协议层的,这个很容易弄混。该信息在chunk流中发送时,Message Stream ID=0,Chunk Stream
Id=2,Message Type Id=4。

支持以下用户控制事件类型:
在这里插入图片描述

5、命令消息(Command Message Message ID=17或20)

(命令消息,Message Type ID=17或20):表示在客户端盒服务器间传递的在对端执行某些操作的命令消息,如connect表示连接对端,对端如果同意连接的话会记录发送端信息并返回连接成功消息,publish表示开始向对方推流,接受端接到命令后准备好接受对端发送的流信息,后面会对比较常见的Command Message具体介绍。当信息使用AMF0编码时,Message Type ID=20,AMF3编码时Message Type ID=17。

发送端发送时会带有命令的名字,如connect,TransactionID表示此次命
令的标识,Command Object表示相关参数。接受端收到命令后,会返回以下三种消息中的一种:
_result消息表示接受该命令,对端可以继续往下执行流程。
_error消息代表拒绝该命令要执行的操作。
method name消息代表要在之前命令的发送端执行的函数名称。这三种回应的消息都要带有收到的命令消息中的TransactionId来表示本次的回应作用于哪个命令。
可以认为发送命令消息的对象有两种:
一种是NetConnection,表示双端的上层连接。
一种是NetStream,表示流信息的传输通道,控制流信息的状态,如Play播放流,Pause暂停。

5.1、NetConnection Commands(连接层的命令)

用来管理双端之间的连接状态,同时也提供了异步远程方法调用(RPC)在对端执行某方法,以下是常见的连接层的命令:

5.1.1、connect:用于客户端向服务器发送连接请求

握手之后先发送一个connect 命令消息,这些信息是以AMF格式发送的,消息的结构如下:
在这里插入图片描述
第三个字段中的Command Object中会涉及到很多键值对,使用时可以参考协议的官方文档。

消息的回应有两种,_result表示接受连接,_error表示连接失败。
以下是连接命令对象中使用的名称-值对的描述:
在这里插入图片描述
在这里插入图片描述
命令执行时消息流动如下:
1、 客户端发送 connect 命令到服务器端以请求对服务器端应用实例的连接。
2、 收到 connect 命令后,服务器端发送协议消息 ‘窗口确认大小’ 到客户端。服务器端也会连接到 connect 命令中提到的应用。
3、 服务器端发送协议消息 ‘设置对端带宽’ 到客户端。
4、在处理完协议消息 ‘设置对端带宽’ 之后客户端发送协议消息 '窗口确认大小’到服务器端。
5、 服务器端发送另一个用户控制消息 (StreamBegin) 类型的协议消息到客户端。
6、 服务器端发送结果命令消息告知客户端连接状态 (success/fail)。这一命令定义了事务ID(常常为 connect 命令设置为 1)。这一消息也定义了一些属性,比如 FMS 服务器版本 (字符串)。之外,它还定义了݊其他连接关联到的信息,比如 level (字符串)、code (字符串)、 description (字符串)、objectencoding (数字) 等等。

5.1.2、Call:用于在对端执行某函数

即常说的RPC:远程进程调用,消息的结构如下:
在这里插入图片描述
如果消息中的TransactionID不为0的话,对端需要对该命令做出响应,响应的消息结构如下:
在这里插入图片描述

5.1.3、Create Stream:创建传递具体信息的通道

从而可以在这个流中传递具体信息,传输信息单元为Chunk。
当发送完createStream消息之后,解析服务器返回的消息会得到一个stream ID, 这个ID也就是以后和服务器通信的 message stream ID, 一般返回的是1,不固定。
在这里插入图片描述

5.2、NetStream Commands(流连接上的命令)

Netstream建立在NetConnection之上,通过NetConnection的createStream命令创建,用于传输具体的音频、视频等信息。在传输层协议之上只能连接一个NetConnection,但一个NetConnection可以建立多个NetStream来建立不同的流通道传输数据。

以下会列出一些常用的NetStream Commands,服务端收到命令后会通过onStatus的命令来响应客户端,表示当前NetStream的状态。

onStatus命令的消息结构如下:
在这里插入图片描述

5.2.1、play(播放)

由客户端向服务器发起请求从服务器端接受数据(如果传输的信息是视频的话就是请求开始播流),可以多次调用,这样本地就会形成一组数据流的接收者(创建一个播放列表)。注意其中有一个reset字段,表示是覆盖之前的播流(设为true)还是重新开始一路播放(设为false)。
play命令的结构如下:
在这里插入图片描述
在这里插入图片描述
命令执行时的消息流动:
1、 当客户端从服务器端接收到 createStream 命令的结果是为 success 时,发送play 命令。
2、 一旦接收到 play 命令,服务器端发送一个协议消息来设置块大小。
3、 服务器端发送另一个协议消息( 用户控制 ) , 这个消息中定义了
‘StreamIsRecorded’ 事件和流 ID。消息在前两个字节中保存事件类型,在后四个字节中保存流 ID。
4、服务器端发送另一个协议消息 (用户控制),这一消息包含 ‘StreamBegin’ 事件,来指示发送给客户端的流的起点。
5、 如果客户端发送的 play 命令成功,服务器端发送一个 onStatus 命令消息NetStream.Play.Start & NetStream.Play.Reset。只有当客户端发送的 play 命令设置了 reset 时,服务器端才会发送 NetStream.Play.Reset。如果要播放的流没有找到,服务器端发送 onStatus消息 NetStream.Play.StreamNotFound。
之后,服务器端发送视频和音频数据,客户端对݊其进行播放。

5.2.2、play2(播放)

和上面的play命令不同的是,play2命令可以在不改变播放内容时间轴的情况下切换到不同的比特率,服务器端会维护多种比特率的文件来供客户端使用play2命令来切换。
在这里插入图片描述
在这里插入图片描述

5.2.3、deleteStream(删除流)

用于客户端告知服务器端本地的某个流对象已被删除,不需要再传输此
路流。
在这里插入图片描述

5.2.4、 receiveAudio(接收音频)

NetStream 通过发送 receiveAudio 消息来通知服务器端是否发送音频到客户端。
receiveAudio命令结构如下:
在这里插入图片描述
如果发送来的 receiveAudio 命令布尔字段被设为 false 时服务器端不发送任何回复。
如果这一标识被设为 true, 服务器端发送状态消息NetStream.Seek.Notify 和
NetStream.Play.Start 进行回复。

5.2.5、 receiveVideo(接收视频)

NetStream 通过发送receiveVideo 消息来通知服务器端是否发送视频到客户端。
receiveVideo命令结构如下:
在这里插入图片描述
如果发送来的 receiveVideo 命令布尔字段被设为 false 时服务器端不发送任何回复。
如果这一标识被设为 true, 服务器端发送状态消息NetStream.Seek.Notify 和
NetStream.Play.Start 进行回复。

5.2.6、 publish(推送数据)

由客户端向服务器发起请求推流到服务器。
publish命令结构如下:
在这里插入图片描述

5.2.7、 seek(定位流的位置)

定位到视频或音频的某个位置,以毫秒为单位。
seek命令的结构如下:
在这里插入图片描述

5.2.8、pause(暂停)

客户端告知服务端停止或恢复播放。
pause命令的结构如下:
在这里插入图片描述
如果Pause为true即表示客户端请求暂停的话,服务端暂停对应的流会返回NetStream.Pause.Notify的onStatus命令来告知客户端当前流处于暂停的状态,当Pause为false时,服务端会返回NetStream.Unpause.Notify的命令来告知客户端当前流恢复。如果服务端对该命令响应失败,返回_error信息。

6、数据消息(Data Message ID=15或18)(也是对应flv中的script data的tag type = 18 )

(数据消息,Message Type ID=15或18):传递一些元数据(MetaData,比如视频名,分辨率等等)或者用户自定义的一些消息。当信息使用AMF0编码时,Message Type ID=18,AMF3编码时Message Type ID=15。

7、共享消息(Shared Object Message ID=16或19)

(共享消息,Message Type ID=16或19):表示一个Flash类型的对象,由键值对的集合组成,用于多客户端,多实例时使用。当信息使用AMF0编码时,Message Type ID=19,AMF3编码时Message Type ID=16。
每个消息可以包含有不同事件。
在这里插入图片描述
支持以下事件:
在这里插入图片描述

8、音频消息(Audio Message ID=8)(也是对应flv中的audio data的tag type = 8 )

(音频信息,Message Type ID=8):音频数据。

9、视频消息(Video Message ID=9)(也是对应flv中的video data的tag type = 9 )

(视频信息,Message Type ID=9):视频数据。

10、聚集消息(Aggregate Message ID=22)

(聚集信息,Message Type ID=22):多个RTMP子消息的集合。

11、推流流程

在这里插入图片描述

在这里插入图片描述

12、拉流流程

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/m0_37599645/article/details/116082210
今日推荐