上个月看了一个简单的 RTMP 服务(threaded_rtmp_server)代码,在比较高的封装层级了解了数据的处理与转发,终于有时间继续学习 RTMP 协议,结合网上的文章与自己抓包采集到的数据。
环境说明
推流使用的:FFmpeg
RTMP 服务使用的:threaded_rtmp_server
Wireshark 抓包文件:FFmpeg推送日志.pcapng
推流命令
ffmpeg -re -i salv.mp4 -c copy -f flv rtmp://192.168.3.188:1935/live/stream
Wireshark 过滤参数
ip.addr == 192.168.3.188 && !(tcp.port == 22) && not tcp.len==0
过滤出和指定 IP 通信的数据,过滤掉 22 端口及 tcp 长度为 0 的心跳数据。
握手
由图可知,客户端发送 C0 + C1,服务端回复 S0 + S1 + S2,客户端回复 C2,握手完成。
协议握手规则:客户端发起 C0,C1 服务端回复 S0,S1,当客户端收到 S0 和 S1 后发送 C2,当服务端收到 C0 和 C1 后发送 S2,当双方各受到 C2,S2 后握手完成,实现时为了降低延时提升效率,就像上图看到的,C0 + C1 一起发送,S0 + S1 + S2 一起回复。
握手时做了两件事儿,确定了协议版本号及发送随机数据校验网络可达性。
握手包的格式分为简单握手和复杂握手,本次的示例是简单握手。
上图是客户端发起握手请求的 C0 + C1 数据,根据协议,C0 就一个字节,最前边那个 03
,接着紫色框中是四字节的 time 字段,一般都为 0,橙色内的四个字节为 zero 字段,根据协议来说,也应该用 0 来填充,看起来 FFmpeg 没有完全按照协议实现,不过没什么影响。后续的数据就是 1528 个字节的随机数了。
继续查看 S0 + S1 + S2 服务端返回的数据
看一些网上关于 RTMP 协议解析的文章写,S2 的随机数据部分应该为 C1 的随机数据。可是根据抓包看到的数据,C1 随机数据开头部分是 f7 78 55 ...
而 S2 的随机数据起始为 c6 62 04 ...
显然是不同的,那只有一个解释,客户端没有对 C1 与 S2 的数据部分进行校验。
最后看一下客户端回复的 C2 数据
依旧跳过前8个字节(time + time2),可以看到 C2 回复的包符合规范的要求,把服务端生成的 S1 随机数据部分 65 aa 37...
按照规范进行了回复,也难怪,自己可以不校验偷懒,C2 要是不规范,服务端进行校验握手就失败了。
PS:规范和实现存在偏差无处不在,能用就行呗的理论果真世界通用。
RTMP 块流
RTMP 中的块内容由 RTMP Header 和 RTMP Body 组成,RTMP Header 由基本头和消息头扩展时间戳组成。
阅读块流格式更详尽的解释,访问:https://chenlichao.gitbooks.io/rtmp-zh_cn/content/5.3-chunking.html
直接看抓包的数据,这是一个通知对端,其发送的数据块最大大小的协议控制消息。
上图绿色方框中的数据就是基础头,块类型决定块消息头的格式(上图橙色框部分),总共有四种,此处的块消息头类型为 0,表示块消息头共占用 11 个字节(不包含基础头),也就是 00 00 00 00 00 04 01 00 00 00 00
。
上图为块消息头为0的详细格式,可以跟抓包数据进行对照,其它类型详细格式查看上边的文档网址。
继续看上边的抓包图中的绿色框内部分,它此时的长度为 1 字节(8位),前两位 00 表示消息块的格式类型,后 6 位表示 CS_ID,值为 2,可以得到两个信息,即基本头长度为一个字节,这是一条控制消息。
需要注意,基本头的长度不是固定的,可能为1,2,3个字节,如果后 6 位的值为 0,那么基本头的长度为 2 个字节,后 6 位值为 1,则基本头长度为 3 个字节。如果后 6 位的值为 2 ,则表示数据用于协议控制消息和命令。
紫色圆框内的为块流数据部分,00 00 10 00
值为 4096,因为 message type id 值为 1,根据协议控制消息类型,它是设置块最大大小的控制命令。
既然了解了 CS_ID,那就有必要进一步的知道流的数量和范围。
块流ID的数量与范围
块流ID(Chunk Stream ID)用于标识一个数据流,比如通过 RTMP 协议推送一个视频流,在持续推送的过程,它的块流ID是不变的。
RTMP最多支持65597个流。
接下来详细的计算一下这个数量是怎么计算出来的。
1)基本头长度为 1 字节
前 2 位用于表示消息头格式类型,后 6 位能用来表示 CS_ID,别忘记 0,1,2 上边说了有特殊意义。
6 位能表示 2^6 = 64 个 CS_ID,排除特殊的 3 个,即 1 字节的基本头能最多能容纳 61 个流,CS_ID 范围为 [3, 63]
2)基本头长度为 2 字节
第一个字节的后 6 位为 000000
,表示基本头长度为 2 字节长度,第二个字节 8 位,2^8 = 256 个 CS_ID,范围 [0, 255],因为和基本头长度为 1 位表示的 CS_ID 重复了,所以整体要加 64,即两个字节情况下,CS_ID 的范围为 [64, 319]
3)基本头长度为 3 字节
第一个字节的后 6 位为 000001
,表示基本头长度为 3 字节长度,后两个字节都可以表示流,2^16 = 65536 个 CS_ID,范围 [0, 65535],同理,跟 1 字节的表示重复,整体加 64,即三字节情况下,CS_ID 的取值范围为 [64, 65599]
可以看到三个字节包含了两个字节的情况,即 [64, 319] 的 CS_ID 用两个字节或三个字节都能够表示,三个字节长度的基本头,当第二个字节全为 0 的,其效果等同于二字节长度基本头,协议就是这么设计的,可能为了简化计算和理解,毕竟也不差那 256 个 ID。
不过我们在写程序的时候,应该合理的进行选择使用,毕竟能用两个字节的时候为什么要用三个字节呢~
协议实现方应该用能够承载块流ID的最短表示法来表示块流ID。
RTMP 协议最大能支持的流数量此时可以很容易计算出来:[3, 65599] = 65597 个。
协议控制消息
回到协议控制消息,在正式推送媒体数据之前,必然有一些命令请求,协商编码之类的操作,协议控制消息就是做这个用的。
RTMP 块流使用消息类型 ID 1、2、3、5、6 作为控制消息。这些消息包含了必要的 RTMP 块流协议信息。
这些协议控制消息必须使用 0 作为消息流ID(作为已知的控制流ID),同时使用 2 作为块流ID。协议控制消息接收立即生效; 解析时,时间戳字段被忽略。
类型为 1 的协议控制消息,在上图的 “Set Chunk Size 4096” 抓包图的时候已将看到了。
另外的几种简要描述如下:
控制消息类型 | 名称 | 简要简述 |
---|---|---|
1 | 设置块大小 | 用来通知对方新的最大的块大小。 |
2 | 中断消息 | 通知对方可以丢弃指定 CS_ID 块流消息,一般应用关闭前发送。 |
3 | 确认消息 | 客户端和服务器在接收到与接收窗口大小相等的数据后,必须发送应答消息给对方。 |
5 | 视窗大小确认 | 客户端和服务器发送这个消息来通知对方应答窗口的大小。 |
6 | 设置对等带宽 | 客户端或服务端发送该消息来限制对端的输出带宽。 |
此处没有详细记录控制消息的格式,较为详细的解释参考:https://chenlichao.gitbooks.io/rtmp-zh_cn/content/5.4-protocol-control-message.html
后续查看抓包数据想必会遇到很多控制消息,再详细对照学习。
用户控制消息
在协议控制消息中,消息类型少了 4 类型,这个类型就是用户控制消息。
暂时未按照 Wireshark 抓包顺序阅读,我选择了一个用户控制消息,可以看到在消息头中,消息类型 “User Control Message(0x04)” 为用户控制消息。
在 RTMP Body 中,橙色框中,前边固定的两个字节是 Event Type,Event Data 数据是变长的,在消息头中的 “Body size: 6” 可以知道用户控制消息消息体中的 Event Data 为 4 字节长度,00 00 00 01
,值为 1。
以下是摘自 -> 用户控制消息事件
上一张抓包图中,可以看到橙色圈中 Event Type 值为 0,对照表格代表服务端已经准备好,通知客户端可以推流。四个字节的 Event Data 值为 1,表示可用的流ID。
了解了 CS_ID,协议控制消息,用户控制消息等先行知识后,之后还是需要回到 Wireshark 抓包文件,按照顺序继续学习 RTMP 推流流程,掌握更多的细节。
今天的学习先到这里。
参考: