1. Briefing
RTMP协议是Real Time Message Protocol(实时信息传输协议)的缩写,它是由Adobe公司提出的一种应用层的协议,用来解决多媒体数据传输流的多路复用(Multiplexing)和分包(packetizing)的问题。随着VR技术的发展,视频直播等领域逐渐活跃起来,RTMP作为业内广泛使用的协议也重新被相关开发者重视起来。
RTMP协议是应用层协议,是要靠底层可靠的传输层协议(通常是TCP,默认通信端口1935)来保证信息传输的可靠性的。
RTMP URL格式:rtmp://ip:[port]/appName/streamName
例如: rtmp://192.168.178.218:1935/live/devzhaoyou
在基于传输层协议的链接建立完成后,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协议的流媒体推流需要经过以下几个步骤:握手,建立连接,建立流,推流。
2. Data Structure
Message
Message Type
它是一个消息类型的ID,通过该ID接收方可以判断接收到的数据的类型,从而做相应的处理。
Message Type ID在1-7的消息用于协议控制,这些消息一般是RTMP协议自身管理要使用的消息,用户一般情况下无需操作其中的数据。
Message Type ID为8,9的消息分别用于传输音频和视频数据。
Message Type ID为15-20的消息用于发送AMF编码的命令,负责用户与服务器之间的交互,比如播放,暂停等。Playload Length
Message Payload消息负载的长度,即音视频等信息的的数据长度,4个字节。TimeStamp
时间戳,3个字节,单位是毫秒ms。因此它最多能表示到16777215=0xFFFFFF=2^24-1,当它的值超过这个最大值时,这三个字节都置为1,实际的timestamp会转存到Extended Timestamp字段中,接受端在判断timestamp字段24个位都为1时就会去Extended timestamp中解析实际的时间戳。Stream ID
消息的唯一标识。拆分消息成Chunk时添加该ID,从而在还原时根据该ID识别Chunk属于哪个消息。Message Body
消息体,承载了音视频等信息。
Chunk
Basic Header
基本的头部信息,在头部信息里面包含了chunk stream ID(流通道Id,用来标识指定的通道)和chunk type(chunk的类型)。Message Header
消息的头部信息,包含了要发送的实际信息(可能是完整的,也可能是一部分)的描述信息。Message Header的格式和长度取决于Basic Header的chunk type。Extended TimeStamp
扩展时间戳。4个字节,能表示的最大数值就是0xFFFFFFFF=4294967295。当扩展时间戳启用时,timestamp字段或者timestamp delta要全置为1,表示应该去扩展时间戳字段来提取真正的时间戳或者时间戳差。注意扩展时间戳存储的是完整值,而不是减去时间戳或者时间戳差的值。Chunk Data
块数据。
RTMP在传输数据的时候,发送端会把需要传输的媒体数据封装成消息,然后把消息拆分成消息块,再一个一个进行传输。接收端收到消息块后,根据Message Stream ID重新将消息块进行组装、组合成消息,再解除该消息的封装处理就可以还原出媒体数据。
RTMP收发数据是以Chunk为单位,而不是以Message为单位。需要注意的是,RTMP发送Chunk必须是一个一个发送,后面的Chunk必须等前面的Chunk发送完成。
为什么RTMP要将Message拆分成不同的Chunk呢?
通过拆分,数据量较大的Message可以被拆分成较小的“Message”,这样就可以避免优先级低的消息持续发送阻塞优先级高的数据,比如在视频的传输过程中,会包括视频帧,音频帧和RTMP控制信息,如果持续发送音频数据或者控制数据的话可能就会造成视频帧的阻塞,然后就会造成看视频时最烦人的卡顿现象。同时对于数据量较小的Message,可以通过对Chunk Header的字段来压缩信息,从而减少信息的传输量。
Chunk的默认大小是128字节,在传输过程中,通过一个叫做Set Chunk Size的控制信息可以设置Chunk数据量的最大值
,在发送端和接受端会各自维护一个Chunk Size,可以分别设置这个值来改变自己这一方发送的Chunk的最大大小。
大一点的Chunk减少了计算每个chunk的时间从而减少了CPU的占用率,但是它会占用更多的时间在发送上,尤其是在低带宽的网络情况下,很可能会阻塞后面更重要信息的传输;小一点的Chunk可以减少这种阻塞问题,但小的Chunk会引入过多额外的信息(Chunk中的Header),少量多次的传输也可能会造成发送的间断导致不能充分利用高带宽的优势,因此并不适合在高比特率的流中传输。
在实际发送时应对要发送的数据用不同的Chunk Size去尝试,通过抓包分析等手段得出合适的Chunk大小,并且在传输过程中可以根据当前的带宽信息和实际信息的大小动态调整Chunk的大小,从而尽量提高CPU的利用率并减少信息的阻塞机率。
※ 举个例子
这一例子阐述了一条消息太大,无法装在一个 128 字节的块里,被分割为若干块。
这是传输的块:
块 1 的数据头说明了整个消息长度是为 307 个字节。
由以上俩例子可以得知,块类型 3(一共有4种类型0-3具体这里就粗展开啦,大概就是0是第一块,之后的看情况了)可以被用于两种不同的方式。第一种是用于定义一条消息的配置。第二种是定义一个可以从现有状态数据中派生出来的新消息的起点。
3. Handshake
要建立一个有效的RTMP Connection链接,首先要“握手”:客户端要向服务器发送C0,C1,C2(按序)三个chunk,服务器向客户端发送S0,S1,S2(按序)三个chunk,然后才能进行有效的信息传输。RTMP协议本身并没有规定这6个Message的具体传输顺序,但RTMP协议的实现者需要保证这几点:
- 客户端要等收到S1之后才能发送C2
- 客户端要等收到S2之后才能发送其他信息(控制信息和真实音视频等数据)
- 服务端要等到收到C0之后发送S1
- 服务端必须等到收到C1之后才能发送S2
- 服务端必须等到收到C2之后才能发送其他信息(控制信息和真实音视频等数据)
理论上来讲只要满足以上条件,如何安排6个Message的顺序都是可以的,但实际实现中为了在保证握手的身份验证功能的基础上尽量减少通信的次数,一般的发送顺序是这样的:
| Client | Server |
|---C0+C1--->|
|<--S0+S1+S2--|
|---C2----> |
(C2 S2属于用于确认Ack包)
wireshake抓包指南:https://www.jianshu.com/p/82bcdb1decf7
- C0与S0
C0和S0的长度是一个字节,在 S0 中这个字段表示服务器选择的 RTMP 版本。
rtmp1.0规范所定义的版本是3。0-2
是早期产品所用的,已被丢弃;4-31
保留在未来使用;32-255
不允许使用(为了区分其他以某一字符开始的文本协议)。如果服务无法识别客户端请求的版本,应该返回 3 。客户端可以选择减到版本 3 或选择取消握手。
- C1与S1
C1 和 S1 有 1536 字节长,由下列字段组成:
时间
:4 字节本字段包含时间戳。该时间戳应该是发送这个数据块的端点的后续块的时间起始点。可以是 0,* 或其他的任何值。为了同步多个流,端点可能发送其块流的当前值。
零
:4 字节本字段必须是全零。
随机数据
:1528 字节。 本字段可以包含任何值。 因为每个端点必须用自己初始化的握手和对端初始化的握手来区分身份,所以这个数据应有充分的随机性。但是并不需要加密安全的随机值,或者动态值。
- C2与S2
C2 和 S2 消息有 1536 字节长。只是 S1 和 C1 的回复。本消息由下列字段组成:
时间
:4 字节 本字段必须包含对等段发送的时间(对 C2 来说是 S1,对 S2 来说是 C1)。
时间 2
:4 字节 本字段必须包含先前发送的并被对端读取的包的时间戳。
随机回复
:1528 字节 本字段必须包含对端发送的随机数据字段(对 C2 来说是 S1,对 S2 来说是 C1) 。 每个对等端可以用时间和时间 2 字段中的时间戳来快速地估计带宽和延迟。 但这样做可能并不实用。
RTMP握手的这个过程就是完成了两件事:
- 校验客户端和服务器端RTMP协议版本号
- 是发了一堆数据,猜想应该是测试一下网络状况,看看有没有传错或者不能传的情况
Uninitialized (未初始化):协议的版本号在这个阶段被发送。客户端和服务器都是 uninitialized (未初始化) 状态。之后客户端在数据包 C0 中将协议版本号发出。如果服务器支持这个版本,它将在回应中发送 S0 和 S1。如果不支持呢,服务器会才去适当的行为进行响应。在 RTMP 协议中,这个行为就是终止连接。
Version Sent (版本已发送):在未初始化状态之后,客户端和服务器都进入 Version Sent (版本已发送) 状态。客户端会等待接收数据包 S1 而服务器在等待 C1。一旦拿到期待的包,客户端会发送数据包 C2 而服务器发送数据包 S2。(客户端和服务器各自的)状态随即变为 Ack Sent (确认已发送)。
Ack Sent (确认已发送):客户端和服务器分别等待 S2 和 C2。
Handshake Done (握手结束):客户端和服务器可以开始交换消息了。
Flash播放器连接服务器时,若服务器只支持简单握手,则无法播放h264和aac的流,可能是adobe的限制。adobe将简单握手改为了有一系列加密算法的复杂握手(complex handshake)。
当服务器和客户端的握手是按照rtmp协议进行,是不支持h264/aac的Flash,有数据,就是没有视频和声音。原因是adobe变更了握手的数据结构,标准rtmp协议的握手的包是随机的1536字节(S1S2C1C2),变更后的是需要进行摘要和加密。
rtmp协议定义的为simple handshake,变更后加密握手可称为complex handshake。
4. Connection
- 客户端发送 connect 命令到服务器端以请求对服务器端应用实例的连接。
- 收到 connect 命令后,服务器端发送协议消息 '窗口确认大小' 到客户端。服务器端也会连接到 connect 命令中提到的应用。
- 服务器端发送协议消息 '设置对端带宽' 到客户端。
- 在处理完协议消息 '设置对端带宽' 之后客户端发送协议消息 '窗口确认大小' 到服务器端。
- 服务器端发送另一个用户控制消息 (StreamBegin) 类型的协议消息到客户端。
- 服务器端发送结果命令消息告知客户端连接状态 (success/fail)。这一命令定义了事务 ID (常常为 connect 命令设置为 1)。这一消息也定义了一些属性,比如 FMS 服务器版本 (字符串)。此外,它还定义了其他连接关联到的信息,比如 level (字符串)、code (字符串)、description (字符串)、objectencoding (数字) 等等。
Window Acknowledgement Size 是设置接收端消息窗口大小,一般是2500000字节,即告诉客户端你在收到我设置的窗口大小的这么多数据之后给我返回一个ACK消息,告诉我你收到了这么多消息。在实际做推流的时候推流端要接收很少的服务器数据,远远到达不了窗口大小,所以基本不用考虑这点。而对于服务器返回的ACK消息一般也不做处理,我们默认服务器都已经收到了这么多消息。
服务器返回的_result命令类型消息的payload length一般不会大于128字节,但是在最新的nginx-rtmp中返回的消息长度会大于128字节,所以一定要做好收包,组包的工作。
connect 命令里面包含什么东西?
真实通信中要指定一些编解码的信息,这些信息是以AMF格式发送的,其中audioCodecs和videoCodecs这两个指定音视频编码信息的不能少的。
属性 | 类型 | 描述 | 范例 |
---|---|---|---|
app | 字符串 | 客户端连接到的服务器端应用的名字。 | testapp |
flashver | 字符串 | Flash Player 版本号。和ApplicationScript getversion() 方法返回的是同一个字符串。 | FMSc/1.0 |
swfUrl | 字符串 | 进行当前连接的 SWF 文件源地址。例如:app/viewplayer.swf | file://C:/FlvPlayer.swf |
tcUrl | 字符串 | 服务器 URL。具有以下格式:protocol://servername:port/appName/appInstance,例如rtmp:IP:PORT/live/ | rtmp://localhost:1935/testapp/instance1 |
fpad | 布尔 | 如果使用了代理就是 true。 | true 或者 false。 |
audioCodecs | 数字 | 表明客户端所支持的音频编码。 | SUPPORT_SND_MP3 |
videoCodecs | 数字 | 表明支持的视频编码。 | SUPPORT_VID_SORENSON |
videoFunction | 数字 | 表明所支持的特殊视频方法。 | SUPPORT_VID_CLIENT_SEEK |
pageUrl | 字符串 | SWF 文件所加载的网页 URL。可以是undefined | http://somehost/sample.html |
objectEncoding | 数字 | AMF 编码方法。0、3.一般采用3 | AMF3 |
这里就只举个video code的例子了,具体其他的几个code可以大家自己从reference里面看哦~
5. Establish Stream
创建完网络连接之后就可以创建网络流了:
- 客户端发送命令消息中releaseStream命令到服务器端
- 客户端发送命令消息中FCPublish命令到服务器端
- 客户端发送命令消息中的“创建流”(createStream)命令到服务器端。
- 服务器端接收到“创建流”命令后,发送命令消息中的“结果”(_result),通知客户端流的状态。
- 解析服务器返回的消息会得到一个stream ID, 这个ID也就是以后和服务器通信的 message stream ID, 一般返回的是1,不固定。
6. Publishing
推流准备工作的最后一步是 Publish Stream,即向服务器发一个publish命令,这个命令的message stream ID 就是上面 create stream 之后服务器返回的stream ID,发完这个命令一般不用等待服务器返回的回应,直接下一步发送音视频数据。有些rtmp库 还会发setMetaData消息,这个消息可以发也可以不发,里面包含了一些音视频编码的信息。
当以上工作都完成的时候,就可以发送音视频了。
7. Pros and Cons
直播应用中,RTMP和HLS基本上可以覆盖所有客户端观看,HLS主要是延时比较大,RTMP主要优势在于延时低。
低延时应用场景包括:
- 互动式直播:譬如2013年大行其道的美女主播,游戏直播等等。各种主播,流媒体分发给用户观看。用户可以文字聊天和主播互动。
- 视频会议:我们要是有同事出差在外地,就用视频会议开内部会议。其实会议1秒延时无所谓,因为人家讲完话后,其他人需要思考,思考的延时也会在1秒左右。当然如果用视频会议吵架就不行。
- 其他:监控,直播也有些地方需要对延迟有要求,互联网上RTMP协议的延迟基本上能够满足要求。
RTMP的特点如下:
Adobe支持得很好:
RTMP实际上是现在编码器输出的工业标准协议,基本上所有的编码器(摄像头之类)都支持RTMP输出。
原因在于PC市场巨大,PC主要是Windows,Windows的浏览器基本上都支持flash,Flash又支持RTMP支持得非常好。适合长时间播放:
因为RTMP支持的很完善,所以能做到flash播放RTMP流长时间不断流,当时测试是100万秒,即10天多可以连续播放。
对于商用流媒体应用,客户端的稳定性当然也是必须的,否则最终用户看不了还怎么玩?
有个案例,最初使用播放器播放http流,需要播放不同的文件,结果就总出问题,如果换成服务器端将不同的文件转换成RTMP流,客户端就可以一直播放;该客户走RTMP方案后,经过CDN分发,没听说客户端出问题了。延迟较低:
比起YY的那种UDP私有协议,RTMP算延迟大的(延迟在1-3秒),比起HTTP流的延时(一般在10秒以上)RTMP算低延时。有累积延迟:
技术一定要知道弱点,RTMP有个弱点就是累积误差,原因是RTMP基于TCP不会丢包。
所以当网络状态差时,服务器会将包缓存起来,导致累积的延迟;待网络状况好了,就一起发给客户端。这个的对策就是,当客户端的缓冲区很大,就断开重连。
有机会再来看下RTSP(Real Time Streaming Protocol)协议吧~ 据说是实时性最好的~
参考:
两种协议对比:https://www.jianshu.com/p/c2284659452f
强推:https://www.cnblogs.com/wanggang123/p/7513812.html
实现&强推:https://www.jianshu.com/p/00aceabce944
why: https://baijiahao.baidu.com/s?id=1627055493270643586&wfr=spider&for=pc