LibRTMP源代码分析8:发送消息

在详细分析函数RTMP_SendPacket()之前,让我们来看看RTMPPacket结构体的定义:

typedef struct RTMPPacket
{
        uint8_t   m_headerType; // ChunkMsgHeader类型(4种)
        uint8_t   m_packetType; // Message type ID(1-7协议控制;8,9音视频;10以后为AMF编码消息)
        uint8_t   m_hasAbsTimestamp; // Timestamp 是绝对值还是相对值?
        int         m_nChannel; // 块流ID (3 <= ID <= 65599)
        uint32_t m_nTimeStamp; // Timestamp,时间戳
        int32_t   m_nInfoField2; // last 4 bytes in a long header,消息流ID 
        uint32_t  m_nBodySize; // 消息长度
        uint32_t  m_nBytesRead; // 该RTMP包数据已经读取到m_body中的字节数
        RTMPChunk *m_chunk; 
        char      *m_body; // 存放实际消息数据的缓冲区
} RTMPPacket;

下面我们来看看RTMP_SendPacket(),各种RTMPPacket(即各种Chunk)都需要用这个函数进行发送。

/**
 * @brief 将RTMP包按照协议分块发送出去
 *
 * @param r :RTMP上下文参数
 * @param packet : RTMP包,含有带发送的消息负载数据。
 * @param queue  :远程方法队列,仅当packet的类型为invoke时使用。
 *
 * @return 1 成功,失败返回0。
 */
int RTMP_SendPacket(RTMP *r, RTMPPacket *packet, int queue)
{
const RTMPPacket *prevPacket = r->m_vecChannelsOut[packet->m_nChannel];
uint32_t last = 0, t;
char *header, *hptr, *hend, hbuf[RTMP_MAX_HEADER_SIZE], c;
char *buffer, *tbuf = NULL, *toff = NULL;
int    nSize, hSize, cSize;
int    nChunkSize, tlen;

// 前一个packet存在且不是完整的ChunkMsgHeader,因此有可能需要调整块消息头的类型
if (prevPacket && packet->m_headerType != RTMP_PACKET_SIZE_LARGE)
        {
/* compress a bit by using the prev packet's attributes */
// 获取ChunkMsgHeader类型,前一个Chunk与当前Chunk比较
// 如果前后两个块的大小、包类型及块头类型都相同,则将块头类型fmt设为2,
// 即可省略消息长度、消息类型id、消息流id
// 可以参考官方协议:流的分块 --- 6.1.2.3节
if (prevPacket->m_nBodySize  == packet->m_nBodySize  && 
prevPacket->m_packetType == packet->m_packetType && 
packet->m_headerType     == RTMP_PACKET_SIZE_MEDIUM )
packet->m_headerType = RTMP_PACKET_SIZE_SMALL;

// 前后两个块的时间戳相同,且块头类型fmt为2,则相应的时间戳也可省略,因此将块头类型置为3
// 可以参考官方协议:流的分块 --- 6.1.2.4节
if (prevPacket->m_nTimeStamp == packet->m_nTimeStamp && 
   packet->m_headerType == RTMP_PACKET_SIZE_SMALL)
packet->m_headerType = RTMP_PACKET_SIZE_MINIMUM;

// 前一个包的时间戳
last = prevPacket->m_nTimeStamp;
        }

// 块头类型fmt取值0、1、2、3,超过3就表示出错
if (packet->m_headerType > 3) /* sanity */
{
RTMP_Log(RTMP_LOGERROR, "sanity failed!! trying to send header of type: 0x%02x.", 
 (unsigned char)packet->m_headerType);
return FALSE;
}

// 块头初始大小 = 基本头(1字节) + 块消息头大小(11/7/3/0) = [12, 8, 4, 1]
// 块基本头是1-3字节,因此用变量cSize来表示剩下的0-2字节
// nSize 表示块头初始大小, hSize表示块头大小
nSize = packetSize[packet->m_headerType];
hSize = nSize;
cSize = 0;

// 时间戳增量
t = packet->m_nTimeStamp - last;

if (packet->m_body)
{
// m_body是指向负载数据首地址的指针;“-”号用于指针前移
header = packet->m_body - nSize; // 块头的首指针
hend    = packet->m_body; // 块头的尾指针
}
else
{
header = hbuf + 6;
hend   = hbuf + sizeof(hbuf);
}

// 块流id(cs id)大于319,则块基本头占3个字节
if (packet->m_nChannel > 319)
cSize = 2;
// 块流id(cs id)在64与319之间,则块基本头占2个字节
else if (packet->m_nChannel > 63)
cSize = 1;

// ChunkBasicHeader的长度比初始长度还要长
if (cSize)
        {
header -= cSize; // header指向块头
hSize  += cSize; // hSize加上ChunkBasicHeader的长度(比初始长度多出来的长度)
       }

// nSize > 1表示块消息头至少有3个字节,即存在timestamp字段
// 相对TimeStamp大于0xffffff,此时需要使用ExtendTimeStamp 
if (nSize > 1 && t >= 0xffffff)
{
header -= 4;
hSize  += 4;
}

hptr = header;
c = packet->m_headerType << 6; // 把ChunkBasicHeader的Fmt类型左移6位

// 设置basic header的第一个字节值,前两位为fmt. 可以参考官方协议:流的分块 --- 6.1.1节
switch (cSize)
{
case 0: // 把ChunkBasicHeader的低6位设置成ChunkStreamID( cs id )
c |= packet->m_nChannel;
break;
case 1: // 同理,但低6位设置成000000
break;
case 2: // 同理,但低6位设置成000001
c |= 1;
break;
}

// 可以拆分成两句*hptr=c; hptr++,此时hptr指向第2个字节 
*hptr++ = c;

// 设置basic header的第二(三)个字节值
if (cSize)
{
// 将要放到第2字节的内容tmp
int tmp = packet->m_nChannel - 64;

// 获取低位存储与第2字节
*hptr++ = tmp & 0xff;

// ChunkBasicHeader是最大的3字节时
if (cSize == 2)
*hptr++ = tmp >> 8; // 获取高位存储于最后1个字节(注意:排序使用大端序列,和主机相反)
       }

// ChunkMsgHeader长度为11、7、3, 都含有timestamp(3字节)
if (nSize > 1)
{
// 将时间戳(相对或绝对)转化为3个字节存入hptr,如果时间戳超过0xffffff,则后面还要填入Extend Timestamp
hptr = AMF_EncodeInt24(hptr, hend, t > 0xffffff ? 0xffffff : t);
}

// ChunkMsgHeader长度为11、7,都含有 msg length + msg type id
if (nSize > 4)
       {
// 将消息长度(msg length)转化为3个字节存入hptr
hptr = AMF_EncodeInt24(hptr, hend, packet->m_nBodySize);
*hptr++ = packet->m_packetType; // msg type id
       }

// ChunkMsgHeader长度为11, 含有msg stream id( 小端)
if (nSize > 8)
hptr += EncodeInt32LE(hptr, packet->m_nInfoField2);

// 如果时间戳大于0xffffff,则需要写入Extend Timestamp
if (nSize > 1 && t >= 0xffffff)
hptr = AMF_EncodeInt32(hptr, hend, t);

// 到此为止,已经将块头填写好了
// 此时nSize表示负载数据的长度, buffer是指向负载数据区的指针
nSize  = packet->m_nBodySize;
buffer = packet->m_body;
nChunkSize = r->m_outChunkSize; //Chunk大小,默认是128字节

RTMP_Log(RTMP_LOGDEBUG2, "%s: fd=%d, size=%d", __FUNCTION__, r->m_sb.sb_socket, nSize);

/* send all chunks in one HTTP request,使用HTTP协议 */
if (r->Link.protocol & RTMP_FEATURE_HTTP)
{
// nSize:Message负载长度;nChunkSize:Chunk长度;  
// 例nSize:307,nChunkSize:128;  
// 可分为(307 + 128 - 1)/128 = 3个  
// 为什么加 nChunkSize - 1?因为除法会只取整数部分!
int chunks = (nSize + nChunkSize - 1) / nChunkSize;

// Chunk个数超过一个
if (chunks > 1)
{
// 注意:ChunkBasicHeader的长度 = cSize + 1
// 消息分n块后总的开销:  
// n个ChunkBasicHeader,1个ChunkMsgHeader,1个Message负载  
// 实际上只有第一个Chunk是完整的,剩下的只有ChunkBasicHeader
tlen = chunks * (cSize + 1) + nSize + hSize;
tbuf = (char *)malloc(tlen);
if (!tbuf)
return FALSE;
toff = tbuf;
}
}

// 消息的负载 + 头
while (nSize + hSize)
       {
int wrote;

// 消息负载大小 < Chunk大小(不用分块)
if (nSize < nChunkSize)
nChunkSize = nSize; // Chunk可能小于设定值

RTMP_LogHexString(RTMP_LOGDEBUG2, (uint8_t *)header, hSize);
RTMP_LogHexString(RTMP_LOGDEBUG2, (uint8_t *)buffer, nChunkSize);

// 如果r->Link.protocol采用Http协议,则将RTMP包数据封装成多个Chunk,然后一次性发送。
// 否则每封装成一个块,就立即发送出去
if (tbuf)
                {
// 将从Chunk头开始的nChunkSize + hSize个字节拷贝至toff中,
// 这些拷贝的数据包括块头数据(hSize字节)和nChunkSize个负载数据
memcpy(toff, header, nChunkSize + hSize);
toff += nChunkSize + hSize;
}
else // 负载数据长度不超过设定的块大小,不需要分块,因此tbuf为NULL;或者r->Link.protocol不采用Http
                {
// 直接将负载数据和块头数据发送出去
wrote = WriteN(r, header, nChunkSize + hSize);
if (!wrote)
return FALSE;
}

nSize  -= nChunkSize; // 消息负载长度 - Chunk负载长度 
buffer += nChunkSize; // buffer指针前移1个Chunk负载长度
hSize = 0; // 重置块头大小为0,后续的块只需要有基本头(或加上扩展时间戳)即可

// 如果消息负载数据还没有发完,准备填充下一个块的块头数据
if (nSize > 0)
{
// 根据是否含有扩展时间戳及cSize的值,确定块头大小和块头指针
if(t >= 0xffffff)
{
// 初始块头 = 初始 Basic Header(1字节) + Extended Timestamp(4字节)
header = buffer - 5;
hSize = 5;
}
else
{
// 初始块头 = 初始 Basic Header(1字节)
header = buffer - 1;
hSize = 1;
}

// 如果Basic Header还有额外字节,调整块头大小
if (cSize)
{
header -= cSize;
hSize  += cSize;
}

// ChunkBasicHeader第1个字节
*header = (0xc0 | c);

// 设置basic header的第二(三)个字节值
if (cSize)
{
int tmp = packet->m_nChannel - 64;
header[1] = tmp & 0xff;
if (cSize == 2)
header[2] = tmp >> 8;
}

// 如果时间戳大于0xffffff,则将Extend Timestamp(4字节)写入进去
if(t >= 0xffffff) 
{
hptr = header + 1 + cSize;
hptr = AMF_EncodeInt32(hptr, hend, t);
}
}
}  // while (nSize + hSize)结束

// 如果tbuf为空,说明负载数据长度小于Chunk大小(采用RTMP_FEATURE_HTTP),只有一个块。
// 或者每封装好一个块就立即发送出去(在上面while中数据已经发送了);
// 如果tbuf非空,则把所有的块都封装好之后,一次性发送出去
if (tbuf)
{
int wrote = WriteN(r, tbuf, toff - tbuf);
free(tbuf);
tbuf = NULL;
if (!wrote)
return FALSE;
}

/* we invoked a remote method */
if (packet->m_packetType == RTMP_PACKET_TYPE_INVOKE)
        {
AVal method;
char *ptr;
ptr = packet->m_body + 1;
AMF_DecodeString(ptr, &method); // ptr处存放数据格式: | 字符串长度(2字节) |  实际字符串 |
RTMP_Log(RTMP_LOGDEBUG, "Invoking %s", method.av_val);

/* keep it in call queue till result arrives */
if (queue)
{
int txn;
ptr += 3 + method.av_len; // 解码前面的字符串占去了 2 + method.av_len 个字节
txn = (int)AMF_DecodeNumber(ptr);
AV_queue(&r->m_methodCalls, &r->m_numCalls, &method, txn);
}
}

if (!r->m_vecChannelsOut[packet->m_nChannel])
r->m_vecChannelsOut[packet->m_nChannel] = malloc(sizeof(RTMPPacket));
memcpy(r->m_vecChannelsOut[packet->m_nChannel], packet, sizeof(RTMPPacket));
return TRUE;
}

上面的数据块最终是通过函数WriteN()发送出去,接下来分析一下WriteN()函数:

/**
 * @brief 发送数据报
 *
 * @param r : 一个RTMP连接
 * @param buffer : 待发送的数据
 * @param n : 待发送的数据长度
 *
 * @热return 发送成功返回true,否则返回false.
 */
static int WriteN(RTMP *r, const char *buffer, int n)
{
const char *ptr = buffer;
while (n > 0)
{
int nBytes;

// 因使用的协议不同而调用不同的函数
if (r->Link.protocol & RTMP_FEATURE_HTTP) // 使用Http协议进行连接
nBytes = HTTP_Post(r, RTMPT_SEND, ptr, n);
else
nBytes = RTMPSockBuf_Send(r, &r->m_sb, ptr, n);
/*RTMP_Log(RTMP_LOGDEBUG, "%s: %d\n", __FUNCTION__, nBytes); */

// 成功发送字节数 < 0, 表示发送失败
if (nBytes < 0)
{
int sockerr = r->m_sock.getsockerr();
RTMP_Log(RTMP_LOGERROR, "%s, RTMP send error %d (%d bytes)", __FUNCTION__, sockerr, n);

// 被中断的系统调用,但不是Ctrl+C,重新发送
if (sockerr == EINTR && !RTMP_ctrlC)
continue;

RTMP_Close(r);
n = 1;
break;
}

if (nBytes == 0)
break;

n -= nBytes;
ptr += nBytes;
}

return n == 0;
}

函数WriteN()的数据是通过调用函数HTTP_Post或者RTMPSockBuf_Send发送数据的,查看HTTP_Post的代码发现该函数只是在函数WriteN()待发送的数据前添加了一些http连接信息后一起通过函数RTMPSockBuf_Send发送出去。接下来看看这两个函数:

/**
 * @brief 在待发送的数据buf前面添加http连接信息,一起发送出去。
 *  返回原始数据区buf中被发送的字节数。
 */
static int HTTP_Post(RTMP *r, RTMPTCmd cmd, const char *buf, int len)
{
char hbuf[512];
int hlen = snprintf(hbuf, sizeof(hbuf), "POST /%s%s/%d HTTP/1.1\r\n"
"Host: %.*s:%d\r\n"
"Accept: */*\r\n"
"User-Agent: Shockwave Flash\n"
"Connection: Keep-Alive\n"
"Cache-Control: no-cache\r\n"
"Content-type: application/x-fcs\r\n"
"Content-length: %d\r\n\r\n", RTMPT_cmds[cmd],
r->m_clientID.av_val ? r->m_clientID.av_val : "",
r->m_msgCounter, r->Link.hostname.av_len, r->Link.hostname.av_val,
r->Link.port, len);
RTMPSockBuf_Send(r, &r->m_sb, hbuf, hlen);
hlen = RTMPSockBuf_Send(r, &r->m_sb, buf, len);
r->m_msgCounter++;
r->m_unackd++;
return hlen;
}

/**
 * @brief Socket发送(指明套接字,buffer缓冲区,数据长度). 返回所发数据量
 */
int RTMPSockBuf_Send(RTMP *r, RTMPSockBuf *sb, const char *buf, int len)
{
int rc;
//#ifdef _DEBUG
//  fwrite(buf, 1, len, netstackdump);
//#endif
        {
// 向一个已连接的套接口发送数据。  
// int send( SOCKET s, const char * buf, int len, int flags);  
// s:一个用于标识已连接套接口的描述字。  
// buf  :包含待发送数据的缓冲区。     
// len  :缓冲区中数据的长度。  
// flags:调用执行方式。  
// rc   : 所发数据量。  
rc = r->m_sock.send(sb->sb_socket, buf, len, 0);
        }
return rc;
}

到这个函数的时候,发现一层层的调用终于完成了,最后调用了系统Socket的send()函数完成了数据的发送功能。
Socket的关闭调用也是调用了系统Socket的closesocket()函数完成关闭功能。

/**
 * @brief 关闭socket
 */
int RTMPSockBuf_Close(RTMP *r, RTMPSockBuf *sb)
{
if (sb->sb_socket != -1)
return r->m_sock.closesocket(sb->sb_socket);
return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值