前言
最近在学习rtmp协议,在看官方文档的时候总是懵懵懂懂,硬生生看了两天,现在基本上了解rtmp协议了,想用自己觉得比较清晰的方式来讲解rtmp协议,希望能够对向我一样的初学者有所帮助。
本文将通过以下四部分讲解rtmp协议。
- 1、消息
- 2、块
- 3、rtmp的消息类型
- 4、实例分析rtmp传输过程
一、消息
消息是rtmp的基本数据单元,服务端和客户端通过在网络上发送RTMP消息进行通讯。消息可能包含音频,视频,数据,或其他的消息。
消息格式
RTMP消息头和载荷两部分。
消息头
值 | 长度 | 含义 |
---|---|---|
message type | 1byte | 表示消息类型 |
payload length | 3byte | 表示载荷的字节数,big-endian格式 |
timestamp | 4byte | 表示消息的时间戳,big-endian格式 |
stream id | 3byte | 表示消息流ID,big-endian格式 |
- 消息类型:
1-7的消息ID用于协议控制消息
8、9的消息分别用于传输音频和视频数据
15-20的消息用于发送AMF编码的命令,负责用户与服务器之间的交互,比如播放,暂停
载荷
载荷中消息中包含的真实数据。例如,它可以是音频样本或压缩的视频数据。
二、块
消息是rtmp协议的基本数据单元,在网络传输时,消息会被重新封装成块进行传输,每个块都有固定的大小,如果消息大小大于块的大小,消息就会被拆分成几个块发送。
块格式
块由头和载荷组成
块头结构
值 | 长度 | 含义 |
---|---|---|
basic header | 1byte或2byte或3byte | 块基本头 |
chunk msg header | 11byte或2byte或3byte | 块消息头 |
extended timestamp | 4byte | 扩展时间戳 |
a、块基本头(basic header)
- 基本头第一个字节的结构:
值 | 长度 | 含义 |
---|---|---|
fmt | 2bit | 块格式 |
cs id | 6bit | 块流ID |
- fmt的含义
值 | 含义 |
---|---|
0 | 表示块消息头是类型0 |
1 | 表示块消息头是类型1 |
2 | 表示块消息头是类型2 |
3 | 表示块消息头是类型3 |
- cs id的含义
值 | 含义 |
---|---|
0 | 块基本头2个字节,块流ID计算方法(第2个字节 64),范围(64-319) |
1 | 块基本头3个字节,块流ID计算方法(第3个字节*255 第2个字节 64),范围(3-65599) |
2 | 块基本头1个字节,2表示低层协议消息 |
3-64 | 块基本头1个字节,该数表示块流ID,范围(3-64) |
本协议支持65597种流,ID从3-65599。ID 0、1、2作为保留。
b、块消息头(chunk msg header)
- 类型0
类型0总共11byte,格式如下:
值 | 长度 | 含义 |
---|---|---|
timestamp | 3byte | 时间戳 |
message length | 3byte | 载荷的长度 |
message type id | 1byte | 消息类型id |
message stream id | 4byte | 消息流id |
- 类型1
类型1总共7byte,格式如下:
值 | 长度 | 含义 |
---|---|---|
timestamp | 3byte | 时间戳 |
message length | 3byte | 载荷的长度 |
message type id | 1byte | 消息类型id |
- 类型2
类型1总共3byte,格式如下:
值 | 长度 | 含义 |
---|---|---|
timestamp | 3byte | 时间戳 |
- 类型3
类型3的块没有头。流ID,消息长度,时间戳都不出现。这种类型的块使用与先前块相同的数据。当一个消息被分成多个块,除了第一块以外,所有的块都应使用这种类型。
c、扩展时间戳(extended timestamp)
扩展时间戳总共4个字节,只有当块消息头中的普通时间戳设置为0x00ffffff时,本字段才被传送。如果普通时间戳的值小于0x00ffffff,那么本字段一定不能出现。
上面已经详解讲解了rtmp的数据格式了,下面来讲解具体的rtmp协议内容。
载荷
块的载荷就是消息的载荷内容。
总结一下:消息是rtmp的基本数据单元,块是用于将消息重新封装在网络上传输。
三、rtmp的消息类型
rtmp的消息类型可分为三大类:协议控制消息、用户控制消息、RTMP命令消息
1、协议控制消息
1.1.设置块大小消息(Message Type=1)
设置块大小,被用来通知对方新的最大块大小。 默认最大块大小为128字节,客户端和服务器可以使用此消息来修改默认的块大小。例如,假设客户端想要发送的音频数据大小为131字节,而块大小为128字节。在这种情况下,客户端可以通知服务器新的块大小为131字节,然后就可以使用一个块来发送完整的音频数据了。
最大的块大小建议至少为128字节,但必须至少为1字节。通信的每一个方向(例如从客户端到服务器)拥有独立的块大小设置。最大的块大小由通信双方 (服务器或者客户端) 自行维护。
设置块大小消息载荷一共4个字节,格式如下:
值 | 长度 | 含义 |
---|---|---|
0 | 1bit | 第一位必须为0 |
chunk size | 31bit | 新的最大块大小值 |
1.2.终止消息(Message Type=2)
终止消息,用于通知对端,如果正在等待一条消息的部分块(已经接收了一部分),那么可以丢弃之前已经接收到的块。对端将接收到的块流ID作为当前协议控制消息的有效负载。应用程序可能会在关闭的时候发送这个消息以指示不需要进一步对这个消息的处理了。
终止消息载荷一共4个字节,格式如下:
值 | 长度 | 含义 |
---|---|---|
chunk stream id | 4byte | 块流id,该块流的消息会别丢齐 |
1.3.确认消息(Message Type=3)
客户端或者服务器在接收到等同于窗口大小的字节之后必须发送给对端一个确认消息。窗口大小是指发送者在没有收到接收者确认消息之前发送的最大字节数。这个消息定义了序列号,也就是到目前为止接收到的字节数。
确认消息载荷一共4个字节,格式如下:
值 | 长度 | 含义 |
---|---|---|
sequence number | 4byte | 目前为止接收到的字节数 |
1.4.确认窗口大小消息(Message Type=5)
客户端或服务端发送本消息来通知对方发送确认(致谢)消息的窗口大小。例如,服务端希望每当发送的字节数等于窗口大小时从客户端收到确认(致谢)。服务端在成功处理了客户端的连接请求后向客户端更新窗口大小。
确认窗口大小消息载荷一共4个字节,格式如下:
值 | 长度 | 含义 |
---|---|---|
acknowledge window size | 4byte | 确认窗口的大小 |
1.5.设置对端带宽消息(Message Type=6)
客户端或服务端发送本消息更新对等端的输出带宽。输出带宽值与窗口大小值相同。如果对等端在本消息中收到的值与窗口大小不相同,则发回确认(致谢)窗口大小消息。
值 | 长度 | 含义 |
---|---|---|
acknowledge window size | 4byte | 确认窗口的大小 |
limit type | 1byte | 限制类型字段 |
- limit type:
发送者可以在限制类型字段把消息标记为硬(0),软(1),或者动态(2)。如果是硬限制对等端必须按提供的带宽发送数据。如果是软限制,对等端可以灵活决定带宽,发送端可以限制带宽。如果是动态限制,带宽既可以是硬限制也可以是软限制。
2、用户控制消息(Message Type=4)
RTMP使用消息类型ID 4表示用户控制消息。这些消息包含RTMP流传输层所使用的信息。协议控制消息使用的ID为 1、2、3、5 和 6 (前面已经介绍过了)。 用户控制消息应该使用消息流ID 0 (以被认为是控制流),并且以RTMP块流发送时以块流ID为2。协议控制消息接收立即生效;解析时,时间戳字段被忽略。
客户端或者服务器端发送这个消息来通知对端一些用户控制事件。
这一消息携带有事件类型和事件数据,格式如下:
值 | 长度 | 含义 |
---|---|---|
event type | 2byte | 事件类型 |
event data | ? | 事件数据 |
- event type
事件 | 描述 |
---|---|
Stream Begin (=0) | 服务器发送这个事件来通知客户端一个流已就绪并可以用来通信。默认情况下,这一事件在成功接收到客户端的连接命令之后以ID=0发送。事件数据为4字节,代表了已就绪流的流ID。 |
Stream EOF (=1) | 服务器发送这一事件来通知客户端请求的流的数据回放已经结束。在发送额外的命令之前不再发送任何数据。客户端将丢弃接收到的这个流的消息。事件数据为4字节,代表了回放已结束的流的流 ID。 |
StreamDry (=2) | 服务器发送这一事件来通知客户端当前流中已没有数据。当服务器在一段时间内没有检测到任何消息,它可以通知相关客户端当前流已经没数据了。这一事件数据为4字节,代表了已没数据的流的流 ID。 |
SetBuffer Length (=3) | 客户端发送这一事件来通知服务器缓冲区大小 (以毫秒为单位),这个缓冲区用于缓存来自流的任何数据。此事件在服务器开始处理流之前就发送。事件数据的前4个字节代表了流ID,紧接其后的4个字节代表了以毫秒为单位的缓冲区的长度。 |
Streams Recorded (=4) | 服务器发送这一事件来通知客户端当前流是一个录制流。事件数据为4字节,代表了录制流的流 ID。 |
PingRequest (=6) | 服务器端发送这一事件用于测试客户端是否可达。事件数据是为一个4字节的时间戳,代表了服务器端发送这一命令时的服务器本地时间。客户端在接收到这一消息后会立即发送 PingResponse 回复。 |
PingResponse(=7) | 客户端发送这一事件用于回复服务器的PingRequest。事件数据是为一个4字节的时间戳,该时间戳是从接收到的PingRequest的事件数据中获取的。 |
3、RTMP命令消息
3.1.数据消息(Message Type=18或15)
客户端或服务端通过本消息向对方发送元数据和用户数据。元数据包括数据的创建时间、时长、主题等细节。消息类型为18的用AMF0编码,消息类型为15的用AMF3编码。
3.2.共享对象消息 (Message Type=19或16)
共享对象是跨多个客户端,实例同步的FLASH对象(名值对的集合)。消息类型kMsgContainer=19用AMF0编码,kMsgContainerEx=16用AMF3编码,这两个消息用于共享对象事件。每个消息可包含多个事件。
格式如下:
事件 | 描述 |
---|---|
Use (=1) | 客户端发送这个事件通知服务端创建一个命名的共享对象。 |
Release (=2) | 当客户端删除共享对象时,发送这个事件通知服务端。 |
Requeset change (=3) | 当请求改变一个共享对象的某个命名参数的关联的值时,客户端发送本消息。 |
Change (=4) | 当某个命名参数的关联值被改变时,服务端发送本事件给所有的客户端。 |
Success (=5) | 当接受请求改变事件后,服务端向发请求的客户端响应本消息。 |
SendMessage (=6) | 客户端向服务端发送本事件广播一个消息。接收到本事件后服务端向包括发送本事件在内的所有客户端广播一个消息。 |
Status (=7) | 针对一个错误状态,服务端向客户端发送本事件。 |
Clear (=8) | 服务端向客户端发送本事件,清除一个共享对象。本事件也作为客户端在连接时发送use事件的响应。 |
Remove (=9 ) | 服务端发送本事件使客户端删除一个插槽。 |
Request Remove (=10) | 客户端删除一个插槽时向服务端发送本事件。 |
Use Success(=11) | 当连接成功时服务端向客户端发送本事件。 |
3.3.音频消息 (Message Type=8)
客户端和服务端使用该消息向对端发送音频数据。
3.4.视频消息 (Message Type=9)
客户端和服务端使用该消息向对端发送视频数据。
3.5.聚合消息(Message Type=22)
聚合消息是包含一系列子消息的单个消息。
格式如下:
聚合消息的消息流ID将覆盖消息聚合内的子消息的流ID。 聚合消息与第一条子消息时间戳的区别是偏移量,它用于将子消息的时间戳重新归一到流时间表。偏移量被添加到每个子消息的时间戳以达到归一化流时间。 第一个子消息的时间戳应该与聚合消息的时间戳相同,所以偏移应该为零。 返回指针包含前一个消息的大小,包括它的消息头。 包含消息头是为了与FLV文件的格式相匹配和用于向后查找
3.6.命令消息(Message Type=20或17)
命令消息承载用AMF编码的客户端与服务端之间的命令。消息类型为20的用AMF0编码,消息类型为17的用AMF3编码。
这些消息用于在远端实现连接,创建流,发布,播放和暂停等操作。状态,结果等命令消息用于通知发送者请求命令的状态。命令消息由命令名,传输ID,和包含相关参数的命令对象组成。客户端或服务端可以使用命令消息向远端请求远程过程调用。
a.NetConnection相关命令
- connect
- call
- close
- createStream
b.NetStream相关命令
- play
- play2
- deleteStream
- closeStream
- receiveAudio
- receiveVideo
- publish
- seek
- pause
- 服务器使用”onStatus”命令向客户端发送NetStream状态:
四、实例分析rtmp传输过程
这部分将介绍rtmp传输过程,并且使用wireshark抓包,分析实例。
- 握手
- 建立网络连接
- 建立流
- 播放
1、握手
(1)握手开始于客户端发送C0,C1块。
(2)服务端在接收到C0后发送S0,S1块。
(3)客户端接收到S0,S1块后,发送C2块,服务端接收到C0,C1块后发送S2块。
(4)当客户端接收到S2,服务端接受到C2,分别校验后握手成功。
示意图如下:
C0、S0的只有1个字节,格式如下:
值 | 长度 | 含义 |
---|---|---|
version | 1byte | 版本号 |
C1、S1有1526个字节,格式如下:
值 | 长度 | 含义 |
---|---|---|
time | 4byte | 时间戳 |
zero | 4byte | 零 |
random bytes | 1528byte | 本字段用于校验使用 |
C2和S2的消息格式
C2和S2的消息个事和C1、S2一样,C2是对S1的拷贝,S2是对C1的拷贝。
服务端在接受到C2后会与S1对比。
客户端在接受到S2后会与C1对比。
rtmp通过这种手段来判断握手是否成功。
实例分析:
(1)握手开始于客户端发送C0,C1块:
代码语言:javascript复制03:这个字节是C0,表示版本号。
之后的1536个字节都是C1
ff fd fe b4:时间戳
00 00 00 00:四个零
之后1528个字节都是随机数
(2)服务端在接收到C0后发送S0,S1块:
代码语言:javascript复制03:这个字节是S0,表示版本号。
之后的1536个字节都是S1
ff fd fe b4:时间戳
00 00 00 00:四个零
之后1528个字节都是随机数
(3)客户端接收到S0,S1块后,发送C2块,服务端接收到C0,C1块后发送S2块。
下图是服务端发送的S2:
可以看出,数据是与C1相同的。
下图是客户端发送的C2:
可以看出,数据是与S1相同的。
到此握手完成。
2、建立网络连接
(1)客户端发送”connect消息”请求连接。
(2)服务收到”connect”请求后,发送一个”确认窗口大小消息”。
(3)服务端发送”设置带宽消息”到客户端。
(4)客户端处理”设置带宽消息”后,发送”确认窗口大小消息”到服务端(设置带宽消息的值与窗口大小不相同才会发送此消息)。
(5)服务器发送用户控制消息中的“流开始(Stream Begin)消息“到客户端。
(6)服务器发送“命令消息”中的”结果“(_result),通知客户端连接的状态。
示意图如下:
实例分析:
客户端向服务端发送命令消息的”connect命令”:
代码语言:javascript复制03:bit[7:8]表示fmt为0,bit[0:6]表示块流ID为3(如果是0,则表示有一个扩展字节;如果是1,则表示有两个扩展字节,如果是2则表示底层协议控制消息和命令)
因为fmt为0,所以接下来的块消息头有11个字节
00 00 00:timestamp
00 00 5f:message length
14:message type,十进制为20,表示消息数据类型使用AMF0编码
00 00 00 00:msg stream id
接下来是块数据
02:amf type(表示字符串)
00 07:string length(表示字符串长度)
63 6f 6e 6e 65 63 74:string("connect")
00 :amf type(表示double型)
3f f0 00 00 00 00 00 00:val
03:amf type(表示Object型)
key:
00 03:string length(表示字符串长度)
61 70 70:string("app")
value:
02:amf type(表示字符串)
00 04:string length(表示字符串长度)
74 79 70 65:string("live")
key:
00 04:string length(表示字符串长度)
74 79 70 65:string("type")
val:
02:amf type(表示字符串)
00 0a:string length(表示字符串长度)
6e 6f 6e 70 72 69 76 61 74 65:string("nonprivate")
key:
00 05:string length(表示字符串长度)
74 63 55 72 6c:string("tcUrl")
val:
02:amf type(表示字符串)
00 1f:string length(表示字符串长度)
72 74 6d 70 3a 2f 2f 31 39 32 2e 31 36 38 2e 31 36 38 2e 31 36 2e 31 32 38 3a 38 30 30 31 2d 6c 69 76 65:string("rtmp://192.168.16.128:8001/live")
00 00 09:表示结束
服务端向客户端发送确认窗口消息:
代码语言:javascript复制02:bit[7:8]表示fmt为0,bit[0:6]表示块流ID为2(2表示底层协议控制消息和命令)
因为fmt为0,所以接下来的块消息头有11个字节
00 00 00:timestamp
00 00 54:message length
05:message type(5表示确定窗口大小事件)
00 00 00 00:msg stream id
00 4c 4b 40:window size
此后服务端又向客户端发送了三条消息,这里和文档说的有点不大一样,我也不太理解,希望有大佬能够解释解释:
该块流包含几条消息
- 第一条消息为设置对端带宽消息
02:bit[7:8]表示fmt为0,bit[0:6]表示块流ID为2(2表示底层协议控制消息和命令)
因为fmt为0,所以接下来的块消息头有11个字节
00 00 00:timestamp
00 00 05:message length
06:message type(6表示设置对端带宽消息)
00 00 00 00:msg stream id
00 4c 4b 40:window size(带宽为5M)
02 :limit type
0 - Hard:对端应该限制其输出带宽不超过指定的窗口大小。
1 - Soft:对端应该限制其输出带宽不超过指定的窗口大小,或者已经有限制在起作用的话,就取两个窗口大小之间的较小值。
2 - Dynamic:如果先前的限制类型为 Hard,则这条消息的也被视为Hard消息,否则的话忽略这条消息。
- 第二条消息为设置块大小消息
02:bit[7:8]表示fmt为0,bit[0:6]表示块流ID为2(2表示底层协议控制消息和命令)
因为fmt为0,所以接下来的块消息头有11个字节
00 00 00:timestamp
00 00 04:message length
01:message type(1表示设置块大小消息)
00 00 00 00:msg stream id
00 00 10 00
- 第三条消息为connect回复消息
03:bit[6:7]表示fmt为0,bit[0:5]表示块流ID为1(3表示块流ID)
因为fmt为0,所以接下来的块消息头有11个字节
00 00 00:timestamp
00 00 be:message length
14:message type(十进制为20,表示消息数据以AMF0格式编码)
00 00 00 00:msg stream id
02:amf type(表示字符串)
00 07:string length(表示字符串长度)
5f 71 65 73 75 6c 74:string("_resulte")
00:amf type(表示double)
3f f0 00 00 00 00 00 00:val(double型)
03:amf type(表示Object类型)
key:
00 06:string length(表示字符串长度)
66 6d 73 56 65 72:string("fmsVer")
val:
02:amf type(表示字符串)
00 0d:string length(表示字符串长度)
46 4d 53 2f 33 2c 30 2c 30 2c 31 2c 31 32 33:string("FMS/2,0,1,123")
...
00 00 09:end
3、建立流
(1)客户端发送”命令消息”中的”创建流命令(createStream)”到服务端。
(2)服务端接受到消息之后,发送”命令消息”中的”结果(_result)”。
示意图如下:
实例分析:
(1)客户端向服务端发送”命令消息”中的”createStream命令”:
(2)服务端向客户端发送”命令消息”中的”结果(_result)”:
4、发布音视频数据
(1)客户端发送”命令消息”中的”publish命令”。
(2)服务端接受到消息之后发送用户控制消息中的“streambegin”。
(3)客户端开始发送音视频数据。
示意图如下:
实例分析:
客户端发送”命令消息”中的”publish命令”:
服务端回复:
客户端发送多媒体信息:
…
客户端发送多媒体数据:
…
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/200812.html原文链接:https://javaforall.cn