【流媒体】RTMPDump—RTMP_Connect函数(握手、网络连接)
目录
- 1. RTMP_Connect函数
- 1.1 网络层连接(RTMP_Connect0)
- 1.2 RTMP连接(RTMP_Connect1)
- 1.2.1 握手(HandShake)
- 1.2.2 RTMP的NetConnection(SendConnectPacket)
- 2.小结
RTMP协议相关:
【流媒体】RTMP协议概述
【流媒体】RTMP协议的数据格式
【流媒体】RTMP协议的消息类型
【流媒体】RTMPDump—主流程简单分析
【流媒体】RTMPDump—RTMP_Connect函数(握手、网络连接)
【流媒体】RTMPDump—RTMP_ConnectStream(创建流连接)
【流媒体】RTMPDump—Download(接收流媒体信息)
【流媒体】RTMPDump—AMF编码
【流媒体】基于libRTMP的H264推流器
参考雷博的系列文章(可以从一篇链接到其他文章):
RTMPdump 源代码分析 1: main()函数
前面进行了RTMPDump主流程的分析,包括初始化和一些解析过程,现在分析RTMPDump是如何进行握手和网络连接,这是进行RTMP通信的第一步
1. RTMP_Connect函数
函数首先添加连接的地址,如果设置socksport,则使用socks连接,否则直接连接;随后,调用了RTMP_Connect0()和RTMP_Connect1()两个函数实现连接
int
RTMP_Connect(RTMP * r, RTMPPacket * cp)
{struct sockaddr_in service;if (!r->Link.hostname.av_len)return FALSE;memset(&service, 0, sizeof(struct sockaddr_in));service.sin_family = AF_INET;if (r->Link.socksport) // 如果设置了socksport,则通过socks连接{/* Connect via SOCKS */if (!add_addr_info(&service, &r->Link.sockshost, r->Link.socksport))return FALSE;}else // 否则直接连接{/* Connect directly */if (!add_addr_info(&service, &r->Link.hostname, r->Link.port))return FALSE;}// 网络层TCP连接if (!RTMP_Connect0(r, (struct sockaddr*) & service))return FALSE;r->m_bSendCounter = TRUE;// 建立RTMP连接return RTMP_Connect1(r, cp);
}
1.1 网络层连接(RTMP_Connect0)
RTMP_Connect0实现了TCP连接功能,使得client和server在网络层能够进行通信,首先使用socket()函数初始化协议为TCP,随后使用connect()进行TCP连接
int
RTMP_Connect0(RTMP * r, struct sockaddr* service)
{int on = 1;r->m_sb.sb_timedout = FALSE;r->m_pausing = 0;r->m_fDuration = 0.0;r->m_sb.sb_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 初始化为TCP连接if (r->m_sb.sb_socket != -1){if (connect(r->m_sb.sb_socket, service, sizeof(struct sockaddr)) < 0) // 进行TCP连接{int err = GetSockError();RTMP_Log(RTMP_LOGERROR, "%s, failed to connect socket. %d (%s)",__FUNCTION__, err, strerror(err));RTMP_Close(r);return FALSE;}if (r->Link.socksport){RTMP_Log(RTMP_LOGDEBUG, "%s ... SOCKS negotiation", __FUNCTION__);if (!SocksNegotiate(r)){RTMP_Log(RTMP_LOGERROR, "%s, SOCKS negotiation failed.", __FUNCTION__);RTMP_Close(r);return FALSE;}}}else{RTMP_Log(RTMP_LOGERROR, "%s, failed to create socket. Error: %d", __FUNCTION__,GetSockError());return FALSE;}/* set timeout */{SET_RCVTIMEO(tv, r->Link.timeout);if (setsockopt(r->m_sb.sb_socket, SOL_SOCKET, SO_RCVTIMEO, (char*)& tv, sizeof(tv))){RTMP_Log(RTMP_LOGERROR, "%s, Setting socket timeout to %ds failed!",__FUNCTION__, r->Link.timeout);}}setsockopt(r->m_sb.sb_socket, IPPROTO_TCP, TCP_NODELAY, (char*)& on, sizeof(on));return TRUE;
}
1.2 RTMP连接(RTMP_Connect1)
该函数主要调用了两个函数:(1)握手(HandShake);(2)RTMP的NetConnection(SendConnectPacket)
int
RTMP_Connect1(RTMP * r, RTMPPacket * cp)
{if (r->Link.protocol & RTMP_FEATURE_SSL){
#if defined(CRYPTO) && !defined(NO_SSL)TLS_client(RTMP_TLS_ctx, r->m_sb.sb_ssl);TLS_setfd(r->m_sb.sb_ssl, r->m_sb.sb_socket);if (TLS_connect(r->m_sb.sb_ssl) < 0){RTMP_Log(RTMP_LOGERROR, "%s, TLS_Connect failed", __FUNCTION__);RTMP_Close(r);return FALSE;}
#elseRTMP_Log(RTMP_LOGERROR, "%s, no SSL/TLS support", __FUNCTION__);RTMP_Close(r);return FALSE;#endif}if (r->Link.protocol & RTMP_FEATURE_HTTP){r->m_msgCounter = 1;r->m_clientID.av_val = NULL;r->m_clientID.av_len = 0;HTTP_Post(r, RTMPT_OPEN, "", 1);if (HTTP_read(r, 1) != 0){r->m_msgCounter = 0;RTMP_Log(RTMP_LOGDEBUG, "%s, Could not connect for handshake", __FUNCTION__);RTMP_Close(r);return 0;}r->m_msgCounter = 0;}RTMP_Log(RTMP_LOGDEBUG, "%s, ... connected, handshaking", __FUNCTION__);// 1. 握手if (!HandShake(r, TRUE)){RTMP_Log(RTMP_LOGERROR, "%s, handshake failed.", __FUNCTION__);RTMP_Close(r);return FALSE;}RTMP_Log(RTMP_LOGDEBUG, "%s, handshaked", __FUNCTION__);// 2. RTMP的NetConnectionif (!SendConnectPacket(r, cp)){RTMP_Log(RTMP_LOGERROR, "%s, RTMP connect failed.", __FUNCTION__);RTMP_Close(r);return FALSE;}return TRUE;
}
1.2.1 握手(HandShake)
握手是RTMP协议实现的第一个步骤,需要重点分析。值得一提的是,在雷博记录的文章当中(RTMPdump(libRTMP)源代码分析 4: 连接第一步——握手(Hand Shake)),记录的应该是包含加密过程的握手函数,不包含加密过程的握手函数应该在rtmp.c中
前面自己记录的关于握手过程的文章为【流媒体】RTMP协议概述,握手的流程为
RTMPDump中关于非加密握手的代码,如下所示
static int
HandShake(RTMP * r, int FP9HandShake)
{int i;uint32_t uptime, suptime;int bMatch;char type;// clientbuf当中包含了C0和C1数据报,并且同时发出去char clientbuf[RTMP_SIG_SIZE + 1], *clientsig = clientbuf + 1; // RTMP_SIG_SIZE = 1536char serversig[RTMP_SIG_SIZE];// 1. C0数据报// 0x03表示客户端所期望的版本号,即C0clientbuf[0] = 0x03; /* not encrypted */// 2. C1数据报// 获取当前的timestamp并且转换成为大端存储uptime = htonl(RTMP_GetTime());// 将时间戳拷贝到clientsig当中memcpy(clientsig, &uptime, 4);// 填充4个字节的0值memset(&clientsig[4], 0, 4);#ifdef _DEBUGfor (i = 8; i < RTMP_SIG_SIZE; i++)clientsig[i] = 0xff;
#elsefor (i = 8; i < RTMP_SIG_SIZE; i++) // 填充1536 - 8 = 1528个随机字节clientsig[i] = (char)(rand() % 256);
#endif// 3. 发送C0和C1数据报if (!WriteN(r, clientbuf, RTMP_SIG_SIZE + 1))return FALSE;// 4. 接收S0数据报,即server返回的可以使用的RTMP版本号if (ReadN(r, &type, 1) != 1) /* 0x03 or 0x06 */return FALSE;RTMP_Log(RTMP_LOGDEBUG, "%s: Type Answer : %02X", __FUNCTION__, type);// 检查client期望的RTMP版本号是否与server所支持的RTMP版本号匹配if (type != clientbuf[0])RTMP_Log(RTMP_LOGWARNING, "%s: Type mismatch: client sent %d, server answered %d",__FUNCTION__, clientbuf[0], type);// 5. 接收S1数据报if (ReadN(r, serversig, RTMP_SIG_SIZE) != RTMP_SIG_SIZE)return FALSE;/* decode server response */// 解析接收数据报时间戳memcpy(&suptime, serversig, 4);suptime = ntohl(suptime);RTMP_Log(RTMP_LOGDEBUG, "%s: Server Uptime : %d", __FUNCTION__, suptime);RTMP_Log(RTMP_LOGDEBUG, "%s: FMS Version : %d.%d.%d.%d", __FUNCTION__,serversig[4], serversig[5], serversig[6], serversig[7]);/* 2nd part of handshake */// 6. 发送C2数据报// 发送出去的C2数据报就是接收到的S1数据报if (!WriteN(r, serversig, RTMP_SIG_SIZE))return FALSE;// 7. 读取S2数据报if (ReadN(r, serversig, RTMP_SIG_SIZE) != RTMP_SIG_SIZE)return FALSE;// 检查S2数据报和C1数据报的timestamp是否匹配bMatch = (memcmp(serversig, clientsig, RTMP_SIG_SIZE) == 0);if (!bMatch){RTMP_Log(RTMP_LOGWARNING, "%s, client signature does not match!", __FUNCTION__);}return TRUE;
}
代码基本就是按照标准来写的,唯一需要注意的是C0和C1数据报是同时发送的,C0和C1都存储在clientbuf当中
1.2.2 RTMP的NetConnection(SendConnectPacket)
该函数的主要作用是进行NetConnection,再回顾一下NetConnection的流程图
从流程图中看,client在进行握手成功之后,会向server发送一个 “connect” 的命令,申请进行RTMP连接,而SendConnectPacket实现的功能就是发送一个 “connect” 的命令。另外,也回顾一下connect命令当中可以携带的参数,这些参数在SendConnectPacket当中都有考虑到
RTMPDump会定义所需要使用的命令,例如connect命令会被定义成为 av_connect,如下所示
#define AVC(str) {str,sizeof(str)-1}
#define SAVC(x) static const AVal av_##x = AVC(#x)// connect 命令中使用的名值对对象的描述
SAVC(app);
SAVC(connect); // connect命令,{"connet", sizeof(connect) - 1}
SAVC(flashVer);
SAVC(swfUrl);
SAVC(pageUrl);
SAVC(tcUrl);
SAVC(fpad);
SAVC(capabilities);
SAVC(audioCodecs);
SAVC(videoCodecs);
SAVC(videoFunction);
SAVC(objectEncoding);
SAVC(secureToken);
SAVC(secureTokenResponse);
SAVC(type);
SAVC(nonprivate);
SendConnectPacket函数用于发送一个connect命令,会写入connect当中可能会写入的参数,随后使用RTMP_SendPacket()将信息发送出去
static int
SendConnectPacket(RTMP * r, RTMPPacket * cp)
{RTMPPacket packet;// pend是尾缀char pbuf[4096], * pend = pbuf + sizeof(pbuf);char* enc;if (cp)return RTMP_SendPacket(r, cp, TRUE);// 块流ID设置为3(似乎在标准文档中没有说是多少?)packet.m_nChannel = 0x03; /* control channel (invoke) *//*#define RTMP_PACKET_SIZE_LARGE 0 // #define RTMP_PACKET_SIZE_MEDIUM 1 // #define RTMP_PACKET_SIZE_SMALL 2 // #define RTMP_PACKET_SIZE_MINIMUM 3 //*/packet.m_headerType = RTMP_PACKET_SIZE_LARGE; // m_headerType对应于Basic Header中的fmt// RTMP_PACKET_TYPE_INVOKE = 0x14 = 20,表明这是一条命令消息,并且以AMF0的格式进行编码packet.m_packetType = RTMP_PACKET_TYPE_INVOKE; // m_packetType对应于Messge Header中的message type idpacket.m_nTimeStamp = 0;packet.m_nInfoField2 = 0;packet.m_hasAbsTimestamp = 0;packet.m_body = pbuf + RTMP_MAX_HEADER_SIZE; // body = 4096 - 18enc = packet.m_body;/*+-----------------+-----------+-------------+------------+------------| RTMP Max Header | Data Type | Data Length | Data Value | xxx| (18 Bytes) | (1 Bytes) | (x Bytes) | (L Bytes) | (.. Bytes) +-----------------+-----------+-------------+------------+------------pbuf enc (packet.m_body)*/// 下面会写入packet.m_body的信息,这些信息用于connect// av_connect = { "connect", sizeof("connect") - 1 };enc = AMF_EncodeString(enc, pend, &av_connect); // 将connect命令写入到enc中enc = AMF_EncodeNumber(enc, pend, ++r->m_numInvokes); // 将呼叫的次数写入到enc中*enc++ = AMF_OBJECT; // 写入data类型为object// 写入app信息enc = AMF_EncodeNamedString(enc, pend, &av_app, &r->Link.app);if (!enc)return FALSE;if (r->Link.protocol & RTMP_FEATURE_WRITE){enc = AMF_EncodeNamedString(enc, pend, &av_type, &av_nonprivate);if (!enc)return FALSE;}if (r->Link.flashVer.av_len){enc = AMF_EncodeNamedString(enc, pend, &av_flashVer, &r->Link.flashVer);if (!enc)return FALSE;}if (r->Link.swfUrl.av_len){enc = AMF_EncodeNamedString(enc, pend, &av_swfUrl, &r->Link.swfUrl);if (!enc)return FALSE;}if (r->Link.tcUrl.av_len){enc = AMF_EncodeNamedString(enc, pend, &av_tcUrl, &r->Link.tcUrl);if (!enc)return FALSE;}if (!(r->Link.protocol & RTMP_FEATURE_WRITE)){enc = AMF_EncodeNamedBoolean(enc, pend, &av_fpad, FALSE);if (!enc)return FALSE;enc = AMF_EncodeNamedNumber(enc, pend, &av_capabilities, 15.0);if (!enc)return FALSE;enc = AMF_EncodeNamedNumber(enc, pend, &av_audioCodecs, r->m_fAudioCodecs);if (!enc)return FALSE;enc = AMF_EncodeNamedNumber(enc, pend, &av_videoCodecs, r->m_fVideoCodecs);if (!enc)return FALSE;enc = AMF_EncodeNamedNumber(enc, pend, &av_videoFunction, 1.0);if (!enc)return FALSE;if (r->Link.pageUrl.av_len){enc = AMF_EncodeNamedString(enc, pend, &av_pageUrl, &r->Link.pageUrl);if (!enc)return FALSE;}}if (r->m_fEncoding != 0.0 || r->m_bSendEncoding){ /* AMF0, AMF3 not fully supported yet */enc = AMF_EncodeNamedNumber(enc, pend, &av_objectEncoding, r->m_fEncoding);if (!enc)return FALSE;}if (enc + 3 >= pend)return FALSE;*enc++ = 0;*enc++ = 0; /* end of object - 0x00 0x00 0x09 */*enc++ = AMF_OBJECT_END; // 写完object/* add auth string */// 写认证信息if (r->Link.auth.av_len){enc = AMF_EncodeBoolean(enc, pend, r->Link.lFlags & RTMP_LF_AUTH);if (!enc)return FALSE;enc = AMF_EncodeString(enc, pend, &r->Link.auth);if (!enc)return FALSE;}if (r->Link.extras.o_num){int i;for (i = 0; i < r->Link.extras.o_num; i++){enc = AMFProp_Encode(&r->Link.extras.o_props[i], enc, pend);if (!enc)return FALSE;}}packet.m_nBodySize = enc - packet.m_body;return RTMP_SendPacket(r, &packet, TRUE);
}
RTMP_SendPacket()的实现如下所示,基本就是按照标准文档来写入chunk信息并发送,关键位置有注释,大体的步骤为:
(1)确定头信息内容
(a)确定message header size
(b)确定basic header size
(c)确定extended timestamp
(2)写入头信息内容
(a)写入basic header
(b)写入message header中的timestamp(3字节)
(c)写入bodySize(即msg length)(3字节)
(d)写入packetType(即msg type id)(1字节)
(e)写入最后4字节(即message stram id)(4字节)
(f)写入扩展的时间戳(4字节)
(3)发送信息
关于代码的实现,这里有一个小点:
在代码中,是以chunk的格式来存储所有格式的,而不是以message的格式,但最后还有一个分包的操作。按理来说,以message格式存储才需要分包,这里相当于是把一个大的chunk分成了多个小的chunk来发送了
int
RTMP_SendPacket(RTMP * r, RTMPPacket * packet, int queue)
{const RTMPPacket* prevPacket;uint32_t last = 0;int nSize;int hSize, cSize;char* header, * hptr, * hend, hbuf[RTMP_MAX_HEADER_SIZE], c;uint32_t t;char* buffer, * tbuf = NULL, * toff = NULL;int nChunkSize;int tlen;// 检查channel数量if (packet->m_nChannel >= r->m_channelsAllocatedOut){int n = packet->m_nChannel + 10;RTMPPacket** packets = realloc(r->m_vecChannelsOut, sizeof(RTMPPacket*) * n);if (!packets) {free(r->m_vecChannelsOut);r->m_vecChannelsOut = NULL;r->m_channelsAllocatedOut = 0;return FALSE;}r->m_vecChannelsOut = packets;memset(r->m_vecChannelsOut + r->m_channelsAllocatedOut, 0, sizeof(RTMPPacket*) * (n - r->m_channelsAllocatedOut));r->m_channelsAllocatedOut = n;}prevPacket = r->m_vecChannelsOut[packet->m_nChannel];/*Chunk Format:+--------------+----------------+--------------------+-----------------| Basic Header | Message Header | Extended Timestamp | Chunk Data....+--------------+----------------+--------------------+-----------------|<----------------- Chunk Header ------------------->|(1) Basic Header(a) type 10 1 2 3 4 5 6 7+---------------+|fmt| cs id | +---------------+(b) type 20 10 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +---------------+-------------+|fmt| 0 | cs id - 64 |+---------------+-------------+(c) type 30 1 20 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 +---------------+------------------------------+|fmt| 1 | cs id - 64 |+---------------+------------------------------+(2) Message Header(a) type 0 (11 bytes)0 1 2 30 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1+----------------------------------------------+---------------+| timestamp | message length|+----------------------------------------------+---------------+| message length (cont) | message type id| msg stream id |+----------------------------------------------+---------------+| message stream id(cont) |+----------------------------------------------+(b) type 1 (7 bytes)0 1 2 30 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1+----------------------------------------------+---------------+| timestamp delta | message length|+----------------------------------------------+---------------+| message length (cont) | message type id|+----------------------------------------------+(c) type 2 (3 bytes)0 1 2 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 +----------------------------------------------+| timestamp delta |+----------------------------------------------+(d) type 3 (no message header)*/// 1. 确定头信息内容// m_headerType对应于BasicHeader中的fmt字段,表示后续msg的格式// m_packetType对应于Message Header中的message type id,表示消息的类型if (prevPacket && packet->m_headerType != RTMP_PACKET_SIZE_LARGE){// 前面packet的数据是否和当前packet数据有相同之处,如果有,则去掉当前packet的冗余信息/* compress a bit by using the prev packet's attributes */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;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;}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;}// static const int packetSize[] = { 12, 8, 4, 1 };/*问:标准当中的msg header size为11, 7, 3, 0,为什么在这里加1?答:RTMP数据报中包括Basic Header、Message Header和Chunk Data,其中Basic Header由fmt和chunk stream id组成,fmt占据2比特,chunk stream id指示当前chunk在当前stream当中位于第几个位置,如果是 (2, 63],则为1字节,这样packetSize就加1*/// nSize : message header size// (1.a) 确定message header sizenSize = packetSize[packet->m_headerType];hSize = nSize; cSize = 0;t = packet->m_nTimeStamp - last;if (packet->m_body){header = packet->m_body - nSize; // header起始地址hend = packet->m_body; // header结束地址}else{header = hbuf + 6; // header size = 6hend = hbuf + sizeof(hbuf);}/*stream_id的范围给出了chunk basic header的大小+------------+--------------+| range | Bytes |+------------+--------------+| (2, 63] | 1 byte |+------------+--------------+| (63, 319] | 2 Bytes |+------------+--------------+|(319, 65599]| 3 Bytes |+------------+--------------+*/// chunk stream id的检查// cSize描述Basic header的大小// (1.b) 确定basic header sizeif (packet->m_nChannel > 319)cSize = 2; // chunk basic header为3个字节else if (packet->m_nChannel > 63)cSize = 1; // chunk basic header为2个字节if (cSize){header -= cSize;hSize += cSize;}// (1.c) 确定extended timestampif (t >= 0xffffff) // 时间戳过大,需要增加额外的extended timestamp{header -= 4;hSize += 4;RTMP_Log(RTMP_LOGWARNING, "Larger timestamp than 24-bit: 0x%x", t);}// 2. 写入头信息hptr = header;c = packet->m_headerType << 6; // 高2位设置为fmtswitch (cSize){// 低6位设置成为stream idcase 0:c |= packet->m_nChannel;break;case 1:break;case 2:c |= 1;break;}// (2.a) 写入basic header*hptr++ = c;// 如果cSize不为0,说明chunk basic header为2字节或者3字节,需要写入0值或者1值if (cSize){int tmp = packet->m_nChannel - 64;*hptr++ = tmp & 0xff;if (cSize == 2)* hptr++ = tmp >> 8;}// (2.b) 写入message header中的timestamp,3字节if (nSize > 1){// 用于编码24位整数值,将其从主机字节序转换为AMF格式所使用的网络字节序hptr = AMF_EncodeInt24(hptr, hend, t > 0xffffff ? 0xffffff : t);}if (nSize > 4){// (2.c) 写入bodySize(msg length),3字节hptr = AMF_EncodeInt24(hptr, hend, packet->m_nBodySize);// (2.d) 写入packetType(msg type id),1字节*hptr++ = packet->m_packetType;}// (2.e) 写入最后4字节 (message stram id)if (nSize > 8) // 将一个整数以小端序(little-endian)的方式进行编码为32位的整数hptr += EncodeInt32LE(hptr, packet->m_nInfoField2);// (2.f) 写入扩展的时间戳,4字节if (t >= 0xffffff) // 用于编码32位整数值hptr = AMF_EncodeInt32(hptr, hend, t);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 */if (r->Link.protocol & RTMP_FEATURE_HTTP){int chunks = (nSize + nChunkSize - 1) / nChunkSize;if (chunks > 1){tlen = chunks * (cSize + 1) + nSize + hSize;tbuf = malloc(tlen);if (!tbuf)return FALSE;toff = tbuf;}}// 3. 发送消息// 前面已经将所需要的信息写入到了packet中,现在需要将packet分成多个chunk发送出去while (nSize + hSize) // nSize = bodySize, hSize = headerSize;{int wrote;if (nSize < nChunkSize) // 当前剩余的size,不需要分成多个chunknChunkSize = nSize;RTMP_LogHexString(RTMP_LOGDEBUG2, (uint8_t*)header, hSize);RTMP_LogHexString(RTMP_LOGDEBUG2, (uint8_t*)buffer, nChunkSize);if (tbuf){memcpy(toff, header, nChunkSize + hSize);toff += nChunkSize + hSize;}else{// 发出信息,大小为nChunkSize + hSize // nChunkSize默认为128字节wrote = WriteN(r, header, nChunkSize + hSize);if (!wrote)return FALSE;}// 更新sizenSize -= nChunkSize;buffer += nChunkSize;hSize = 0; // 第一次就会把header中的信息全部发完if (nSize > 0) // 还有消息没有发送完,需要将剩余的信息打包成为chunk,用于后续的发送{header = buffer - 1;hSize = 1;// 重新处理头部信息if (cSize){header -= cSize;hSize += cSize;}if (t >= 0xffffff){header -= 4;hSize += 4;}// 取出c中的前2位,即fmt信息*header = (0xc0 | c); // 0xc0 : 1100 0000if (cSize){int tmp = packet->m_nChannel - 64;header[1] = tmp & 0xff;if (cSize == 2)header[2] = tmp >> 8;}if (t >= 0xffffff){char* extendedTimestamp = header + 1 + cSize;AMF_EncodeInt32(extendedTimestamp, extendedTimestamp + 4, t);}}}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);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;txn = (int)AMF_DecodeNumber(ptr);AV_queue(&r->m_methodCalls, &r->m_numCalls, &method, txn);}}// 存储前面发送的packetif (!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;
}
2.小结
记录了RTMPDump中如何以client的视角与对端server建立连接的过程,分为几个步骤:
(1)建立socket连接
(2)建立RTMP连接
(a)握手
(b)RTMP的网络连接(会发送connect命令)
基于此,RTMPDump的client就与server进行了正式的连接,后续可以进行互相传输信息了
相关文章:
![](https://i-blog.csdnimg.cn/direct/2ba18cfeacee46bd8459f25ed03539fc.png)
【流媒体】RTMPDump—RTMP_Connect函数(握手、网络连接)
目录 1. RTMP_Connect函数1.1 网络层连接(RTMP_Connect0)1.2 RTMP连接(RTMP_Connect1)1.2.1 握手(HandShake)1.2.2 RTMP的NetConnection(SendConnectPacket) 2.小结 RTMP协议相关&am…...
![](https://www.ngui.cc/images/no-images.jpg)
通过https方式访问内网IP
单位要做个用浏览器扫二维码的功能。我先在本地测试一直不成功,后来放到服务器上运行成功了。比较了一下,服务器上是https,但是本地没有证书。我问了一下信安的同事,要求二维码必须在本地扫描,不能上公网。所以只好在本…...
![](https://www.ngui.cc/images/no-images.jpg)
flutter 键盘弹出 都会重新Build
原因是调用MediaQuery.of(context)后,点击TextField组件时会导致调用build方法。 解决方法:在Scaffold组件的body嵌套Builder组件,然后设置一个BuildContext变量,将Builder组件中的context传递给BuildContext变量,然后…...
![](https://www.ngui.cc/images/no-images.jpg)
RedisDistributedLock 分布式锁
设计一个简单的 RedisDistributedLock 类,实现单例模式,并包含基本的锁定机制。这个类将使用 Redis 来管理锁,确保在分布式系统中资源的同步访问 import redis.clients.jedis.Jedis;public class RedisDistributedLock {private static Redi…...
![](https://i-blog.csdnimg.cn/direct/549cb3b0c7fe4c85a3d06a004dc4a8eb.png)
Java之包装类
Java中的包装类(Wrapper Classes)是基本数据类型的对象包装类。Java为每个基本数据类型(如int、char等)提供了对应的包装类,使得基本类型可以被当作对象来处理。这些包装类位于java.lang包中。 包装类的用途 对象化&a…...
![](https://i-blog.csdnimg.cn/direct/9cf11cf3c4f84335ae7db200a78dda89.png)
Linux - 权限
文章目录 一、用户二、文件 一、用户 1、Linux下有两种用户:超级用户(root)、普通用户。 超级用户:可以再linux系统下做任何事情,不受限制 。 普通用户:在linux下做有限的事情。 超级用户的命令提示符是“…...
![](https://i-blog.csdnimg.cn/direct/0fe5db714ccf411fa740872d9619a2c7.png)
免费图形化nginx管理工具nginxWebUI
nginxWebUI是一款图形化管理nginx配置得工具, 可以使用网页来快速配置nginx的各项功能, 包括http协议转发, tcp协议转发, 反向代理, 负载均衡, 静态html服务器, ssl证书自动申请、续签、配置等, 配置好后可一建生成nginx.conf文件, 同时可控制nginx使用此文件进行启动与重载, 完…...
![](https://www.ngui.cc/images/no-images.jpg)
编程上的挫折不可怕,可怕的是你畏惧了
如何克服编程学习中的挫折感 编程学习之路上,挫折感就像一道道难以逾越的高墙,让许多人望而却步。然而,真正的编程高手都曾在这条路上跌倒过、迷茫过,却最终找到了突破的方法。那么,我是如何在Bug的迷宫中找到出口的&…...
![](https://www.ngui.cc/images/no-images.jpg)
docker逃逸手法
docker逃逸手法 基本docker操作docker 命令dockerfilesDocker Compose漏洞利用容器漏洞 基本docker操作 docker 命令 # docker拉取 docker pull # 指定版本拉取 docker pull ubuntu:22.04# 显示镜像可执行的操作 docker image # 列出存储在本地系统上的所有图像 docker image…...
![](https://i-blog.csdnimg.cn/direct/d3f10a2ad4e8428ca5b0051f327d7a2e.png)
3 pytest Fixture
3 pytest Fixture 3.1 通过 conftest.py 共享 fixture3.2 使用 fixture 执行配置及销毁逻辑3.3 使用 --setup-show 回溯 fixture 的执行过程3.4 使用 fixture 传递测试数据3.5 使用多个 fixture3.6 指定 fixture 作用范围3.7 使用 usefixtures 指定 fixture3.8 为常用 fixture …...
![](https://i-blog.csdnimg.cn/direct/1970990be04948acbf674f7de0737f90.png)
pinctl 和 gpio子系统驱动
一.设备树中添加pinctl节点模板 1.创建对应的节点 同一个外设的 PIN 都放到一个节点里面,打开 imx6ull-14x14-evk.dts,在 iomuxc 节点 中的“imx6ul-evk”子节点下添加 “pinctrl_test” 节点。添加完成以后如下所示: pinctrl_test:test_g…...
![](https://www.ngui.cc/images/no-images.jpg)
RocketMQ消息堆积了怎么解决?
RocketMQ 的消息堆积,一般都是因为客户端本地消费过程中,由于消费耗时过长或消费并发度较小等原因,导致客户端消费能力不足,出现消息堆积的问题。 当线上出现消息堆积的问题时,一般有以下几种方式来解决: 增加消费者…...
![](https://i-blog.csdnimg.cn/direct/63bc18f1e0904332b1b75781db124f59.png)
C++第十二弹 -- STL之list模拟实现
文章索引 前言模拟实现list1. ListNode节点类2. list的迭代器封装3. 反向迭代器4. list类的模拟实现测试代码 list的反向迭代器总结 前言 通过模拟实现可以让我们更加深刻的理解C底层STL的实现逻辑, 本篇就对list的底层进行模拟实现. 博客主页: 酷酷学!!! 点击关注 共同进步!…...
![](https://i-blog.csdnimg.cn/direct/f0fd297d7a5b47df9c4018af54c17d3b.jpeg#pic_center)
Destiny of Gods首轮测试正式开启,参与玩家数量突破10万
天神风云,波澜再兴,GameFi链游聚合平台Destiny of Gods首款同名数字卡牌回合制游戏首轮测试定档8月20日20:00(GMT8),现已正式开启! 这是一个由人、游灵和神灵共存的世界,历经蛮荒时期的纷争与信…...
![](https://i-blog.csdnimg.cn/direct/17d141900878495cb65e6cadc2f6ab49.png)
QT聊天室基于Tcp
server.cpp #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget),server(new QTcpServer(this)) // 给服务器指针对象实例化空间{ui->setupUi(this); }Widget::~Widget() {delete ui; }…...
![](https://i-blog.csdnimg.cn/direct/adb54c8327894abf88378dde364aae14.png)
公开课观后感:密歇根大学python for everyone
从2024年1月17日到2024年8月20日,终于将密歇根大学的python for everyone的python公开课跟完。站在一月份规划的时刻来看,比我想象中花费的时间更多,我当时肯定没有想到要花上整整七个月的时间才能将这个公开课的内容看完,毕竟这个…...
![](https://www.ngui.cc/images/no-images.jpg)
goweb框架-gin
文章目录 Gin框架概览Gin框架的特点Gin框架的安装和基本使用安装基本使用 路由系统路由的基本概念Gin框架路由的特点 Radix Tree(基数树)基数树的定义和原理基数树在Gin框架中的应用节省空间的优化动态路由和通配符处理 路由树的构建注册路由的过程路由树…...
![](https://img-blog.csdnimg.cn/694b35de52e6493c99f913729355584f.png)
2024年接口测试高频面试题及答案
1. 什么是接口测试? •接口测试就是通过测试不同情况下的入参与之相应的出参信息来判断接口是否符合或满足相应的功能性、安全性要求 •测试的重点是要检查数据的交换,传递和控制管理过程,以及系统间的相互逻辑依赖关系 2. 为什么要做接口…...
![](https://i-blog.csdnimg.cn/direct/0d1f2b7a9c8846329c600d14d2d1900b.png)
ESP32-C3在MQTT访问时出现“transport_base: Poll timeout or error”问题的分析(8)
接前一篇文章:ESP32-C3在MQTT访问时出现“transport_base: Poll timeout or error”问题的分析(7) 前边几回分析了笔者在MQTT测试时所遇到的问题: 最终定位到了是由于components\components\tcp_transport\transport_ssl.c的base_poll_write函数中调用的select函数超时返回…...
![](https://www.ngui.cc/images/no-images.jpg)
Linux: 忘记密码的解决方法,passwd
https://www.redhat.com/sysadmin/recover-root-passwd 这里的方法很简单,就是通过console进去,添加一个启动参数,加载sysroot,然后用passwd命令修改密码。这个是RHEL7适用。 https://access.redhat.com/solutions/1192 这个是提…...
![](https://i-blog.csdnimg.cn/direct/13c8bb3d6e4b42e186e606827a2bcce8.png)
36. 有效的数独【 力扣(LeetCode) 】
一、题目描述 请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。 数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现一次。 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图…...
![](https://www.ngui.cc/images/no-images.jpg)
机器学习中的没有免费午餐定理
嘿,各位机器学习的爱好者们!今天,让我们一起深入探讨机器学习中那个神秘而又重要的概念——没有免费午餐定理。 一、定理引入:探索算法森林的钥匙 在广阔无垠的机器学习领域中,免费午餐定理就如同一把神奇的钥匙&…...
![](https://www.ngui.cc/images/no-images.jpg)
高级java每日一道面试题-2024年8月21日-框架篇[Spring篇]-使用IOC容器应该注意哪些?
如果有遗漏,评论区告诉我进行补充 面试官: 使用IOC容器应该注意哪些? 我回答: 1. 理解IOC的基本概念 控制反转:在传统的编程模式中,程序会主动控制依赖关系的创建和管理。而在IoC容器中,这种控制权被反转给了容器本身。程序员只需要声明…...
![](https://www.ngui.cc/images/no-images.jpg)
LLM训练推理相关概念
1. 有监督微调(Supervised Fine-Tuning)与指令微调(Instruction Fine-Tuning)对模型参数的影响 **有监督微调(Supervised Fine-Tuning, SFT)和指令微调(Instruction Fine-Tuning, Instruct-Tun…...
![](https://i-blog.csdnimg.cn/direct/a8ee8d8b7da04ec4895db84065eb891b.png)
IP in IP 协议
IP in IP 是一种多重IP协议,即:客户机可以发送一个IP协议内部在嵌套一个IP协议到某个特定的主机上,在由具体的主机作为路由进行转发的协议。 例如: IP in IP帧协议结构为,第一层为发送到IP in IP 路由主机的报文&…...
![](https://www.ngui.cc/images/no-images.jpg)
DAY2: HTTP请求报文和响应报文是怎样的,有哪些常见的字段?| HTTP有哪些请求方式?| GET请求和POST请求的区别
目录 HTTP请求报文和响应报文是怎样的,有哪些常见的字段? 请求报文 响应报文 HTTP有哪些请求方式? GET请求和POST请求的区别 HTTP请求报文和响应报文是怎样的,有哪些常见的字段? HTTP报文分为请求报文和响应报文…...
线性代数:每日一题1/特征值与相似对角化
设A, B 为二阶矩阵,且 AB BA , 则“A有两个不相等的特征值”是“B可对角化"的() A. 充分必要条件 B. 充分不必要条件 C.必要不充分条件 D.既不充分也不必要条件 知识点: 特征向量与特征值的关系 相似矩阵的定义和性质 n阶…...
![](https://www.ngui.cc/images/no-images.jpg)
Android UI:PopupWindow:API
文章目录 类操作 对PopupWindow的操作 创建PopupWindow对象的操作添加并显示PopupWindow的操作移除PopupWindow的操作更新PopupWindow的操作显示内容的相关操作 布局的相关操作进入退出动画的相关操作 Transition设置进入动画的相关操作Transition设置退出动画的相关操作XML设置…...
![](https://img-blog.csdnimg.cn/img_convert/e12d3a9ac1616a047e58e7c789b3ab1e.png)
什么是DevUI?
DevUI是面向企业中后台产品的开源前端解决方案,其设计价值观基于"高效、开放、可信、乐趣"四种自然与人文相结合的理念,旨在为设计师、前端开发者提供标准的设计体系,并满足各类落地场景,是一款企业级开箱即用的产品。 …...
![](https://i-blog.csdnimg.cn/direct/d6193b6881e94ab8b17c3643461de36e.png)
DAY53
作业: 运行1个服务器和2个客户端 实现效果: 服务器和2个客户端互相聊天,服务器和客户端都需要使用select模型去实现 服务器要监视2个客户端是否连接,2个客户端是否发来消息以及服务器自己的标准输入流 客户端要监视服务器是否发来…...
![](http://control.cublog.cn/fileicon/rar.gif)
wordpress怎么设置友情链接/网络营销推广方案设计
第一步:下载jdk和tomcat:JDK下载 Tomcat下载 最新的jdk为1.6.10,tomcat为6.0,建议jdk1.4以上,tomcat4.0以上 第二步:安装和配置你的jdk和tomcat:执行jdk和tomcat的安装程序,然后设置…...
![](https://img-blog.csdnimg.cn/f94e657ecdd8479981cc2ffc9cd1f82c.png)
如何建设网站制作平台/社交媒体营销策略有哪些
我们写东西的时候总会遇到lua中要调用java代码,当然这个用JNI肯定是可以做到的,但是有更加方便的办法—LuaJavaBridge。 一、luaj 主要特征 * 可以从 Lua 调用 Java Class Static Method * 调用 Java 方法时,支持 int/float/boolean/String…...
![](/images/no-images.jpg)
网站css初始化/平台如何做推广
Lighting Box2源码分析(一):整体功能Lighting Box2源码分析(二):Sun ShaftsLighting Box2源码分析(三):Depth Of Field景深Lighting Box2源码分析(四):Global Fog全局雾...
![](/images/no-images.jpg)
wordpress ux主题/免费个人网站建站
对于mesa的交叉编译。该文章的目标是编译一套aarch64 Linux Debian嵌入式版本上可以运行的版本库,接下来就开始趟坑。老套路,先把linux桌面版搞好,然后 移植到嵌入式Linux Debian 板子上。 1 mesa简介 Mesa 3D是一个在MIT许可证下开放源代码…...
![](/images/no-images.jpg)
wordpress免登录评论/如何网站关键词优化
身处大数据时代,各企业对云计算相关业务越来越依赖。各种服务器类型的市场需求也愈之加大,更多的虚拟机主机服务商转阵成服务器,当然不乏新踏入行业的新人。怎么才能做好业务,根据以往虚拟机的经验来说,选择一套管理系…...
![](https://img-my.csdn.net/uploads/201208/13/1344826904_1719.png)
免费网站建设公司推荐/优化营商环境
author YHC 这个教程向你展示如何附加节点到tree,我们将创建一个食品tree包含水果和蔬菜节点,然后添加一些其他水果到已存在的水果节点. 创建 foods(食品) tree 首先,我们创建食品树,代码就像这样. <div style"width:200px;height:auto;border:1px solid #ccc;"&…...