一、RTMP 协议简介
RTMP(Real-Time Messaging Protocol),译为:实时消息传输协议,它是由 Adobe 公司提出的一种基于 TCP 的应用层协议,用来解决多媒体数据传输流的多路复用(Multiplexing)和分包(Packetizing)的问题。RTMP 在音视频相关的协议中的突出优点是连接可靠、低延时。RTMP 在两个对等的通信端之间通过可靠的传输协议提供双向的消息多路服务,用来传输带有时间信息的并行的视频、音频和数据。通常的协议的实现会给不同类型的消息赋予不同的优先级,当传输能力受到限制时它会影响消息下层流发送的队列顺序。由于协议设计对低延时、音视频同步等能力的良好支持,RTMP 是实时直播场景,尤其是在推流上行链路中,最常用的传输协议之一。
二、块流(RTMP Chunk Stream)
RTMP Chunk Stream 为配合 RTMP 协议而设计,主要为 RTMP 提供复用和分包的功能。每个消息包含时间戳和负载类型信息。RTMP Chunk Stream 为配合 RTMP 协议而设计, 它也可以服务于任何发送消息流的协议。 每个消息包含时间戳和负载类型信息。RTMP Chunk Stream 除自身内置的协议控制消息外, 还为上层协议提供了用户控制消息的机制。RTMP Chunk Stream 和 RTMP 适合各种音视频应用, 不管是一对一还是一对多场景都能很好的满足。 RTMP Chunk Stream 可以理解为是对传输 RTMP Chunk 的流的逻辑上的抽象,客户端和服务器之间有关 RTMP 的信息都在这个流上通信。
三、消息(RTMP Message)
1、RTMP 消息格式(RTMP Message Format)
RTMP 消息是 RTMP 协议中基本的数据单元。服务端和客户端通过在网络上发送消息实现之间的交互,消息包括但不限于音频、视频、数据。这里的消息是指满足该协议格式的、可以切分成 Chunk 发送的消息。RTMP 消息包含两部分:消息头(Message Header
)和有效数据(Message Body
)。消息头(Message Header
)包含以下信息:
Message Type:消息类型,占用 1 个字节。1~6 的消息类型 ID 是为协议控制消息保留的。
Length:有效负载的字节数,即音视频等信息的数据长度。占用 3 个字节,采用大端存储(big-endian)模式。
Timestamp:时间戳,占用 4 个字节,采用大端存储模式。
Message Stream ID:消息流 ID,标识消息所使用的流,采用大端存储模式。
2、分块(Chunking)
RTMP 在收发数据时并不是以 Message 为单位的,而是把 Message 拆分成 Message Chunk 发送,被拆分的每个 Message Chunk 都有一个唯一的 ID, 最后在接收端会按照 Message Chunk ID 将 Message Chunk 重新组装成 Message。在传输时必须在一个 Message Chunk 发送完成之后才能开始发送下一个。把数据量较大的 Message 拆分成较小的 Message Chunk,在传输时可以避免优先级低的消息持续发送阻塞优先级高的数据,比如 Message 中的数据会包括视频帧、音频帧和控制信息,如果持续发送音频数据或者控制数据的话可能就会造成视频帧的阻塞,然后就会造成看视频时最烦人的卡顿现象。同时对于数据量较小的 Message,可以通过对 Chunk Header 的字段来压缩信息,从而减少信息的传输量。
Message Chunk 的默认大小是 128 字节,在传输过程中,通过 Set Chunk Size 可以设置 Message Chunk 数据量的最大值,在发送端和接受端会各自维护一个 Chunk Size,可以分别设置这个值来改变自己这一方发送的 Chunk 的最大大小。大一点的 Message Chunk 减少了计算每个 Message Chunk 的时间从而减少了 CPU 的占用率,但是它会占用更多的时间在发送上,尤其是在低带宽的网络情况下,很可能会阻塞后面更重要信息的传输。小一点的 Message Chunk 可以减少这种阻塞问题,但小的 Message Chunk 会引入过多额外的信息(Chunk 中的 Header),少量多次的传输也可能会造成发送的间断导致不能充分利用高带宽的优势,因此并不适合在高比特率的流中传输。在实际发送时应对要发送的数据用不同的 Chunk Size 去尝试,通过抓包分析等手段得出合适的 Message Chunk 大小,并且在传输过程中可以根据当前的带宽信息和实际信息的大小动态调整 Message Chunk 的大小,从而尽量提高 CPU 的利用率并减少信息的阻塞机率。
3、RTMP 消息块格式(RTMP Message Chunk Format)
Message Chunk 由块头(Chunk Header
)和数据(Chunk Data
)组成,Chunk Header
包含 3 部分:基本头(Basic Header
)、消息头(Message Header
)和扩展时间戳(Extended Timestap
)。
3.1、基本头(Basic Header)
该部分编码Chunk Stream ID
(流通道 ID,简称 CSID
) 和 Chunk Type(下图中fmt
字段)。基本头的长度是 1~3 字节,采用小端存储模式,其长度取决于CSID
,CSID
是一个变长字段,用来唯一标识一个特定的流通道。Chunk Type 决定了消息头(Message Header
)的编码格式,长度固定占 2 位(bit)。在足够存储这两个字段的前提下应该用尽可能少的字节来表示,这样能够减少由于引入 Header 增加的数据量。
RTMP 最多支持 65597 个流,CSID
在3~65599
范围内。CSID
的 0
,1
,2
为保留值。0
表示块基本头为 2 个字节,并且CSID
范围在64~319
之间(第二个字节 + 64
);1
表示块基本头为 3 个字节,并且 ID 范围在64~65599
之间(第三个字节 * 256 + 第二个字节 + 64
);取值在 3~63 之间的 ID 表示完整的CSID
。值2
是为低版本协议保留的,用于协议控制消息和命令,第0~5
位(不重要的)表示CSID
。
当Basic Header
长度为 1 个字节时,CSID
占 6 位(6 位表示的范围为0~63
),由于CSID
的 0,1,2 为保留值,因此用户可以定义的CSID
范围为3~63
,此时的 CSID
是完整的,不需要计算得出:
当Basic Header
长度为 2 个字节时,CSID
占 14 位,此时 RTMP 将与 Chunk Type 所在字节的其他位都置为 0,剩下的 1 个字节来表示CSID - 64
,这样共有 8 位(8 位表示的范围为 0~255
)来存储CSID
,CSID
计算公式:CSID = 第二个字节的值 + 64
,因此计算得到CSID
范围是64~319
:
当Basic Header
长度为 3 个字节时,CSID
占 22 位,此时 RTMP 将与 Chunk Type 所在字节的其他位都置为 1,剩下的 16 位表示CSID - 64
,这样共有 16 位(16 位表示的范围为0~65535
)来存储CSID
,因此计算得到 CSID
范围是64~65599
。CSID
计算公式为:第三个字节值*255 + 第二个字节值 + 64.
。 64~319 范围内的SCID
可以用 2 字节长度来编码,也可以用 3 字节长度来编码。但实际实现时还是应该秉着最少字节的原则使用 2 个字节的表示方式来表示范围为64~319
的CSID
:
3.2、消息头(Message Header)
Message Header 共有 4 种不同的格式,Message Header
的格式和长度取决于 Basic Header 中的 Chunk Type(即fmt
字段值)。协议实现方应该用最紧凑的格式来表示块消息头。该部分编码所发送消息的描述信息(无论是整个消息还是一部分)。该部分的长度可能是 0 字节、3 字节、7 字节或者 11 字节,其长度取决于基本头中指定的 Chunk Type。
3.2.1、块类型 0(Type 0)
0 类型
的块消息头占 11 个字节长度。在 Chunk Stream 发送第一个 Chunk 时和Message Header
中的时间戳后退(即当前 Chunk 的时间戳小于上一个 Chunk 的时间戳,回退播放时会出现这种情况)时必须使用0 类型
的 Chunk。
timestamp(3 字节):0 类型的 Chunk 的绝对时间戳。 如果时间戳大于等于16777215(0xFFFFFF),该字段的值必须为16777215,即 3 个字节全部置为 1,此时实际的 timestamp 会转存到 Chunk 的Extended Timestamp
字段中,接受端在判断 timestamp 字段 24 个位都为 1 时就会去Extended Timestamp
中解析实际的时间戳。
message length(3 字节): 消息长度, 0 类型和 1 类型的 Chunk 包含此字段,表示消息长度,即当前 Chunk 所属消息的数据总长度,而非当前 Chunk 的数据长度。除了最后一个 Chunk,其他 Chunk 的数据长度大小都等于 Chunk Size 大小。
message type id(1 字节):消息类型 ID(数据的类型),如 8 代表音频数据、9 代表视频数据。
message stream id(4 字节):消息流 ID,表示该 Chunk 所在流的 ID,采用小端存储模式。通常,相同 Chunk Stream 中的消息属于用一个 Message Stream。虽然不同的 Message Stream 复用相同的 Chunk Stream 会导致Message Header
无法有效压缩,但是当一个 Message Stream 已关闭,才打开另外一个 Message Stream,就可以通过发送一个新的0 类型
Chunk 来实现复用。
3.2.2、块类型 1(Type 1)
1 类型
的块消息头占用 7 个字节长度,不包含message stream id
,表示和上一个 Chunk 的message stream id
是相同的。对于传输大小可变消息的流(如多数视频格式),在发送第一个消息之后的每个消息,第一个块都应该使用该类型格式。
timestamp delta(3字节): 时间戳增量。 1 类型
和2 类型
的 Chunk 包含此字段,存储的是当前 Chunk 的timestamp
和上一个 Chunk 的timestamp
差值。当它的值超过 3 个字节所能表示的最大值时,三个字节都置为 1,实际的时间戳差值就会转存到Extended Timestamp
字段中,接受端在判断timestamp delta
字段 24 个位都为 1 时就会去Extended timestamp
中解析实际的与上次时间戳的差值。
message length(3 字节): 同0 类型
的 message length
。
message type id(1 字节):同0 类型
的 message type id
。
3.2.3、块类型 2(Type 2)
2 类型
的块消息头占用 3 个字节长度,仅包timestamp delta
(同1 类型
的 message delta
),表示当前 Chunk 和上一次发送的 Chunk 的 message length
、 message type id
和message stream id
都相同。对于传输固定大小消息的流(如音频和数据格式),在发送第一个消息之后的每一个消息,第一个块都应该使用该类型格式。
3.2.4、块类型 3(Type 3)
3 类型
的 Chunk 占用 0 字节,也就是没有消息头,表示当前 Chunk 的 Message Header
和上一个 Chunk 的 Message Header
是相同的。
而当它跟在1 类型
或者2 类型
的 Chunk 后面时,表示和上一个 Chunk 的timestamp delta
是相同的。比如第一个0 类型
Chunk 的timestamp
=1000;第二个2 类型
Chunk 的timestamp delta
= 20,则时间戳为1000 + 20 = 1020;第三个3 类型
Chunk 的timestamp delta
= 20,则时间戳为 1020 + 20 = 1040;
下图中展示了上图中消息流分割成的块:
Chunks
当它跟在0 类型
的 Chunk 后面时,表示和上一个 Chunk 的时间戳是相同的。这种情况出现在一个 Message 被拆分成了多个 Chunk 时,当前 Chunk 和上一个 Chunk 同属于一个 Message 的情况下。如下图:
下图是被分割成的块:
Chunks
4、扩展时间戳(Extended Timestamp)
前面提到在0 类型
Chunk 的Message Header
中会有时间戳timestamp
,以及1 类型
和2 类型
Chunk 的Message Header
中有时间戳差timestamp delta
,当timestamp
或者timestamp delta
大于 3 个字节所能表示的最大数值16777215
(0xFFFFFF
)时,才会用到这个字段,否则这个字段值为 0
。扩展时间戳占 4 个字节,能表示的最大数值就是4294967295
(0xFFFFFFFF
)。当Extended Timestamp
启用时,timestamp
或者timestamp delta
要全置为1
,表示应该去扩展时间戳字段来提取真正的时间戳或者时间戳差。扩展时间戳存储的是完整值,而不是减去时间戳或者时间戳差的值。
三、协议控制消息(Protocol Control Messages)
RTMP Chunk Stream 用Message Type
为1
,2
,3
,5
和6
的 Message 来作为协议控制消息,这些 Message 包含 RTMP Chunk Stream Protocol 所需要的信息,所以协议控制消息是在 RTMP Chunk Stream Protocol 层的消息。在 Chunk Stream 中发送时,Message Stream ID = 0
,CSID = 2
。协议控制消息收到后立即生效,它们的时间戳信息是被忽略的。
1、设置块大小(Set Chunk Size,Message Type = 1)
用于设置Chunk Data
的最大长度 ,Chunk Size 默认是 128 字节(不能小于1字节,通常应该不低于 128 字节)。通信过程中可以通过发送该消息来设置Chunk Data
的大小,客户端或服务端均可以修改此值。例如,假设一个客户端想要发送 158 字节的音频数据,而最大块大小为 128 字节。在这种情况下,客户端可以向服务端发送该消息,通知它最大块大小被设置为了 158 字节,这样客户端只用一个块就可以发送这些音频数据,否则需要要将该消息拆分为Chunk Data
分别为128 字节和 30 字节的 2 个 Chunk 发送。以下为 Set Chunk Size 协议控制消息的Chunk Data
:
0:该位必须为0;
chunk size(31位): 该字段以字节形式保存新的最大块大小,该值将用于后续的所有块的发送,知道收到新的通知。该值可取值范围为1~2147483647
(0x7FFFFFFF
),由于Chunk size 不能大于 Message 的长度,所以超过 Message 的长度1677215
(0xFFFFFF
)的值是用不上的。
2、中止消息(Abort Message,Message Type = 2)
当一个 Message 被划分为多个 Chunk,接受端只接收到了部分 Chunk 时,发送该控制消息表示发送端不再传输同 Message 的 Chunk 了,接受端接收到这个消息后要丢弃已收到不完整的Chunk。该控制消息的Chunk Data
中只有一个CSID
字段(32 字节),表示丢弃所有已接收到的块流 ID 为CSID
的 Chunk。
3、确认消息(Acknowledgement,Message Type = 3)
当客户端或服务端收到对端的消息大小等于窗口大小(Window Size)的消息后要回馈一个 ACK 给发送端告知对端可以继续发送数据。窗口大小就是指在收到接受端返回的 ACK 前可以发送的最大字节数,返回的 ACK 中会带有从发送上一个 ACK 后接收到的总字节数。
Payload for the 'Acknowledgement' protocol message4、窗口大小确认(Window Acknowledgement Size,Message Type = 5)
客户端或服务端发送该消息来告知对端在两个 ACK 之间所使用的窗口大小。发送端发送窗口大小的数据后等待接收端发送 ACK。接收端在上一个 ACK 发送之后,接收到窗口大小的数据后必须发送 ACK,如果之前没有发送过 ACK 就从会话开始之后算起接收到窗口大小的数据后发送 ACK。
Payload for the 'Window Acknowledgement Size' protocol message5、设置对等带宽(Set Peer Bandwidth,Message Type = 6)
客户端或服务端发送该消息用来限制对端的输出带宽。对端收到该消息后通过将已发送但未收到的消息大小限制为该消息中的Acknowledgement Window size
来实现限制发送带宽。如果该消息中的Acknowledgement Window size
与上一次发送给发送端的不同的话发送端要回馈一个 Window Acknowledgement Size 消息。
Limit Type(限制类型)有以下选项:
0 - Hard:消息接收端应该将输出带宽限制为该消息中Acknowledgement Window size
指定的大小;
1 - Soft:消息接收端应该将输出带宽限制为该消息中Acknowledgement Window size
指定的大小或者当前窗口大小,限制为二者中的最小值。
2 - Dynamic:如果上次的 Set Peer Bandwidth 消息中的Limit Type
为0 - Hard
,本次也按0 - Hard
处理,否则忽略本消息。
四、RTMP 消息类型(Types of Messages)
1、命令消息(Command Message,Message Type = 20,Message Type = 17)
用于客户端和服务器间传递在对端执行某些操作的命令消息,比如发送这些消息来完成连接、创建流、发布、播放、暂停等操作,以及通知发送者命令请求状态和结果等等。当消息使用AMF0
编码时,Message Type = 20,使用AMF3
编码时 Message Type = 17。
命令消息中包含Command Name(命令名称)、transaction ID(命令标识)、command object(相关参数)。接受端收到命令消息后,会返回发送端以下三种消息中的一种:_result
响应消息表示接受该命令,对端可以继续往下执行流程;_error
响应消息代表拒绝该命令要执行的操作;method name
响应消息代表要在命令消息的发送端执行的函数名称。响应消息都要带有当前收到的命令消息中的 transaction ID 来表示接收端本次回应的是哪个命令消息。
1.1、连接层命令(NetConnection Commands)
连接层命令用于管理客户端和服务端之间的链接状态。同时也提供了异步远程方法调用(RPC)在对端执行某方法,以下是常见的连接层的命令:
1.1.1、connect
用于客户端向服务器发送链接请求。客户端发送到服务端的消息结构如下:
Command Name:命令名称,当前命令设置为 "connect";
Transaction ID:对于连接请求该字段恒为 1;
Command Object:命令参数,用键值对集合表示(可参考官方文档 7.2.1.1 章节);
Optional User:用户自定义的额外信息;
执行 connect 命令时的消息流顺序:
1、客户端向服务端发送 connect 命令消息请求建立连接;
2、服务端收到 connect 命令消息后向客户端发送 Window Acknowledgement Size(窗口大小确认)消息,并且服务端连接 connect 命令消息提及的应用;
3、服务端向客户端发送 Set Peer Bandwidth(设置对等带宽)消息;
4、客户端收到 Set Peer Bandwidth 消息后回馈 Window Acknowledgement Size 消息给服务端;
5、服务端向客户端发送 StreamBegin 消息;
6、服务端向客户端发送transaction ID = 1
的回应消息给客户端,服务端消息的回应有两种:_result
表示接受连接,_error
表示连接失败。
1.1.2、call
用于远程在对端执行某函数(即 RPC:远程进程调用)。消息的结构如下:
Procedure Name:要调用的远程进程名称;
Transaction ID:如果想要对端发送响应消息,需要设置为非 0 值,否则置为 0;
Command Object:命令参数;
Optional Arguments:用户自定义参数;
如果消息的Transaction ID
不为 0 的话,对端需要对该命令做出响应,响应的消息结构如下:
Command Name:命令名称;
Transaction ID:接收到的命令消息中的Transaction ID
;
Command Object:命令参数;
Response:调用方法的响应;
1.1.3、createStream
客户端发给服务端此命令消息创建传输消息的通道,从而可以在这个流中传输音频、视频或者元数据等,传输信息单元为 Chunk。
客户端发给服务端消息的结构:
Command Name:命令名称,当前命令设置为 "createStream";
Transaction ID:消息标识;
Command Object:命令参数;
服务端回馈给客户端消息结构:
Command Name:命令名称,当前命令设置为 "createStream";
Transaction ID:与客户端发送到服务端消息的Transaction ID
相同;
Command Object:命令参数;
Stream ID:返回 Stream ID 或者错误信息对象。
1.2、流连接层命令(NetStream Commands)
NetStream 建立在 NetConnection 之上,用于定义传输音频和视频等信息的通道。在传输层协议之上只能连接一个 NetConnection,但一个 NetConnection 可以建立多个 NetStream 来建立不同的流通道传输数据。服务端收到命令后会通过 onStatus 命令来响应客户端,表示当前 NetStream 的状态。onStatus 命令消息结构如下:
2、数据消息(Data Message,Message Type = 18,Message Type = 15)
传递一些 MetaData(元数据,比如主题、创建时间和时长等等)或者用户自定义的一些消息。当消息使用AMF0
编码时,Message Type = 18,使用AMF3
编码时 Message Type = 15。
3、共享消息(Shared Object Message,Message Type = 19,Message Type = 16)
表示一个 Flash 类型的对象(由键值对的集合组成),用于多客户端,多实例进行同步时使用。每条消息可包含多个事件。当消息使用AMF0
编码时,Message Type = 19,使用AMF3
编码时 Message Type = 16。
4、音频消息(Audio Message,Message Type = 8)
客户端或服务端通过发送此消息来发送音频数据给对方。
5、视频消息(Video Message,Message Type = 9)
客户端或服务端通过发送此消息来发送视频数据给对方。
6、组合消息(Aggregate Message,Message Type = 22)
一个组合消息消息包含多个子 RTMP 消息。
The Aggregate Message body format7、用户控制消息(User Control Messages,Message Type = 4)
RTMP 协议将Message Type
为4
的消息作为了用户控制消息,在 Chunk Stream 中发送时,Message Stream ID = 0
,CSID = 2
。接收端收到用户控制消息后立即生效,用户控制消息的时间戳信息是被忽略的。和前面提到的协议控制消息不同,用户控制消息是在 RTMP Protocol 层的,而不是在 RTMP Chunk Stream Protocol 层的。
客户端或服务端通过发送该消息告知对端用户控制事件。该消息携带事件类型和事件数据两部分。开头的 2 个字节用于指定事件类型,后面紧跟着是事件数据。事件数据字段长度可变,使用 RTMP Chunk Stream 传输时,最大块大小要足够大,以便可以使用一个单独的 Chunk 进行传输用户控制消息。
Payload for the 'User Control' protocol message用户控制消息支持以下事件:
0(流开始):服务端发送该事件,用来通知客户端一个流已经可以用来传输消息了。默认情况下,该事件是在收到客户端连接指令并成功处理后发送的第一个事件。该事件的Event Data
使用 4 个字节来表示可用的message stream id
。
1(流结束):服务端发送该事件,用来通知客户端其在流中请求的回放数据已经结束了。如果没有额外的指令,将不会再发送任何数据,而客户端会丢弃之后从该流接收到的消息。该事件的Event Data
使用 4 个字节来表示回放完成的message stream id
。
2(流枯竭):服务端发送该事件,用来通知客户端流中没有更多数据了。如果服务端在一定时间后没有探测到更多数据,它就可以通知所有订阅该流的客户端,流已经枯竭。该事件的Event Data
使用 4 个字节来表示枯竭的message stream id
。
3(设置缓冲区大小):客户端发送该事件,用来告知服务端缓冲区大小(单位毫秒)。该事件在服务端开始处理流数据之前发送。Event Data
中,前 4 个字节表示message stream id
,后 4 字节表示缓冲区大小(单位毫秒)。
4(流已录制):服务端发送该事件,用来通知客户端该流是一个录制流。事件数据用4个字节表示录制的message stream id
。
6(ping请求):服务端发送该事件,用来探测客户端是否处于可达状态。Event Data
里携带的是一个 4 字节长度的时间戳,表示服务端分发该事件时的服务器本地时间。客户端收到后用 ping 请求后应该回复服务端。
7(ping响应):客户端用该事件响应服务端的 ping 请求,Event Data
为收到的 ping 请求中携带的时间戳,长度 4 字节。
五、握手(Handshake)
参考链接:
RTMP 原版协议
网友评论