热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

LibRTMP源代码分析5:建立网络连接

服务器和客户端之间只能建立一个网络连接,但是基于该连接可以创建很多网络流。他们的关系如图所示: 网络连接的基本步骤在第一篇文章中有所介绍,此处不再重复。源代码中的RTMP_Conn
服务器和客户端之间只能建立一个网络连接,但是基于该连接可以创建很多网络流。他们的关系如图所示:

LibRTMP源代码分析5:建立网络连接(NetConnection) - nkwavelet - 小波的世界

  网络连接的基本步骤在第一篇文章中有所介绍,此处不再重复。源代码中的 RTMP_Connect(...) 用于建立RTMP网络连接。

/**
 * @brief 建立RTMP中的网络连接(NetConnection).
 *  a) 创建并设置目标socket,包括ip地址和端口号
 *  b) 建立socket连接,设置socket的超时和接收、发送缓冲区的大小
 *  c) 握手操作
 *  d) 发送含有connect命令的数据报,用于建立RTMP连接
 */
int RTMP_Connect(RTMP *r, RTMPPacket *cp, struct sockaddr *dst)
{
// socket结构体
struct sockaddr_in service;
if (!r->Link.hostname.av_len)
return FALSE;

// 设置socket地址
memset(&service, 0, sizeof(struct sockaddr_in));
service.sin_family = AF_INET;

if (r->Link.socksport)
{
/* Connect via SOCKS,使用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;
       }

if (r->m_nstub == 1)
{
// 设置socket的ip地址和端口号
rtmp_sockaddr_set_ip((struct sockaddr*)&service, inet_addr(r->m_stubip));
service.sin_port = htons(r->m_stubport);
}

// 第0次连接,主要用于建立socket连接,并未开始真正的建立RTMP连接
// 设置socket的超时和socket接收、发送缓冲区的大小
if (!RTMP_Connect0(r, (struct sockaddr *)&service))
return FALSE;

if (dst) 
memcpy(dst, &service, sizeof(struct sockaddr));

r->m_bSendCounter = TRUE;

// 第1次连接,真正建立RTMP连接,主要包括握手和发送connect命令
return RTMP_Connect1(r, cp);
}

 RTMP_Connect()函数主要调用了上述红色标记出来的四个函数来完成RTMP网络连接,接下来我们分别来分析这四个函数。

/**
 * @brief 将主机的ip地址和端口号添加到socket地址结构中
 *
 * @param service : socket的IPv4地址结构
 * @param host    : 结构体存有主机名及其长度
 * @param port     : 待添加的端口号
 *
 * @return 成功返回TRUE,否则返回FALSE.
 */
static int add_addr_info(struct sockaddr_in *service, AVal *host, int port)
{
char *hostname;
int  ret = TRUE;

// 获取主机名
if (host->av_val[host->av_len])
        {
// 主机名之后的值非零,为主机名重新申请空间,最后一位补上'\0'
hostname = malloc(host->av_len + 1);
memcpy(hostname, host->av_val, host->av_len);
hostname[host->av_len] = '\0';
        }
else
{
// 主机名之后的值为0,直接将主机名地址赋值即可
hostname = host->av_val;
}

// 将主机名转化为32位网络字节序IP地址
service->sin_addr.s_addr = inet_addr(hostname);

// 如果得到的地址值是非法的,则另谋其道
if (service->sin_addr.s_addr == INADDR_NONE)
        {
// 根据主机名,获取与主机有关的信息,包括主机别名、地址类型以及该主机的所有IP地址
struct hostent *host = gethostbyname(hostname);
if (host == NULL || host->h_addr == NULL)
{
RTMP_Log(RTMP_LOGERROR, "Problem accessing the DNS. (addr: %s)", hostname);
ret = FALSE;
goto finish;
}

// 设置IP地址
service->sin_addr = *(struct in_addr *)host->h_addr;
        }

// 设置端口号
service->sin_port = htons(port);

finish:
// hostname重新申请了空间,则需要释放
if (hostname != host->av_val)
free(hostname);

return ret;
}

/**
 * @brief 设置scoket的ip地址
 *
 * @param a  : 待设置的scoket指针
 * @param ip : 32位的网络字节序IP地址
 */
void rtmp_sockaddr_set_ip(struct sockaddr *a, unsigned long ip)
{
union { struct sockaddr_in ain; struct sockaddr addr; } ts;
ts.addr = a[0];
ts.ain.sin_addr.s_addr = ip;
a[0] = ts.addr;
}

/**
 * @brief 建立socket连接. r创建一个socket连接到service指定的网络地址。
 *  设置socket接收数据的超时时限以及socket接收和发送缓冲区的大小。
 */
int RTMP_Connect0(RTMP *r, struct sockaddr *service)
{
int recv_size = 128*1024;
int send_size = 128*1024;
int len = sizeof(int);
int on  = 1;

r->m_sb.sb_timedout = FALSE;
r->m_pausing = 0;
r->m_fDuration = 0.0;

// 创建一个流式socket
r->m_sb.sb_socket = r->m_sock.socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (r->m_sb.sb_socket != -1)
{
// 将刚刚创建的socket连接至service指定的网络地址
if (r->m_sock.connect(r->m_sb.sb_socket, service, sizeof(struct sockaddr)) <0)
{
int err = r->m_sock.getsockerr();
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 // 创建socket失败
{
RTMP_Log(RTMP_LOGERROR, "%s, failed to create socket. Error: %d", __FUNCTION__, r->m_sock.getsockerr());
return FALSE;
        }

/* set timeout,设置超时 */
if (r->m_sock.setsockopt) 
{
// 获取设定的超时时限,单位毫秒
SET_RCVTIMEO(tv, r->Link.timeout);

// 设置socket接收数据的超时时限,单位毫秒
if (r->m_sock.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);
}

// 设置socket接收和发送缓冲区的大小
r->m_sock.setsockopt(r->m_sb.sb_socket, SOL_SOCKET, SO_RCVBUF, (char*)&recv_size, len);
r->m_sock.setsockopt(r->m_sb.sb_socket, SOL_SOCKET, SO_SNDBUF, (char*)&send_size, len);
}

return TRUE;
}

/**
 * @brief 建立RTMP连接,从握手开始.
 */
int RTMP_Connect1(RTMP *r, RTMPPacket *cp)
{
if (r->Link.protocol & RTMP_FEATURE_SSL)
{
RTMP_Log(RTMP_LOGERROR, "%s, no SSL/TLS support", __FUNCTION__);
RTMP_Close(r);
return FALSE;
}

// 使用Http
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__);
if (!HandShake(r, TRUE)) // 开始握手
        {
RTMP_Log(RTMP_LOGERROR, "%s, handshake failed.", __FUNCTION__);
RTMP_Close(r);
return FALSE;
       }
RTMP_Log(RTMP_LOGDEBUG, "%s, handshaked", __FUNCTION__);

// 发送含有connect命令的数据报,用于建立RTMP连接
if (!SendConnectPacket(r, cp))
        {
RTMP_Log(RTMP_LOGERROR, "%s, RTMP connect failed.", __FUNCTION__);
RTMP_Close(r);
return FALSE;
}

return TRUE;
}

第一次连接RTMP_Connect1( )函数主要做了两件事情: 
 1) HandShake()完成握手,之前已经分析过了;
 2) SendConnectPacket()发送connect命令,用于建立RTMP连接。接下来就具体分析一下这个函数。

/**
 * @brief 发送connect命令. 这是每次程序运行的时候发送的第一个命令消息.
 *  命令消息由命令名,传输ID,和命令对象组成.
 *  命令对象由一系列的相关参数组成.
 *  可参考rtmp协议:rtmp命令消息--4.1.1节
 */
static int SendConnectPacket(RTMP *r, RTMPPacket *cp)
{
RTMPPacket packet;
char pbuf[4096], *pend = pbuf + sizeof(pbuf);
char *enc;

if (cp)
return RTMP_SendPacket(r, cp, TRUE);

packet.m_nChannel = 0x03; /* control channel (invoke), 块流ID */
packet.m_headerType = RTMP_PACKET_SIZE_LARGE; // 块消息头类型,长度为11字节
packet.m_packetType = RTMP_PACKET_TYPE_INVOKE; // 消息类型ID为20,表示该消息用AMF0编码,
// 参考rtmp协议:rtmp命令消息 --- 3.1节
packet.m_nTimeStamp = 0; // 时间戳
packet.m_nInfoField2 = 0; // 消息流id
packet.m_hasAbsTimestamp = 0; // 相对时间
packet.m_body = pbuf + RTMP_MAX_HEADER_SIZE;

enc = packet.m_body;
enc = AMF_EncodeString(enc, pend, &av_connect); // 将"connnect"字符串采用AMF0编码
enc = AMF_EncodeNumber(enc, pend, ++r->m_numInvokes); // 编码命令消息的数目,也就是传输ID
*enc++ = AMF_OBJECT; // 接下来是AMF对象,内含多个属性编码

// 编码客户端要连接到的服务应用名
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)
{
// 编码flash播放器版本
enc = AMF_EncodeNamedString(enc, pend, &av_flashVer, &r->Link.flashVer);
if (!enc)
return FALSE;
}

if (r->Link.swfUrl.av_len)
{
// 编码发起连接的swf文件的url
enc = AMF_EncodeNamedString(enc, pend, &av_swfUrl, &r->Link.swfUrl);
if (!enc)
return FALSE;
}

if (r->Link.tcUrl.av_len)
{
// 编码服务url,有下列的格式  protocol://servername:port/appName/appInstance
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)
{
// 编码SWF文件被加载的页面的Url
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, AMF编码方法 */
enc = AMF_EncodeNamedNumber(enc, pend, &av_objectEncoding, r->m_fEncoding);
if (!enc)
return FALSE;
        }

if (enc + 3 >= pend)
return FALSE;
// AMF对象结束标志
*enc++ = 0;
*enc++ = 0; /* end of object - 0x00 0x00 0x09 */
*enc++ = AMF_OBJECT_END;

/* 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 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);
}

       发送各种命令消息的过程比较相似,基本上都是先建立一个 RTMPPacket 格式的包packet,然后填充该packet的各个字段。各字段的取值可以参考不同命令消息的文档,其中最复杂的一个过程就是生成packet.m_body数据。一旦packet的封装完成,最后都是调用RTMP_SendPacket()函数来发送这个packet。RTMP_SendPacket()函数代码比较长,会在后续单独的一篇文章中详细分析其代码。
最后用一个图总结函数RTMP_Connect()的调用关系:
LibRTMP源代码分析5:建立网络连接(NetConnection) - nkwavelet - 小波的世界

推荐阅读
  • Nginx使用AWStats日志分析的步骤及注意事项
    本文介绍了在Centos7操作系统上使用Nginx和AWStats进行日志分析的步骤和注意事项。通过AWStats可以统计网站的访问量、IP地址、操作系统、浏览器等信息,并提供精确到每月、每日、每小时的数据。在部署AWStats之前需要确认服务器上已经安装了Perl环境,并进行DNS解析。 ... [详细]
  • 基于PgpoolII的PostgreSQL集群安装与配置教程
    本文介绍了基于PgpoolII的PostgreSQL集群的安装与配置教程。Pgpool-II是一个位于PostgreSQL服务器和PostgreSQL数据库客户端之间的中间件,提供了连接池、复制、负载均衡、缓存、看门狗、限制链接等功能,可以用于搭建高可用的PostgreSQL集群。文章详细介绍了通过yum安装Pgpool-II的步骤,并提供了相关的官方参考地址。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 本文介绍了在rhel5.5操作系统下搭建网关+LAMP+postfix+dhcp的步骤和配置方法。通过配置dhcp自动分配ip、实现外网访问公司网站、内网收发邮件、内网上网以及SNAT转换等功能。详细介绍了安装dhcp和配置相关文件的步骤,并提供了相关的命令和配置示例。 ... [详细]
  • 阿里Treebased Deep Match(TDM) 学习笔记及技术发展回顾
    本文介绍了阿里Treebased Deep Match(TDM)的学习笔记,同时回顾了工业界技术发展的几代演进。从基于统计的启发式规则方法到基于内积模型的向量检索方法,再到引入复杂深度学习模型的下一代匹配技术。文章详细解释了基于统计的启发式规则方法和基于内积模型的向量检索方法的原理和应用,并介绍了TDM的背景和优势。最后,文章提到了向量距离和基于向量聚类的索引结构对于加速匹配效率的作用。本文对于理解TDM的学习过程和了解匹配技术的发展具有重要意义。 ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • IhaveconfiguredanactionforaremotenotificationwhenitarrivestomyiOsapp.Iwanttwodiff ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
  • 本文讨论了在Windows 8上安装gvim中插件时出现的错误加载问题。作者将EasyMotion插件放在了正确的位置,但加载时却出现了错误。作者提供了下载链接和之前放置插件的位置,并列出了出现的错误信息。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
author-avatar
mobiledu2502908043
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有