若要建立一个有效的 RTMPConnection链接,首先需要“握手”:客户端要向服务器按序发送C0,C1,C2三个chunk,服务器向客户端按序发送S0,S1,S2三个chunk,然后才能进行有效的信息传输。RTMP 协议本身并没有规定这6个信息的具体传输顺序,但 RTMP 协议的实现者需要保证这几点如下:
- 对于 RTMP 客户端
- 握手以客户端发送
C0和C1开始 - 客户端必须接收
S1之后才能发送C2 - 客户端必须接收
S2之后才能发送其他数据
- 握手以客户端发送
- 对于 RTMP 服务端
- 服务端必须接收到
C0才能发送S0和S1,也可以等待接收到C1再发送S0和S1 - 服务端必须接收
C1才能发送S2 - 服务端必须接收
C2才能发送其他数据
- 服务端必须接收到
握手示意图
+-------------+ +-------------+
| Client | TCP/IP Network | Server |
+-------------+ | +-------------+
| | |
Uninitialized | Uninitialized
| C0 | |
|------------------->| C0 |
| |-------------------->|
| C1 | |
|------------------->| S0 |
| |<--------------------|
| | S1 |
Version Sent |<--------------------|
| S0 | |
|<-------------------| |
| S1 | |
|<-------------------| Version Sent
| | C1 |
| |-------------------->|
| C2 | |
|------------------->| S2 |
| |<--------------------|
AckSSent | Ack Sent
| S2 | |
|<-------------------| |
| | C2 |
| |-------------------->|
Handshake Done | Handshake Done
| | |
Pictorial Representation Of Handshake
握手状态
-
Uninitialized (未初始化):协议的版本在这个状态中被发送。客户端在数据包
C0中将协议版本发出,如果服务端支持这个版本,将在回应中发送S0和S1,并进入Version Sent状态。如果不支持,服务端会终止连接。 -
Version Sent (版本已发送):客户端和服务端分别等待接收
S1和C1。接收完成后,将会进入Ack Sent状态。 -
Ack Sent (确认已发送):客户端和服务端分别等待接收
S2和C2。接收完成后,进入Handshake Done状态。 - Handshake Done (握手完成):客户端和服务端可以开始交换消息了。
握手数据格式
在 Flash10.1 之后,Adobe 对 RTMP 握手进行了一轮修改,握手的步骤和上文记述的没有不同,而是对握手的数据进行了修改,采用了加密数据。因此,现在存在两种握手数据,一般通过C1[4:8]是否为0来区分。我们将使用0值称为简单握手(simple handshake),将非0值的称为复杂握手(complex handshake)。
简单握手 - simple handshake
C0 和 S0(1 byte)
+-+-+-+-+-+-+-+-+-+-+-+
| version (1 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+
-
version(1 byte):版本号,表示客户端请求的 RTMP 版本号或服务端支持的 RTMP 版本号。当前使用的版本是3。
C1 和 S1(1536 bytes)
+-+-+-+-+-+-+-+-+-+-+
| time (4 bytes) |
+-+-+-+-+-+-+-+-+-+-+
| zero (4 bytes) |
+-+-+-+-+-+-+-+-+-+-+
| random bytes |
+-+-+-+-+-+-+-+-+-+-+
| random bytes |
| (cont) |
| .... |
+-+-+-+-+-+-+-+-+-+-+
-
time(4 bytes):本字段包含一个发送时间戳(取值可以为零或其他任意值)。客户端应该使用此字段来标识所有流块的时间戳。为了同步多个块流,客户端可能希望多个块流使用相同的时间戳。 -
zero(4 bytes):本字段必须全为零。 -
random (1528 bytes):本字段可以包含任何值。由于握手的双方需要区分另一端,此字段填充的数据必须足够随机(以防止与其他握手端混淆)。不过没有必要为此使用加密数据或动态数据。
C2 和 S2(1536 bytes)
+-+-+-+-+-+-+-+-+-+-+
| time (4 bytes) |
+-+-+-+-+-+-+-+-+-+-+
| time2 (4 bytes) |
+-+-+-+-+-+-+-+-+-+-+
| random bytes |
+-+-+-+-+-+-+-+-+-+-+
| random bytes |
| (cont) |
| .... |
+-+-+-+-+-+-+-+-+-+-+
-
time(4 bytes):本字段表示对端发送的时间戳(对C2来说是S1,对S2来说是C1)。 -
time2(4 bytes):本字段表示接收对端发送过来的握手包的时间戳。 -
random(1528 bytes):本字段包含对端发送过来的随机数据(对C2来说是S1,对S2来说是C1)。
握手的双方可以使用``time
和time2` 字段来估算网络连接的带宽和或延迟,但是不一定有用。
复杂握手 - complex handshake
C0 和 S0(1 byte)
+-+-+-+-+-+-+-+-+-+-+-+
| version (1 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+
-
version(1 byte):版本号,表示客户端请求的 RTMP 版本号或服务端支持的 RTMP 版本号。当前使用的版本是3。
C1 和 S1(1536 bytes)
+-+-+-+-+-+-+-+-+-+-+
| time (4 bytes) |
+-+-+-+-+-+-+-+-+-+-+
| version (4 bytes) |
+-+-+-+-+-+-+-+-+-+-+
| key (764 bytes) |
+-+-+-+-+-+-+-+-+-+-+
| digest (764 bytes)|
+-+-+-+-+-+-+-+-+-+-+
-
time(4 bytes):发送的时间戳 -
version (4 bytes):版本号- 客户端的
C1一般是0x80000702 - 服务端的
S1一般是0x04050001、0x0d0e0a0d(livego中数值)
- 客户端的
-
key (764 bytes):结构如下-
random-data: (offset) bytes -
key-data: 128 bytes -
random-data: (764 - offset - 128 - 4) bytes -
offset: 4 bytes
-
-
digest (764 bytes):结构如下-
offset: 4 bytes -
random-data: (offset) bytes -
digest-data: 32 bytes -
random-data: (764 - 4 - offset - 32) bytes
-
在不同的包里,
key和diest顺序可能会颠倒,比如nginx-rtmp
C2 和 S2(1536 bytes)
+-+-+-+-+-+-+-+-+-+-+-+-+-+
| random-data (1504 bytes)|
+-+-+-+-+-+-+-+-+-+-+-+-+-+
| digest-data (32 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+
-
random-data和digest-data都应来自对应的数据(对C2来说是S1,对S2来说是C1)
digest的计算方式较为复杂,后续再补全
握手流程设计的理解
-
C0和S0是版本数据包 -
C1和S1是带有验证数据的包 -
S2和C2是验证C1和S1的数据包
实现选用的流程
为了方便开发,在实现上,我们选用以下握手流程,这样服务端可以连续发送S0,S1和S2:
-
Client-->Server: 发送一个创建流的请求(C0、C1) -
Server-->Client: 返回一个流的索引号(S0、S1、S2)。 -
Client-->Server: 开始发送 (C2) -
Client-->Server: 发送音视频数据(这些包用流的索引号来唯一标识)