RTMP(Real Time Messaging Protocol)实时消息传输协议是Adobe公司提出得一种媒体流传输协议,其提供了一个双向得通道消息服务,意图在通信端之间传递带有时间信息得视频、音频和数据消息流,其通过对不同类型得消息分配不同得优先级,进而在网传能力限制下确定各种消息得传输次序。RTMP最早是Adobe公司基于flash player播放器提出得一种音视频封装传输格式,在前期flash盛行时,得到了极其广泛得应用,当前flash基本被废弃,但是RTMP这种协议作为流媒体封装传输得方式,并没有预想中被冷落得情况,相反,在当下直播盛行得阶段,RTMP被经常用来向云端推流得流媒体协议。
本文主要介绍RTMP得基本交互过程及在流媒体传输中得协议示例详解,如需要了解更多有关RTMP得内容,请查看RTMP协议规范《rtmp_specification_1.0》,协议文档及中文翻译可关注公众号壹零仓,发送RTMP,获取规范文档。
RTMP是TCP/IP协议模型中的应用层协议,其工作在TCP之上,默认端口为1935,RTMP协议是基于TCP协议进行传输,因此其需要TCP特性来保证消息传输的可靠性,TCP通过三次握手成功建立连接后,RTMP协议还需要客户端和服务端通过RTMP握手协议来建立RTMP Connection,RTMP握手协议主要目的是协商RTMP版本及时间对齐作用。RTMP Connection上会传输RTMP控制信息,比SetChunkSize,SetACKWindowSize,CreateStream等,其中CreateStream命令会创建一个Stream链接,用于传输具体的音视频数据和控制这些信息传输的命令信息。RTMP协议以RTMP Message格式传输,为了更好地实现多路复用、分包和信息的公平性,发送端把Message划分为带有MessageID的Chunk,每个Chunk可能是一个单独的Message,也可能是Message的一部分,在接受端会根据chunk中包含的data的长度,messageid和message的长度把chunk还原成完整的Message,从而实现信息的收发。
RTMP在建立好传输层TCP连接后,通过RTMP握手协议来完成RTMP的连接,RTMP握手协议由三个固定长度的块组成,客户端和服务端各发送相同的三个块,客户端发送C0、C1、C2,服务端发送S0、S1、S2,RTMP规范中没有详细规定各个块发送的顺序,只需要满足如下条件即可:
其握手示意图如下图所示:
客户端发送C0C1之前客户端和服务器都处于未初始化状态;在未初始化状态之后客户端和服务端都进入版本已发送状态,客户端等待接收 S1 包,服务端等待接收 C1 包,收到所等待的包后,客户端发送 C2 包,服务端发送 S2 包。之后状态进入发送确认状态;客户端和服务端等待接收S2和C2包,收到后进入握手完成状态,客户端和服务端开始交换消息。
从规范上看只要满足以上条件,如何发送6个块的顺序都是可以的,但实际实现中为了在保证握手的身份验证功能的基础上尽量减少通信的次数,一般的发送顺序如下:
握手协议格式解析:
时间戳(Time 4bytes):时间戳,用于C/S发送所有后续块的时间起点,可以从0开始,或者其他值,主要用于多路流传输的时间同步
零值 (Zero 4bytes):规范中说必须为0,实际传输协议中并未对此进行校验,没啥意义,不为零也可正常传输。
随机数据 (Random data 1528bytes):为随机数序列,用户区分出其响应C2/S2来自此RTMP连接发起的握手还是其他方发起的握手。
在构建RTMP代码时一般只针对版本号进行校验,时间戳对其机随机数对其等校验不严格,因为当前很多RTMP流媒体协议并没那么规范,如果严格校验,兼容性会差。
RTMP 传输的数据称为Message,Message包含音视频数据和信令,传输时不是以Message为单位的,而是把Message拆分成Chunk发送,而且必须在一个Chunk发送完成之后才能开始发送下一个Chunk,每个Chunk中带有msg stream id代表属于哪个Message,接受端也会按照这个id来将chunk组装成Message。每个Chunk的默认大小是 128 字节,可以通过Set Chunk Size的控制信息设置Chunk数据量的最大值,在发送端和接受端会各自维护一个Chunk Size,可以分别设置这个值来改变自己这一方发送的Chunk的最大值,其配置大小多少合适,需要我们去根据性能要求来调试合适的大小
Chunk格式包含基本头、消息头、扩展时间戳和负载,如下图所示:
Basic Header(基本的头信息):长度可能是1,2,或3个字节,包含了chunk stream ID(流通道Id,CSID)和chunk type(chunk的类型,fmt),CSID用来唯一标识一个特定的流通道,同一个Chunk Stream ID必然属于同一个信道,chunk type决定了后面Message Header的格式。chunk type占最开始2bits,CSID的长度时可变的,其决定了基本头的长度,在足够表征流通道的前提下,最好用尽量少的字节来表示CSID,从而减少由于引入Header增加的数据量。
RTMP最多可支持65597个流,CSID范围在3-65599 内,CSID辅助 0,1,2为保留值,其中CSID=0表示块基本头为2个字节,并且CSID范围在64-319 之间(第二个字节+64);CSID=1 表示块基本头为3个字节,并且ID范围在64-65599之间(第三个字节*256 + 第二个字节 + 64);3-63 范围内的值表示整个流ID;CSID=2 是为低版本协议保留的,用于协议控制消息和命令。
Message Header(消息头信息):包含了要发送的实际信息(可能是完整的,也可能是一部分)的描述信息。Message Header的格式和长度取决于Basic Header的chunk type(fmt)取值,共有4种不同的格式,由上fmt字段控制。其中第一种格式可以表示其他三种表示的所有数据,但由于其他三种格式是基于对之前chunk的差量化的表示,因此可以更简洁地表示相同的数据,实际使用的时候还是应该采用尽量少的字节表示相同意义的数据,以下按照字节数从多到少的顺序分别介绍这4种格式的。
(1)fmt=0,类型0的chunk消息头长度是11个字节,类型0必须用在块流的开头位置,或者每次当块流的时间戳后退的时候(例如向后拖动的操作),其格式如下:
timestamp(3bytes):最多能表示到16777215=0xFFFFF,如果时间戳大于或等于16777215(0xFFFFFF),该字段值必须为16777215,并且必须设置扩展时间戳Extended Timestamp来一起表示32位的时间戳,否则该字段就是完整的时间戳。接受端在判断timestamp值为0xFFFFFF时就会去Extended timestamp中解析实际的时间戳。
message length(消息数据的长度):占用3个字节,表示实际发送的消息的数据,如音频帧、视频帧等数据的长度,注意这里是Message的长度,也就是chunk属于的Message的总数据长度,而不是chunk本身Data的数据的长度。
message type id(消息的类型id):占用1个字节,表示实际发送的数据的类型,如8代表音频数据、9代表视频数据。
msg stream id(msid):占用4个字节,表示该chunk所在的流的ID,它采用小端存储的方式。
(2)fmt=1,Message Header占用7个字节,省去了表示msg stream id的4个字节,表示此chunk和上一次发的chunk所在的流相同,如果在发送端只和对端有一个流链接的时候可以尽量去采取这种格式,其格式如下:
timestamp delta:占用3个字节,这里和type=0时不同,表示上一个chunk的时间差,当它的值超过3个字节所能表示的最大值时,设置为0xFFFFFF,实际的时间戳差值就会转存到Extended Timestamp字段中,接受端在判断timestamp delta字段24个位都为1时就会去Extended timestamp中解析时机的与上次时间戳的差值。
(3)fmt=2,Message Header占用3个字节,相对于type=1格式又省去了表示消息长度的3个字节和表示消息类型的1个字节,表示此chunk和上一次发送的chunk所在的流、消息的长度和消息的类型都相同,格式如下:
(4)fmt=3,它表示这个chunk的Message Header和上一个是完全相同的,不存在消息头,当它跟在Type=0的chunk后面时,表示和前一个chunk的时间戳都是相同的,就是一个Message拆分成了多个chunk,这个chunk和上一个chunk同属于一个Message;当它跟在Type=1或者Type=2的chunk后面时,表示和前一个chunk的时间戳的差是相同的。比如第一个chunk的Type=0,timestamp=3600,第二个chunk的Type=2,timestamp delta=3600,表示时间戳为3600+3600,第三个chunk的Type=3,表示timestamp delta=3600,时间戳为3600+3600+3600
Extended Timestamp(扩展时间戳):扩展时间戳用来辅助编码超过16777215(0xFFFFFF)的时间戳或时间戳增量。当类型0,1或2的块,无法用24位字段来表示时间戳或时间戳增量时就可以启用扩展时间戳,同时类型0块的时间戳字段或类型1,2的时间戳增量字段值应该设为16777215(0xFFFFFF)。当类型3块最近的属于相同块流ID的类型0块、类型1块或类型2块有此字段时,该类型3块也应该有此字段。
Chunk Data(块数据):用户层面上真正想要发送的与协议无关的数据,长度在[0,chunkSize]之间
在RTMP的chunk会用一些特殊的值来代表协议的控制消息,控制信息的Message Stream ID必须为0(代表控制流信息),CSID必须为2,Message Type ID可以为1/2/3/5/6,控制消息的接受端会忽略掉chunk中的时间戳,收到后立即生效
虽然RTMP被设计成使用RTMP块流传输,但是它也可以使用其他传输协议来发送消息。RTMP块流协议和RTMP协议配合时,非常适合音视频应用,包括一对一和一对多实时直播、视频点播和视频互动会议等。
RTMP消息有两部分,消息头和有效负载,
这里注意RTMP消息的头(RTMP Message Header,不是chunk头中的 Message Header,两个不是同一个东西)有自己的统一格式,当然这部分也是会被切割到 Chunk 里传输的,不过,因为实际意义和 Chunk Header 内容重复,当前主流流媒体服务器在发送RTMP消息时,chunk data中不包含RTMP Message Header,只要双方约定好即可。
主要RTMP消息类型如下,其详细介绍可参照规范,RTMP协议规范《rtmp_specification_1.0》,这里不做详细描述。
前文已经介绍了RTMP传输的单位不是massage,而是把massage拆分成一个或多个chunk来进行传输,可根据msg stream id判断是否属于同一个Massage,其拆分过程如下:
这里采用通用的做法,RTMP Message Header不拆分到chunk data中,虽然规范上RTMP massage应该作为一个整体被拆分成chunk,但是由于RTMP massage header与chunk massage header信息重复,本着最小传输数据原则,一般做法是在chunk data中去掉此信息。
RTMP流媒体需要支持视频源端(视频发布端)通过rtmp推送采集的视频流,同时也要支持播放客户端通过RTMP地址拉流播放,这里详细讲解RTMP推流和拉流的过程。
其上为官方规范文档中描述的流程,实际过程可能稍有不同,发送端和接收端主要对创建流、发布、和数据传输的消息比较关注,解析时一般按照规范顺序和格式解析,其他消息发送顺序并无特殊规定,消息较小时,可一次发送多个RTMP消息。
命令消息/数据消息/共享消息,最常用的消息封装格式为AMF0~AMF3,示例中也是此方式编码,具体编码格式详解参照:
rtmp数据封装二-AMF
音视频数据的消息格式,需要按照规范中flv tag body的格式进行封装,其具体封装格式详解,有点复杂后续单独开一篇文章介绍
示例的抓包数据文件,可关注公众号壹零仓,发送视频流分析,获取抓包文件
通过本文对协议规范的说明,与实际抓包示例的真实过程比较,可以看出,真实rtmp很多地方并没有与规范匹配,所以在进行rtmp音视频解析时,尽量只校验自己需要的数据,其他消息格式放宽校验,增加兼容性,有关AMF格式,很简单,可参照规范,有关FLV封装音视频的方式,后续文章中再介绍