文章目录
流媒体协议之RTMP详解
1 RTMP概述
2 RTMP交互过程
2.1 握手协议
2.2 RTMP分块(chunk)
2.3 协议控制消息(Protocol Control Message)
2.4 RTMP Message Format
2.5 不同类型的RTMP Message
2.6 RTMP Massage和chunk之间关系
3 RTMP流媒体传输详解
3.1 RTMP 推流
3.2 RTMP 拉流
4 结束语
1 RTMP概述
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,从而实现信息的收发。
2 RTMP交互过程
2.1 握手协议
RTMP在建立好传输层TCP连接后,通过RTMP握手协议来完成RTMP的连接,RTMP握手协议由三个固定长度的块组成,客户端和服务端各发送相同的三个块,客户端发送C0、C1、C2,服务端发送S0、S1、S2,RTMP规范中没有详细规定各个块发送的顺序,只需要满足如下条件即可:
握手以客户端发送 C0 和 C1 块开始
客户端必须等待接收到 S1 才能发送 C2
客户端必须等待接收到 S2 才能发送任何其他数据
服务器必须等待接收到 C0 才能发送S0和S1,也可能是接收到 C1 后发送
服务器必须等待接收到 C1 才能发送S2
服务器必须等待接收到 C2 才能发送其他数据
其握手示意图如下图所示:
客户端发送C0C1之前客户端和服务器都处于未初始化状态;在未初始化状态之后客户端和服务端都进入版本已发送状态,客户端等待接收 S1 包,服务端等待接收 C1 包,收到所等待的包后,客户端发送 C2 包,服务端发送 S2 包。之后状态进入发送确认状态;客户端和服务端等待接收S2和C2包,收到后进入握手完成状态,客户端和服务端开始交换消息。
从规范上看只要满足以上条件,如何发送6个块的顺序都是可以的,但实际实现中为了在保证握手的身份验证功能的基础上尽量减少通信的次数,一般的发送顺序如下:
客户端向服务端同时发送C0+C1
服务端确认版本号后,向客户端同时发送S0+S1+S2
客户端接收到S2后发送C2到服务端
握手过程真实交互抓包截图如下:
握手协议格式解析:
C0/S0格式:C0和S0包都是单一的一个字节(8位),表示版本号,C0包中表示客户端请求的RTMP版本,在S0中表示服务器选择的RTMP版本,规范最新定义为3,如果服务器和客户端版本号不一致,则可能会终止交互或者降级。
C1/S1格式:C1/S1长度为1536字节,其格式如下:
时间戳(Time 4bytes):时间戳,用于C/S发送所有后续块的时间起点,可以从0开始,或者其他值,主要用于多路流传输的时间同步
零值 (Zero 4bytes):规范中说必须为0,实际传输协议中并未对此进行校验,没啥意义,不为零也可正常传输。
随机数据 (Random data 1528bytes):为随机数序列,用户区分出其响应C2/S2来自此RTMP连接发起的握手还是其他方发起的握手。
C2/S2格式:长度为1536字节,其格式如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oHuEEkt8-1673347248530)(null)]
时间戳(Time 4bytes):必须是对方C1/S1发来的时间戳(对C2来说是S1的时间戳,对S2来说是C1的时间戳)。
时间戳2(Time2 4bytes):必须是前面自己发送的C/S包里的时间戳。
随机数据回显(Random echo 1528bytes):必须是对方发来的C1/S1包里携带的随机数据(对C2来说是S1,对S2来说是C1)
在构建RTMP代码时一般只针对版本号进行校验,时间戳对其机随机数对其等校验不严格,因为当前很多RTMP流媒体协议并没那么规范,如果严格校验,兼容性会差。
2.2 RTMP分块(chunk)
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]之间
2.3 协议控制消息(Protocol Control Message)
在RTMP的chunk会用一些特殊的值来代表协议的控制消息,控制信息的Message Stream ID必须为0(代表控制流信息),CSID必须为2,Message Type ID可以为1/2/3/5/6,控制消息的接受端会忽略掉chunk中的时间戳,收到后立即生效
Set Chunk Size(Message Type ID=1):设置chunk中Data字段所能承载的最大字节数,默认为128bytes,通信过程中可以通过发送该消息来设置chunk Size的大小(不得小于128bytes),该值将作用于后续的所有块的发送,直到收到新的通知,而且通信双方会各自维护一个chunkSize,两端的chunkSize是独立的。其chunk data格式如下:
其中第一位必须为0,chunk Size占31个位,最大可配置为2147483647=0x7FFFFFFF,但实际上所有大于16777215=0xFFFFFF的值都用不上,因为chunk size不能大于Message的长度,表示Message的长度字段是用3个字节表示的,最大只能为0xFFFFFF
Abort Message(Message Type ID=2):当一个Message被切分为多个chunk,接受端只接收到了部分chunk时,发送该控制消息表示发送端不再传输同Message的chunk,接受端接收到这个消息后要丢弃这些不完整的chunk。
Data数据中只需要一个CSID,表示丢弃该CSID的所有已接收到的chunk。
Acknowledgement(Message Type ID=3):当收到对端的消息大小等于窗口大小(Window Size)时接受端要回馈一个ACK给发送端告知对方可以继续发送数据。窗口大小就是指收到接受端返回的ACK前最多可以发送的字节数量,返回的ACK中会带有从发送上一个ACK后接收到的字节数。
Window Acknowledgement Size(Message Type ID=5):发送端在接收到接受端返回的两个ACK间最多可以发送的字节数,客户端或服务端发送该消息来通知对方发送确认消息(ACK)所使用的窗口大小,并等待对方发送回确认消息(ACK),对方(接收端)在接收到窗口大小确认信息后必须发送确认消息(ACK)。
Set Peer Bandwidth(Message Type ID=6):限制对端的输出带宽。接受端接收到该消息后会通过设置消息中的Window ACK Size来限制已发送但未接受到反馈的消息的大小来限制发送端的发送带宽。如果消息中的Window ACK Size与上一次发送给发送端的size不同的话要回馈一个Window Acknowledgement Size的控制消息。
Hard(Limit Type=0):接受端应该将Window Ack Size设置为消息中的值
Soft(Limit Type=1):接受端可以讲Window Ack Size设为消息中的值,原有值如果小于此值,也可以保存原来的值,
Dynamic(Limit Type=2):如果上次的Set Peer Bandwidth消息中的Limit Type为0,本次也按Hard处理,否则忽略本消息,不去设置Window Ack Size。
2.4 RTMP Message Format
虽然RTMP被设计成使用RTMP块流传输,但是它也可以使用其他传输协议来发送消息。RTMP块流协议和RTMP协议配合时,非常适合音视频应用,包括一对一和一对多实时直播、视频点播和视频互动会议等。
RTMP消息有两部分,消息头和有效负载,
消息头(Message Header):格式如下:
消息头包含以下信息:
Message Type:消息类型,1个字节。消息类型ID为 1 - 6 的是为协议控制消息保留的。
Payload Length:有效负载的字节数(长度),3个字节。该字段是用大字节序(big-endian)表示的。
Timestamp:时间戳,4个字节,用大字节序(big-endian)表示。
Message Stream ID:消息流ID,标识消息所使用的流,用大字节序(big-endian)表示。
消息有效负载(Message Payload):消息的另一部分就是有效负载,也是消息包含的实际数据,比如说音频样本或者压缩的视频数据。有效负载的格式不再本文档的讨论范围之内.
这里注意RTMP消息的头(RTMP Message Header,不是chunk头中的 Message Header,两个不是同一个东西)有自己的统一格式,当然这部分也是会被切割到 Chunk 里传输的,不过,因为实际意义和 Chunk Header 内容重复,当前主流流媒体服务器在发送RTMP消息时,chunk data中不包含RTMP Message Header,只要双方约定好即可。
2.5 不同类型的RTMP Message
主要RTMP消息类型如下,其详细介绍可参照规范,RTMP协议规范《rtmp_specification_1.0》,这里不做详细描述。
Command Message(命令消息,Message Type ID=17或20):表示在客户端盒服务器间传递的在对端执行某些操作的命令消息,如connect表示连接对端,对端如果同意连接的话会记录发送端信息并返回连接成功消息,publish表示开始向对方推流,接受端接到命令后准备好接受对端发送的流信息,后面会对比较常见的Command Message具体介绍。当信息使用AMF0编码时,Message Type ID=20,AMF3编码时Message Type ID=17.
Data Message(数据消息,Message Type ID=15或18):传递一些元数据(MetaData,比如视频名,分辨率等等)或者用户自定义的一些消息。当信息使用AMF0编码时,Message Type ID=18,AMF3编码时Message Type ID=15.
Shared Object Message(共享消息,Message Type ID=16或19):表示一个Flash类型的对象,由键值对的集合组成,用于多客户端,多实例时使用。当信息使用AMF0编码时,Message Type ID=19,AMF3编码时Message Type ID=16.
Audio Message(音频信息,Message Type ID=8):音频数据。
Video Message(视频信息,Message Type ID=9):视频数据。
Aggregate Message (聚集信息,Message Type ID=22):多个RTMP子消息的集合
User Control Message Events(用户控制消息,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。
2.6 RTMP Massage和chunk之间关系
前文已经介绍了RTMP传输的单位不是massage,而是把massage拆分成一个或多个chunk来进行传输,可根据msg stream id判断是否属于同一个Massage,其拆分过程如下:
这里采用通用的做法,RTMP Message Header不拆分到chunk data中,虽然规范上RTMP massage应该作为一个整体被拆分成chunk,但是由于RTMP massage header与chunk massage header信息重复,本着最小传输数据原则,一般做法是在chunk data中去掉此信息。
3 RTMP流媒体传输详解
RTMP流媒体需要支持视频源端(视频发布端)通过rtmp推送采集的视频流,同时也要支持播放客户端通过RTMP地址拉流播放,这里详细讲解RTMP推流和拉流的过程。
3.1 RTMP 推流
RTMP推流流程如下图所示:
首先由发布客户端发起握手协议,handshaking done
发布端向服务器发送连接请求消息 Command Message(connect)
服务端接收到连接命令后,发送窗口应答大小确认信息(Window Acknowledgement Size),配置对端带宽(Set Peer Bandwidth), 发送用户控制协议(Stream Begin)告知流开始信息,并发送连接接收响应信息(_result-connect response)
发布端发起创建流通道(createstream)
服务器接收到创建流通到后,响应创建流(_result-creatStream response)
发布端发起发布命令消息(public)并准备开始传输元数据消息(Metadata)、音频数据(AUdio data)
服务端接收到发布命令后,发送响应消息
发送端配置chunk size、开始发送视频数据
服务端返回发布结果信息,开始接收音视频流
其上为官方规范文档中描述的流程,实际过程可能稍有不同,发送端和接收端主要对创建流、发布、和数据传输的消息比较关注,解析时一般按照规范顺序和格式解析,其他消息发送顺序并无特殊规定,消息较小时,可一次发送多个RTMP消息。
真实的wireshark抓包数据如下:
命令消息/数据消息/共享消息,最常用的消息封装格式为AMF0~AMF3,示例中也是此方式编码,具体编码格式详解参照:
rtmp数据封装二-AMF
音视频数据的消息格式,需要按照规范中flv tag body的格式进行封装,其具体封装格式详解,有点复杂后续单独开一篇文章介绍
示例的抓包数据文件,可关注公众号壹零仓,发送视频流分析,获取抓包文件
3.2 RTMP 拉流
官方规范中给出的RTMP拉流流程如下图:
首先和推流一样,由客户端发起握手协议,发送创建流命令
服务器端接收创建流命令后,发送响应命令
客户端发送命令消息(play)
服务器端接收到播放命令play后,配置chunk大小,发送用户控制协议(StreamIsRecorded、StreamBegin)通知是否录制流,流已开启标志,之后发送播放命令响应消息(刷新当前状态、通知播放开始),这里如果play命令成功,服务端回复onStatus 命令消息 NetStream.Play.Start和NetStream.Play.Reset,其中NetStream.Play.Reset只有当客户端发送的play命令里设置了reset时才会发送,如果要播放的流没有找到,服务端会发送onStatus消息NetStream.Play.StreamNotFound。
服务器端发送音视频消息到客户端,客户端开始播放
wireshark抓包示例及标注如下:
4 结束语
通过本文对协议规范的说明,与实际抓包示例的真实过程比较,可以看出,真实rtmp很多地方并没有与规范匹配,所以在进行rtmp音视频解析时,尽量只校验自己需要的数据,其他消息格式放宽校验,增加兼容性,有关AMF格式,很简单,可参照规范,有关FLV封装音视频的方式,后续文章中再介绍