【流媒体】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进行了正式的连接,后续可以进行互相传输信息了
相关文章:
【流媒体】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方式访问内网IP
单位要做个用浏览器扫二维码的功能。我先在本地测试一直不成功,后来放到服务器上运行成功了。比较了一下,服务器上是https,但是本地没有证书。我问了一下信安的同事,要求二维码必须在本地扫描,不能上公网。所以只好在本…...
flutter 键盘弹出 都会重新Build
原因是调用MediaQuery.of(context)后,点击TextField组件时会导致调用build方法。 解决方法:在Scaffold组件的body嵌套Builder组件,然后设置一个BuildContext变量,将Builder组件中的context传递给BuildContext变量,然后…...
RedisDistributedLock 分布式锁
设计一个简单的 RedisDistributedLock 类,实现单例模式,并包含基本的锁定机制。这个类将使用 Redis 来管理锁,确保在分布式系统中资源的同步访问 import redis.clients.jedis.Jedis;public class RedisDistributedLock {private static Redi…...
Java之包装类
Java中的包装类(Wrapper Classes)是基本数据类型的对象包装类。Java为每个基本数据类型(如int、char等)提供了对应的包装类,使得基本类型可以被当作对象来处理。这些包装类位于java.lang包中。 包装类的用途 对象化&a…...
Linux - 权限
文章目录 一、用户二、文件 一、用户 1、Linux下有两种用户:超级用户(root)、普通用户。 超级用户:可以再linux系统下做任何事情,不受限制 。 普通用户:在linux下做有限的事情。 超级用户的命令提示符是“…...
免费图形化nginx管理工具nginxWebUI
nginxWebUI是一款图形化管理nginx配置得工具, 可以使用网页来快速配置nginx的各项功能, 包括http协议转发, tcp协议转发, 反向代理, 负载均衡, 静态html服务器, ssl证书自动申请、续签、配置等, 配置好后可一建生成nginx.conf文件, 同时可控制nginx使用此文件进行启动与重载, 完…...
编程上的挫折不可怕,可怕的是你畏惧了
如何克服编程学习中的挫折感 编程学习之路上,挫折感就像一道道难以逾越的高墙,让许多人望而却步。然而,真正的编程高手都曾在这条路上跌倒过、迷茫过,却最终找到了突破的方法。那么,我是如何在Bug的迷宫中找到出口的&…...
docker逃逸手法
docker逃逸手法 基本docker操作docker 命令dockerfilesDocker Compose漏洞利用容器漏洞 基本docker操作 docker 命令 # docker拉取 docker pull # 指定版本拉取 docker pull ubuntu:22.04# 显示镜像可执行的操作 docker image # 列出存储在本地系统上的所有图像 docker image…...
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 …...
pinctl 和 gpio子系统驱动
一.设备树中添加pinctl节点模板 1.创建对应的节点 同一个外设的 PIN 都放到一个节点里面,打开 imx6ull-14x14-evk.dts,在 iomuxc 节点 中的“imx6ul-evk”子节点下添加 “pinctrl_test” 节点。添加完成以后如下所示: pinctrl_test:test_g…...
RocketMQ消息堆积了怎么解决?
RocketMQ 的消息堆积,一般都是因为客户端本地消费过程中,由于消费耗时过长或消费并发度较小等原因,导致客户端消费能力不足,出现消息堆积的问题。 当线上出现消息堆积的问题时,一般有以下几种方式来解决: 增加消费者…...
C++第十二弹 -- STL之list模拟实现
文章索引 前言模拟实现list1. ListNode节点类2. list的迭代器封装3. 反向迭代器4. list类的模拟实现测试代码 list的反向迭代器总结 前言 通过模拟实现可以让我们更加深刻的理解C底层STL的实现逻辑, 本篇就对list的底层进行模拟实现. 博客主页: 酷酷学!!! 点击关注 共同进步!…...
Destiny of Gods首轮测试正式开启,参与玩家数量突破10万
天神风云,波澜再兴,GameFi链游聚合平台Destiny of Gods首款同名数字卡牌回合制游戏首轮测试定档8月20日20:00(GMT8),现已正式开启! 这是一个由人、游灵和神灵共存的世界,历经蛮荒时期的纷争与信…...
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; }…...
公开课观后感:密歇根大学python for everyone
从2024年1月17日到2024年8月20日,终于将密歇根大学的python for everyone的python公开课跟完。站在一月份规划的时刻来看,比我想象中花费的时间更多,我当时肯定没有想到要花上整整七个月的时间才能将这个公开课的内容看完,毕竟这个…...
goweb框架-gin
文章目录 Gin框架概览Gin框架的特点Gin框架的安装和基本使用安装基本使用 路由系统路由的基本概念Gin框架路由的特点 Radix Tree(基数树)基数树的定义和原理基数树在Gin框架中的应用节省空间的优化动态路由和通配符处理 路由树的构建注册路由的过程路由树…...
2024年接口测试高频面试题及答案
1. 什么是接口测试? •接口测试就是通过测试不同情况下的入参与之相应的出参信息来判断接口是否符合或满足相应的功能性、安全性要求 •测试的重点是要检查数据的交换,传递和控制管理过程,以及系统间的相互逻辑依赖关系 2. 为什么要做接口…...
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函数超时返回…...
Linux: 忘记密码的解决方法,passwd
https://www.redhat.com/sysadmin/recover-root-passwd 这里的方法很简单,就是通过console进去,添加一个启动参数,加载sysroot,然后用passwd命令修改密码。这个是RHEL7适用。 https://access.redhat.com/solutions/1192 这个是提…...
网络六边形受到攻击
大家读完觉得有帮助记得关注和点赞!!! 抽象 现代智能交通系统 (ITS) 的一个关键要求是能够以安全、可靠和匿名的方式从互联车辆和移动设备收集地理参考数据。Nexagon 协议建立在 IETF 定位器/ID 分离协议 (…...
IGP(Interior Gateway Protocol,内部网关协议)
IGP(Interior Gateway Protocol,内部网关协议) 是一种用于在一个自治系统(AS)内部传递路由信息的路由协议,主要用于在一个组织或机构的内部网络中决定数据包的最佳路径。与用于自治系统之间通信的 EGP&…...
【大模型RAG】Docker 一键部署 Milvus 完整攻略
本文概要 Milvus 2.5 Stand-alone 版可通过 Docker 在几分钟内完成安装;只需暴露 19530(gRPC)与 9091(HTTP/WebUI)两个端口,即可让本地电脑通过 PyMilvus 或浏览器访问远程 Linux 服务器上的 Milvus。下面…...
AI编程--插件对比分析:CodeRider、GitHub Copilot及其他
AI编程插件对比分析:CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展,AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者,分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...
mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包
文章目录 现象:mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包遇到 rpm 命令找不到已经安装的 MySQL 包时,可能是因为以下几个原因:1.MySQL 不是通过 RPM 包安装的2.RPM 数据库损坏3.使用了不同的包名或路径4.使用其他包…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
安宝特案例丨Vuzix AR智能眼镜集成专业软件,助力卢森堡医院药房转型,赢得辉瑞创新奖
在Vuzix M400 AR智能眼镜的助力下,卢森堡罗伯特舒曼医院(the Robert Schuman Hospitals, HRS)凭借在无菌制剂生产流程中引入增强现实技术(AR)创新项目,荣获了2024年6月7日由卢森堡医院药剂师协会࿰…...
无人机侦测与反制技术的进展与应用
国家电网无人机侦测与反制技术的进展与应用 引言 随着无人机(无人驾驶飞行器,UAV)技术的快速发展,其在商业、娱乐和军事领域的广泛应用带来了新的安全挑战。特别是对于关键基础设施如电力系统,无人机的“黑飞”&…...
省略号和可变参数模板
本文主要介绍如何展开可变参数的参数包 1.C语言的va_list展开可变参数 #include <iostream> #include <cstdarg>void printNumbers(int count, ...) {// 声明va_list类型的变量va_list args;// 使用va_start将可变参数写入变量argsva_start(args, count);for (in…...
Golang——9、反射和文件操作
反射和文件操作 1、反射1.1、reflect.TypeOf()获取任意值的类型对象1.2、reflect.ValueOf()1.3、结构体反射 2、文件操作2.1、os.Open()打开文件2.2、方式一:使用Read()读取文件2.3、方式二:bufio读取文件2.4、方式三:os.ReadFile读取2.5、写…...
