RTMP 协议 (Real-Time Messaging Protocol)
协议简介
RTMP协议是一个互联网TCP/IP五层体系结构中应用层的协议.
RTMP协议中基本的数据单元称为消息(Message).
当RTMP协议在互联网中传输数据的时候,消息会被拆分成更小的单元,称为消息块(Chunk)
RTMP有几个变种版本
1.原始版本
基于TCP,默认端口1935
2.RTMPS RTMP over TLS/SSL
3.RTMPE
4.RTMPT 封装于HTTP请求中
5.RTMFP RTMP over UDP
RTMP 将传输的流(stream)分成片段(fragment),其大小由服务器和客户端之间动态协商,
默认的fragment大小为:
音频数据: 64 bytes
视频数据: 128 bytes
PS:以下内容如无特殊说明 数据 均为 大端表示法
RTMP Chunk(RTMP消息块)
数据的发送并不是以message为单位的,而是将message拆分成chunk(>=1),chunk按序发送,接收端再根据 MsgStreamID 还原成1个message
在网络上传输数据时,消息需要被拆分成数据块(Chunk).
| ChunkHeader |
+------------------------------------------------------------------------------------+
| ChunkBasicHeader | ChunkMsgHeader | ExtendedTimeStamp | ChunkData |
+-----------------------------------------------------------------------------------+
解释:
ChunkBasicHeader: 块基本头, 包含 『块流ID』和『块类型』(4中类型),每种类型的块**必须包含**
块基本头示意:
+-----------------------------------+
| fmt(2 bits) | csid(变长) |
+-----------------------------------+
ftm: 表示块类型
csid: chunk stream id (小端表示法) 块流ID 范围[3, 65599] [0,2]为RTMP协议保留表示特殊信息
块基本头长度可能为1、2、3字节
1). ChunkBasicHeader 为 1 字节时, fmt占2 bits,csid占6 bits
csid in [0, 63]
2). ChunkBasicHeader 为 2 字节时, 第一字节除去fmt外全置0, 剩下 1 字节用来表示csid
csid in [64, 2^8 + 64 = 319]
3). ChunkBasicHeader 为 3 字节时,第一字节除去fmt外全置1, 剩下2字节用来表示csid
csid in [64, 2^16 + 64 = 65599]
对于出现重叠的情况,应当使用使header**尽可能小**的实现
代码逻辑上可以这样处理:
1).先接收1字节,分别取高两位和低六位保存起来,
2).判断低六位是全0还是全1,如果是全0,表示ChunkBasicHeader长度为2字节,那么再接收1个字节,
并且csid需要加上64,如果全为1,则表示长度为3字节,需要在接收2字节
ChunkMsgHeader: 块消息头, 发送消息的信息,依据块基本头中的『块类型』分为4种:
块类型0: 11 bytes 流开始的第一个块必须使用这种类型,流时间戳回退时也必须使用这种类型头(backward seek)
+------------------------------------------------------------------------+
| TimeStamp(3 bytes) | MsgLength(3 bytes) | MsgTypeID(1 byte) |
+------------------------------------------------------------------------+
| MsgStreamID(4 bytes)|
+-----------------------+
块类型1: 7 bytes 省去了MsgStreamID 表示此chunk和上一次发送的chunk属于同一个流
message大小变化的流的第一个消息块之后的每一个消息的第一个块应该使用这种头
+-------------------------------------------------------------------------------+
| TimeStampDelta(3 bytes) | MsgLength(3 bytes) | MsgTypeID(1 byte) |
+-------------------------------------------------------------------------------+
块类型2: 3 bytes 表示此chunk和上一次发送的chunk的 MsgTypeID, MsgLength, MsgStreamID 相同,
message大小不变的流的第一条message之后的每条message的第一个chunk
+-------------------------------+
| TimeStampDelta(3 bytes) |
+-------------------------------+
块类型3: 0 byte
没有头 表示此chunk的 ChunkMsgHeader 和上一个完全一样, 表示此chunk时上一个chunk的分块
TimeStamp: 时间戳
MsgLength: 此chunk所属Message总长度,而非 ChunkData 长度
MsgTypeID: 消息类型
MsgStreamID: 消息流ID (小端表示法)
TimeStampDelta: 时间戳增量,溢出时该字段全置1并使用 ExtendedTimeStamp 字段 存储的是完整值
ExtendedTimeStamp: 扩展时间戳,TimeStamp溢出时使用
ChunkData: 块数据 大小在 [0, chunksize] 之间 chunksize由控制消息 set chunk size 决定(见后面描述)
消息分块
消息过长时需要进行分块,消息负载(PayLoad)部分被分割成大小固定的数据块(Chunk)(128 by default),并在其首部加上消息块首部(ChunkHeader).
1.分块前
| 307 |
+-----------------------------------------------------------------------------------+
| MsgHeader | MsgBody |
+-----------------------------------------------------------------------------------+
2.分块后
| 128 | | 128 |
+-------------------------------+ +-------------------------------+
|ChunkBasicHeader| MsgBody | |ChunkBasicHeader| MsgBody |
+-------------------------------+ +-------------------------------+
| 51 |
+-------------------------------+
|ChunkBasicHeader| MsgBody |
+-------------------------------+
RTMP传输媒体数据的过程中,发送端首先把媒体数据封装成消息,然后把消息分割成消息块,最后将分割后的消息块通过TCP协议发送出去.
接收端在通过TCP协议收到数据后,首先把消息块重新组合成消息,然后通过对消息进行解封装处理就可以恢复出媒体数据.
接收RTMP Chunk的代码处理可以参考如下:
//| ChunkHeader |
// +------------------------------------------------------------------------------------+
// | ChunkBasicHeader | ChunkMsgHeader | ExtendedTimeStamp | ChunkData |
// +-----------------------------------------------------------------------------------+
BOOL RTMP_SOCKET::RecvChunk(LPRTMP_Packet& pPacket)
{
// ChunkHeader
int nChannel = 0; // 低6位
unsigned char nHeadFMT = 0; // 高2位
// 接收Chunk Basic Header的第一个字节
// +-----------------------------------+
// | fmt(2 bits) | csid(变长) |
// +-----------------------------------+
char BasicHeaderOneByte;
if(!RecvBlock(&BasicHeaderOneByte, sizeof(BasicHeaderOneByte)))
{
return FALSE;
}
nHeadFMT = (BasicHeaderOneByte & 0xC0) >> 6; //C0: 1100 0000;
nChannel = (BasicHeaderOneByte & 0x3F); //3F: 0011 1111;
switch(nChannel)
{
case 0:
{
// 全0
// 接收 csid 额外1字节
char CsidExt;
if(!RecvBlock(&CsidExt, sizeof(CsidExt)))
{
RTMP_DEBUG_ERR(WSAGetLastError());
return FALSE;
}
nChannel = CsidExt + 64;
break;
}
case 1:
{
// 全1
// 接收 csid 额外2字节
char Csid[2] = { 0 };
if(!RecvBlock(Csid, sizeof(Csid)))
{
RTMP_DEBUG_ERR(WSAGetLastError());
return FALSE;
}
nChannel = (Csid[1] << 8) + Csid[0] + 64;
}
break;
default:
break;
}
// 使用该通道上一个消息的值,因为存在消息分块,需要整合
pPacket = &m_ChannelsInMap[nChannel];
pPacket->ChunkStreamID = nChannel;
pPacket->ChunkMsgHeaderType = nHeadFMT;
// Chunk Message Header
unsigned char PacketHeader[12] = { 0 };
// const unsigned char RTMP_PACKET_SIZE[4] = {12, 8, 4, 1};
unsigned char PacketSize = RTMP_PACKET_SIZE[nHeadFMT];
PacketSize--;
// 块类型为0,则为TimeStamp而非TimeStampDelta
pPacket->HasAbsTimestamp = (nHeadFMT == 0);
if(!RecvBlock((char*)PacketHeader, PacketSize))
{
RTMP_DEBUG_ERR(WSAGetLastError());
return FALSE;
}
int nTimeStamp = 0;
unsigned char *p = PacketHeader;
if(PacketSize >= 3)
{ // 块类型 0,1,2
nTimeStamp = _RTMP_INT24(p); // TimeStamp/TimeStampDelta 3字节
p += 3;
if(PacketSize >= 6)
{ // 块类型0,1
pPacket->MsgBodySize = _RTMP_INT24(p); // MsgLength 3字节
pPacket->BytesRead = 0;
p += 3;
if(pPacket->MsgBody != NULL)
{
_SAFE_DELETE_ARRAY(pPacket->MsgBody);
}
if(PacketSize > 6)
{ // 块类型0,1
pPacket->MsgTypeID = p[0]; // MsgTypeID 1字节
p++;
if(PacketSize == 11)
{ // 块类型0
unsigned int MsgStreamID = 0;
::memcpy(&MsgStreamID, p, 4);
pPacket->MsgStreamID = MsgStreamID; // MsgStreamID 4字节
p += 4;
}
}
}
// ChunkHeader接收完毕
// 接收ExtendedTimeStamp
if(nTimeStamp >= 0xffffff)
{
// ExtendedTimeStamp 4字节
int nTimestampExtends;
if(!RecvBlock((char*)&nTimestampExtends, sizeof(nTimestampExtends)))
{
RTMP_DEBUG_ERR(WSAGetLastError());
return FALSE;
}
nTimeStamp = ::ntohl(nTimestampExtends);
}
}
if(pPacket->HasAbsTimestamp)
{
pPacket->TimeStamp = nTimeStamp;
}
else
{
pPacket->TimeStamp += nTimeStamp;
}
if(pPacket->MsgBodySize>0 && pPacket->MsgBody == NULL)
{
RTMP_ALLOC_BYTE(pPacket->MsgBody, pPacket->MsgBodySize + 1);
}
int nReadChunkSize = m_nInChunkSize;
int nToRead = pPacket->MsgBodySize - pPacket->BytesRead;
if(nToRead < nReadChunkSize)
{
nReadChunkSize = nToRead;
}
if(pPacket->ChunkData)
{
/* Does the caller want the raw chunk? */
}
if(!RecvBlock(pPacket->MsgBody + pPacket->BytesRead, nReadChunkSize))
{
ASSERT(FALSE);
RTMP_DEBUG_ERR(WSAGetLastError());
return FALSE;
}
pPacket->BytesRead += nReadChunkSize;
return TRUE;
}
RTMP流媒体播放过程:
// ToDo
1.握手:
C0 & S0: 1 byte 仅包含rtmp版本信息
+-------------+
| version |
+-------------+
C1 & S1: 1536 bytes
1).Simple HandShake
+---------------------------+
| time(4 bytes) |
+---------------------------+
| zero(4 bytes) |
+---------------------------+
| random bytes(1528 bytes) |
+---------------------------+
time: 时间戳, 可以为0,用作流起始时间标志
zero; 全为0
Simple HandShake:
random bytes: 随机数据
2).Complex HandShake(随机部分需要进行摘要和加密)
两种情况:
a).
+---------------------------+
| time(4 bytes) |
+---------------------------+
| version(4 bytes) |
+---------------------------+
| key(764 bytes) |
+---------------------------+
| digest(764 bytes) |
+---------------------------+
b).
+---------------------------+
| time(4 bytes) |
+---------------------------+
| version(4 bytes) |
+---------------------------+
| digest(764 bytes) |
+---------------------------+
| key(764 bytes) |
+---------------------------+
key结构:
+-------------------------------------+
| offset(offset bytes) |
+-------------------------------------+
| public key(128 bytes) |
+-------------------------------------+
| random data(764-offset-128-4 bytes) |
+-------------------------------------+
| offset(4 bytes) |
+-------------------------------------+
digest结构:
+-------------------------------------+
| offset(4 bytes) |
+-------------------------------------+
| random data(offset bytes) |
+-------------------------------------+
| digest data(32 bytes) |
+-------------------------------------+
| random data(764-4-offset-32 bytes) |
+-------------------------------------+
C2 & S2: 1536 bytes 同 C1 S1 长度
1).Simple HandShake
+---------------------------+
| time(4 bytes) |
+---------------------------+
| time2(4 bytes) |
+---------------------------+
| random echo(1528 bytes) |
+---------------------------+
time: 为 C1(对S2来说)的time值, 或 S1(对C2来说)的time值
time2: 接收到C1/S1时的时间
2).Complex HandShake 提供对C1 / S1的验证
+-------------------------------------+
| random data(1504 bytes) |
+-------------------------------------+
| digest data(32 bytes) |
+-------------------------------------+
C1/S1 C2/S2 生成算法
可以参考这篇博客,讲的很清
http://blog.youkuaiyun.com/win_lin/article/details/13006803/
这里也简单说一下(PS,服务端的握手流程):
为了减少发送次数,一般流程为客户端发送c0c1,服务端接收处理后发送s0s1s2,客户端处理后发送c2,服务端处理c2后握手完成
一.简单握手:
1.接收c0c1,并记录接收到的时间,判断rtmp版本,判断zero区是否全0(非全0表示复杂握手)
2.生成s0s1s2,生成方法如上图所示,发送s0s1s2
3.接收c2,验证c2的time是否与s1的time相等,time2字段是否与接收c0c1时的时间相等,随机数据是否与s1的随机数据相等
4.完成握手
//////////////////////////////////////////////////////////////////////////
// 简单握手
BOOL RTMP_SimpleHandshake::ShakeHandWithClient(RTMP_SOCKET* pSvrSocket)
{
unsigned int recvC1Time = 0;
C0C1C2 c0_c1_c2;
S0S1S2 s0_s1_s2;
char* pC0 = NULL;
char* pC1 = NULL;
char* pC2 = NULL;
char* pS0S1S2 = NULL;
RTMP_ALLOC_BYTE(c0_c1_c2.ptr, 1 + RTMP_SIG_SIZE * 2);
pC0 = c0_c1_c2.ptr;
pC1 = c0_c1_c2.ptr + 1;
pC2 = pC1 + RTMP_SIG_SIZE;
// 1.recv c0c1
if (!pSvrSocket->RecvBlock(c0_c1_c2.ptr, 1 + RTMP_SIG_SIZE))
{
return FALSE;
}
recvC1Time = htonl(timeGetTime());
c0_c1_c2.version = pC0[0];
// 暂时只提供 原始版本 rtmp 支持
if (c0_c1_c2.version != RTMP_VERSION)
{
return FALSE;
}
// 处理c1
::memcpy(&c0_c1_c2.time, pC1, 4);
int nZero = 0;
if (::memcmp(pC1, &nZero, 4) != 0)
{ // zero区非全0,代表使用复杂握手
return FALSE;
}
// 2.send s0s1s2
pS0S1S2 = s0_s1_s2.MakeS0S1S2(&c0_c1_c2, recvC1Time);
if (!pSvrSocket->SendBlock(pS0S1S2, RTMP_SIG_SIZE * 2 + 1))
{
return FALSE;
}
// 3.rcv c2
if (!pSvrSocket->RecvBlock(pC2, RTMP_SIG_SIZE))
{
return FALSE;
}
// Time 字段必须为S1的Time字段的值
if (::memcmp(pC2, pS0S1S2 + 1, 4) != 0)
{
return FALSE;
}
// Time2 字段必须为接收到来自对端的上一个包的时间
if (::memcmp(pC2 + 4, &recvC1Time, 4) != 0)
{
return FALSE;
}
// random echo 必须与S1的random data相等
if (::memcmp(pC2 + 8, pS0S1S2 + 1 + RTMP_SIG_SIZE + 8, RTMP_SIG_SIZE - 8) != 0)
{
return FALSE;
}
return TRUE;
}
char* s0s1s2::MakeS0S1S2( LPC0C1C2 pC0C1C2 , unsigned int c1upTime)
{
RTMP_ALLOC_BYTE(ptr, 1 + RTMP_SIG_SIZE * 2);
char* pS0 = ptr;
char* pS1 = pS0 + 1;
char* pS2 = pS1 + RTMP_SIG_SIZE;
unsigned int Time = htonl(timeGetTime());
// s0
*pS0 = pC0C1C2->version;
// s1
::memcpy(pS1, &Time, 4);
WriteRandomBytes(pS1 + 4, RTMP_SIG_SIZE - 4);
// s2
::memcpy(pS2, &pC0C1C2->time, 4);
::memcpy(pS2 + 4, &c1upTime, 4);
::memcpy(pS2 + 8, pC0C1C2->ptr + 1537 + 8, RTMP_SIG_SIZE - 8); // C1 random data
return ptr;
}
二.复杂握手:
1.接收c0c1,记录接收到的时间,判断rtmp版本0x03表示使用明文,0x06表示使用openssl加密
2.生成s0s1s2,s0写入rtmp版本,s1先全部写入随机数据,再拷贝接收到c0c1的时间到s1的time字段,写入version字段一般为0x04050001
3.分别对c1尝试两种格式,确定采用的哪一种格式,c1的摘要偏移量以及c1的key偏移量,然后通过c1的key计算得到s1的key,分别将key和key偏移量写入s1
4.计算得到摘要偏移量,用被分割的两部分随机数据计算得到s1摘要,写入编译量和摘要,验证s1
5.写入随机数据到s2,通过c1的key创建临时key,再通过临时key得到s2的摘要,写入s2,验证s2,到此可以发送s0s1s2了
6.接收c2,验证c2
//////////////////////////////////////////////////////////////////////////
// 复杂握手
BOOL RTMP_ComplexHandShake::ShakeHandWithClient(RTMP_SOCKET* pSvrSocket)
{
// 1.recv c0c1
unsigned int recvC1Time = 0;
C0C1C2 c0_c1_c2;
S0S1S2 s0_s1_s2;
BOOL bEncrypted = FALSE; // 是否使用openssl加密
char* pC0 = NULL;
char* pC1 = NULL;
char* pC2 = NULL;
char* pS0 = NULL;
char* pS1 = NULL;
char* pS2 = NULL;
int offalg = 0;
getoff* getkeyoffset = NULL;
getoff* getdigoffset = NULL;
RTMP_ALLOC_BYTE(c0_c1_c2.ptr, 1 + RTMP_SIG_SIZE * 2);
RTMP_ALLOC_BYTE(s0_s1_s2.ptr, 1 + RTMP_SIG_SIZE * 2);
pC0 = c0_c1_c2.ptr;
pC1 = pC0 + 1;
pC2 = pC1 + RTMP_SIG_SIZE;
pS0 = s0_s1_s2.ptr;
pS1 = pS0 + 1;
pS2 = pS1 + RTMP_SIG_SIZE;
// 1.读取 c0c1
if (!pSvrSocket->RecvBlock(c0_c1_c2.ptr, 1 + RTMP_SIG_SIZE))
{
return FALSE;
}
recvC1Time = htonl(timeGetTime());
c0_c1_c2.version = pC0[0];
// 暂时只提供 原始版本 rtmp 支持
if (c0_c1_c2.version == RTMP_VERSION)
{ // 使用明文
bEncrypted = FALSE;
}
else if (c0_c1_c2.version == RTMP_VERSION_OPENSSL)
{ // 使用openssl加密
bEncrypted = TRUE;
}
else
{
return FALSE;
}
::memset(&pSvrSocket->m_Link.rc4keyIn, 0, sizeof(RC4_KEY));
::memset(&pSvrSocket->m_Link.rc4keyOut, 0, sizeof(RC4_KEY));
::memcpy(&c0_c1_c2.time, pC1, 4);
// S0
*pS0 = c0_c1_c2.version;
// S1
WriteRandomBytes(pS1, RTMP_SIG_SIZE);
// S1 time
::memcpy(pS1, &recvC1Time, 4);
int nZero = 0;
if (::memcmp(pC1 + 4, &nZero, 4) == 0)
{ // zero区全0,代表使用简单握手
return FALSE;
}
else
{
// version 0x04050001
char* pTmp = pS1 + 4;
pTmp[0] = 0x4;
pTmp[1] = 0x5;
pTmp[2] = 0x0;
pTmp[3] = 0x1;
// schema1
getdigoffset = digoff[offalg];
getkeyoffset = keyoff[offalg];
}
int nC1KeyOffset = 0;
int nC1DigestOffset = 0;
int nS1KeyOffset = 0;
int nS1DigestOffset = 0;
// 检测C1采用的schema格式
// 先尝试schema1
nC1DigestOffset = getdigoffset((unsigned char*)pC1, RTMP_SIG_SIZE);
if (!VerifyDigest(nC1DigestOffset, (unsigned char*)pC1, GenuineFPKey, 30))
{ // 再尝试schema0
offalg ^= 1;
getdigoffset = digoff[offalg];
getkeyoffset = keyoff[offalg];
nC1DigestOffset = getdigoffset((unsigned char*)pC1, RTMP_SIG_SIZE);
if (!VerifyDigest(nC1DigestOffset, (unsigned char*)pC1, GenuineFPKey, 30))
{
ASSERT(FALSE);
return FALSE;
}
}
if (bEncrypted)
{
int nS1KeyLength = 0;
unsigned char S1KeyData[128] = { 0 }; // S1 key
if ((pSvrSocket->m_Link.dh = DHInit(1024)) == NULL)
{
RTMP_DEBUG_LOG("%s: Couldn't initialize Diffie-Hellmann!",__FUNCTION__);
return FALSE;
}
// 计算相对于C1的key_offset
nC1KeyOffset = getkeyoffset((unsigned char*)pC1, RTMP_SIG_SIZE);
// 由C1 key计算得到S1 key
nS1KeyLength = DHComputeSharedSecretKey((DH*)pSvrSocket->m_Link.dh,
(unsigned char*)pC1 + nC1KeyOffset,
128,
S1KeyData);
if (nS1KeyLength < 0 || nS1KeyLength > 128)
{
RTMP_DEBUG_LOG("Caculate S1 Key Data Failed %s", __FUNCTION__);
return FALSE;
}
if (offalg == 1)
{ //schema 0
int nTmp = htonl(nS1KeyOffset);
::memcpy(pS1 + 8 + 764 - 4, &nTmp, 4);
nS1KeyOffset = getkeyoffset((unsigned char*)pS1, RTMP_SIG_SIZE);
::memcpy(pS1 + nS1KeyOffset, S1KeyData, sizeof(S1KeyData));
}
else if (offalg == 0)
{ //schema 1
int nTmp = htonl(nS1KeyOffset);
::memcpy(pS1 + RTMP_SIG_SIZE - 4, &nTmp, 4);
nS1KeyOffset = getkeyoffset((unsigned char*)pS1, RTMP_SIG_SIZE);
::memcpy(pS1 + nS1KeyOffset, S1KeyData, sizeof(S1KeyData));
}
else
{
ASSERT(FALSE);
}
}
// 获取S1 digest_offset
nS1DigestOffset = getdigoffset((unsigned char*)pS1, RTMP_SIG_SIZE);
// 用S1的randomdata1和randomdata2计算得到S1 digest
CalculateDigest(
nS1DigestOffset,
(unsigned char*)pS1,
GenuineFMSKey, 36,
(unsigned char*)(pS1 + nS1DigestOffset));
// 3.验证 s1 digest 算法相同
// 用S1的randomdata1和randomdata2计算得到digest
// 4.生成s2
// s2结构: randomdata(1504 bytes) digest(32 bytes)
WriteRandomBytes(pS2, RTMP_SIG_SIZE);
unsigned char TempKey[32] = { 0 };
// 根据c1的digest创建临时key
HMACsha256(
(unsigned char*)pC1 + nC1DigestOffset,
32,
GenuineFMSKey,
sizeof(GenuineFMSKey),
TempKey);
HMACsha256(
(unsigned char*)pS2,
RTMP_SIG_SIZE - 32,
TempKey,
sizeof(TempKey),
(unsigned char*)pS2 + RTMP_SIG_SIZE - 32);
// 验证s2 digest
// ...
// 5.send s0s1s2
if (!pSvrSocket->SendBlock(pS0, 1 + 2 * RTMP_SIG_SIZE))
{
return FALSE;
}
// 6.rcv c2
if (!pSvrSocket->RecvBlock(pC2, RTMP_SIG_SIZE))
{
return FALSE;
}
// verify c2
// never verify c2, for ffmpeg will failed.
// it's ok for flash.
return TRUE;
}
## RTMP 协议
RTMP 报文结构:
RTMP 消息(基本数据单元)
| MsgHeader |
+------------------------------------------------------------------------------------+
| MsgTypeID(1 byte) | PayloadLength(3 bytes) | TimeStamp(4 bytes) | StreamID(3 bytes)|
+------------------------------------------------------------------------------------+
| MsgBody(PayloadLength bytes) |
+---------------------------------+
解释:
MsgTypeID: 消息类型ID 1 byte,标志不同种类的消息. MsgTypeID in [1,7] 用于 控制消息,
MsgTypeID=8 用于传输音频, MsgTypeID=9 用于传输视频
MsgTypeID in [15,20] 用于 发送AMF编码的命令,负责用户与服务器间的交互(播放,暂停etc.)
PayloadLength: 载荷长度(MsgBody的长度) 3 bytes
TimeStamp: 时间戳 4 bytes
MsgBody: 载荷 *控制消息类型的载荷长度一定*
MsgStreamID: 消息流ID 4 bytes (小端法表示) 当MsgTypeID in [1,7]时,MsgStreamID 恒为 0, 称为 控制流
控制消息类型解释:
***『控制消息的 MsgStreamID=0, chunk stream id=2』***
MsgTypeID 1:
设置块大小(set chunk size) 设置新的块最大值
载荷示意:
+---------------------------+
| chunk size(4 bytes) |
+---------------------------+
MsgTypeID 2:
取消消息(abort message) 发送端将 块流ID(chunk stream id)发送给接收端,告知余下部分不再发送
载荷示意:
+-------------------------------+
| chunk stream id(4 bytes) |
+-------------------------------+
MsgTypeID 3;
ACK 确认消息 对等端(peer)收到的字节等同于窗口大小(window size)字节时,发送 消息序列号 给对等端
载荷示意:
+-------------------------------+
| sequence number(4 bytes) |
+-------------------------------+
MsgTypeID 4:
用户控制消息
载荷示意:
+---------------------------------------+
| event type(2 byte) | event data |
+---------------------------------------+
event data 变长字节
MsgTypeID 5:
设置确认窗口大小(acknowledgment window size)
载荷示意:
+---------------------------------------+
| acknowledgment window size(4 bytes) |
+---------------------------------------+
MsgTypeID 6:
设置对等端带宽 带宽值与窗口大小值相同,如果对等端接收此消息时收到的值与窗口大小不同,则发回 消息5(设置确认窗口大小)
载荷示意:
+-------------------------------------------------------+
| acknowledgment window size(4 bytes) | limit type |
+-------------------------------------------------------+
limit type: Hard(0) Soft(1) Dynamic(2)
## RTMP 命令消息
PS: 均为AMF编码
1.网络连接命令(Net Connection Command):
connect
客户端–>服务器
CommandName “connect”
TransactionID 1
CommandObject
Optional UserArg
服务器-->客户端
CommandName "_result" / "_error"
TransactionID 1
Properties "fmsver" 等字段.
Information "code" "level" "description" 等字段
call
close
createStream
客户端-->服务器
CommandName "createStream"
TransactionID ID
CommandObject
服务器-->客户端
CommandName "_result" / "_error"
TransactionID ID
CommandObject
StreamID 流ID
2.网络流命令(Net Stream Command):
play 播放流, 多次使用可以创建播放列表
客户端-->服务器
CommandName "play"
TransactionID 0
CommandObject null
StreamName eg: xxx(for flv) or mp3:xxx(for mp3) mp4:xxx.m4v(for H.264/AAC)
Start 开始时间(s) 默认-2
Duration 默认-1
Reset 是否刷新之前的播放列表
服务器-->客户端
CommandName "onStatus"
Description NetStream.Play.Start / NetStream.Play.StreamNotFound
play2
deleteStream
客户端-->服务器
CommandName "deleteStream"
TransactionID 0
CommandObject null
StreamID 要被销毁的流ID
服务器-->客户端 none
closeStream
receiveAudio
receiveVideo
publish
seek
pause
“`
AMF 编码 (AMF0 AMF3) 大端法存储
AMF0
U8 unsigned 8-bit data
U16 unsigned 16-bit data
S16 signed 16-bit data
U32 unsigned 32-bit data
DOUBLE 8-byte 双精度浮点型
KB 1024-byte
GB 1024*1024-KB
UTF-8 U16 * (UTF8-char)
UTF-8-long U32 * (UTF8-char)
UTF-8-empty U16
PS: UTF8-char = UTF8-1 | UTF8-2 | UTF8-3 | UTF8-4
Marker: U8
number-marker = 0x00
boolean-marker = 0x01
string-marker = 0x02
object-marker = 0x03
movieclip-marker = 0x04
null-marker = 0x05
undefined-marker = 0x06
reference-marker = 0x07
ecma-array-marker = 0x08
object-end-marker = 0x09
strict-array-marker = 0x0A
date-marker = 0x0B
long-string-marker = 0x0C
unsupported-marker = 0x0D
recordset-marker = 0x0E
xml-document-marker = 0x0F
typed-object-marker = 0x10
avmplus-object-marker = 0x11
number-type = number-marker DOUBLE (1 + 8 字节)
boolean-type = boolean-marker U8 (0 false, !0 true)
string-type = string-marker UTF-8
AMF3
// To Do
本文介绍了RTMP(实时消息协议)的基本概念,包括协议简介和RTMP Chunk(消息块)的处理,特别是消息分块的接收过程。此外,还探讨了AMF(Action Message Format)编码,包括AMF0和AMF3两种编码格式,强调其在大端法存储中的应用。通过阅读,读者将能够理解RTMP协议的握手过程和AMF编码的原理。
6794





