TeamTalk客户端源码分析七

末蓝、 2022-03-20 16:00 803阅读 0赞

TeamTalk客户端源码分析七

  • 一,CBaseSocket类
  • 二,select模型
  • 三,样例分析:登录功能

    上篇文章我们分析了network模块中的引用计数,智能锁,异步回调机制以及数据的序列化和反序列化操作,本文主要介绍该模块中socket通信的实现。

一,CBaseSocket类

  1. network中实现了一个socket基类,它封装了socket编程常见的几个函数listenbindconnect等,这些细节就不再介绍,我们主要看一下这几个函数的使用:

在这里插入图片描述
_SetNonblock在CBaseSocket::Listen和CBaseSocket::Connect中都有调用,它的作用是设置当前socket连接为非阻塞模式(在windows下通过ioctlsocket()函数设置FIONBIO命令值为1来实现),可以保证客户端和服务端的通信在发包结束后就直接返回,不陷入阻塞等待模式,代码如下:
在这里插入图片描述
_SetReuseAddr用在CBaseSocket::Listen中,它的作用是设置当前socket为可复用状态,在closesocket之后,再次调用bind函数,仍旧可以使用之前的端口,否则由于TIME_WAIT的关系,会导致bind失败(详见socket服务器开发中的SO_REUSEADDR选项与让人心烦的TIME_WAIT),代码如下:
在这里插入图片描述
_SetNoDelay用在CBaseSocket::Connect中,它的作用是禁止Nagle算法(如果某个给定连接上有待确认数据,那么原本应该作为用户写操作之响应的在该连接上立即发送相应小分组的行为就不会发生,直到收到上一个包的ACK,这里的小分组指的是小于MSS的任何分组,在IPV4中,MSS一般为1460字节。对于某些应用来说,这种算法将降低系统性能),由于Teamtalk是一个网络数据量传输比较小的应用,要求用户的输入能够及时获得返回,有较低的延时。如果开启了Nagle算法,就很可能出现频繁的延时,导致用户体验极差,所以这里通过TCP_NODELAY来关闭Nagle算法。代码如下:
在这里插入图片描述

二,select模型

  1. CBaseSocket中还有一个关键的地方,TeamTalk客户端调用listen与服务端建立连接后,并没有使用accept或者receive来接收数据,而是把当前的socket放入应的fd\_set集合中,用于select筛选再进行处理,比如CBaseSocket::Listen中:

在这里插入图片描述
我们再来看看CEventDispatch::Instance()->AddEvent的实现:
在这里插入图片描述
在CEventDispatch中定义了三个fd_set:m_read_set(处理写操作),m_write_set(处理读操作),m_excep_set(处理异常)。通过AddEvent接口,将当前的socket根据事件类型不同填充进不同的fd_set中,然后再通过select接口来判断套接字上是否存在数据,或者能否向一个套接字写入数据,以及该套接字是否异常,select调用如下:
在这里插入图片描述
StartDispatch是一个死循环,不停地调用select来判断fd_set集合中套接字的状态,StartDispatch是在一个单独的线程中来调用的,该线程的启动是在主程序的入口处实现的,这个自己跟一下就清楚了。
如果select返回值大于0,那么遍历三个fd_set集合中的每一个socket,调用相应的功能函数:
在这里插入图片描述
以read_set举例,对于这个集合中的每一个socket,调用OnRead函数,实现如下:
在这里插入图片描述 这里还调用了ioctlsocket函数来确定套接字上的可读数目,这样做的原因是因为:在TeamTalk中使用的套接字都是非阻塞的,在调用select之前,connect的连接可能已经建立,并有来自对端的数据到达。这种情况下,即使套接字上不发生错误,套接字也是可读又可写,这和连接建立失败情况下的套接字的读写情况一样,因此需要处理这种情形,同样地,在OnWrite中也有类似的处理。(更详细的套接字就绪条件请参见这篇博客套接字描述的就绪条件
最终调用的还是一个函数指针,而这个指针又是在CBaseSocket::Listen(const char* server_ip, uint16_t port, callback_t callback, void* callback_data)中第三个参数传递进来的,自此,整个通信流程就清楚了。
在network中和有两个文件netlib.h和imconn.h,这两个文件中只是对CBaseSocket功能的二次封装,这里就不再多介绍了,可以自己看一下源码。
下面我们以登录功能举例,将整个过程串起来再分析一遍。

三,样例分析:登录功能

  1. 登录的入口在TcpClientModule\_Impl.cpp中:

在这里插入图片描述
首先通过imcore::IMLibCoreConnect进行socket的初始化和bind工作,内部实现也就是调用的CBaseSocket的接口,然后imcore::IMLibCoreRegisterCallback将当前类设置为回调类,TcpClientModule_Impl类继承了ITcpSocketCallback,实现下面这几个接口,用于处理网络数据的接收和状态处理。
在这里插入图片描述
onConnectDone回调回来,连接成功,触发事件:
在这里插入图片描述
在TcpClientModule_Impl::doLogin中,继续往下走,开始进行数据的填充,对于登录而言,就是用户名和密码,这里的IM::Login::IMLoginReq结构就是上一章中提到过的Protobuf,然后调用sendPacket发送数据,并等待事件m_eventReceived的触发,接收登录结果。
在这里插入图片描述
在_sendPacket(sendPacket最终调用的_sendPacket)中,对要传输的数据进行序列化和压缩操作,保存在std::unique_ptr data中,再调用network模块中的imcore::IMLibCoreWrite发送数据,IMLibCoreWrite内部是调用的CImConn::Send,我们看一下它的实现:
在这里插入图片描述
真正实现发包操作的是netlib_send接口,内部还是调用的CBaseSocket::Send,不过在这里我们要关注的是另一个点:while循环。当传输数据超过发送窗口大小时,一次发送不完,需要进行多次发送,虽然这个功能在登录模块是用不上的,但是作者考虑的非常细致,个人觉得这个地方处理的非常好,而且这里还用到了上文中介绍的CSimpleBuffer,来控制数据的剩余量。
当select模型检测到有数据时,会调用imconn_callback(在CImConn::Connect中传入的),imconn_callback中最终还是调用的CImConn接口,这里以接收数据为例,是CImConn::OnRead(),代码如下:
在这里插入图片描述
这里依旧也是用了一个while,来判断接收的数据是否一次性能够接受完。
对于接收到的数据首先通过imcore::TTPBHeader::unSerialize接口进行反序列化操作,然后再调用m_pTcpSocketCB->onReceiveData,而m_pTcpSocketCB就是我们前面通过imcore::IMLibCoreRegisterCallback(m_socketHandle, this)传进去的,最终还是进入到TcpClientModule_Impl::onReceiveData(const char* data, int32_t size),代码如下:
在这里插入图片描述
首先判断当前包类型是否是心跳包,如果是直接过滤掉,不派发到业务层处理。然后判断序列号是否和登录发包的序列号一致,如果一致,那就是登录的回包,m_pImLoginResp->ParseFromArray进行解包操作,得到登录结果,直接返回(注意这里它单独的把登录回包处理剔除出来了,没有走下面的_handlePacketOperation流程,因为按照_handlePacketOperation走下去的登录回包,只处理CID_LOGIN_KICK_USER消息)。如果是其他业务的回包,比如用户列表等信息,调用_handlePacketOperation丢到各自的业务逻辑模块去处理:
在这里插入图片描述
在_handlePacketOperation中,通过ModuleId获取到不同的module::IPduPacketParse
在这里插入图片描述
比如SID_LOGIN,通过 module::getLoginModule()获取到的是LoginModule_Impl*。在TcpClientModule_Impl::_handlePacketOperation中调用的是module::IPduPacketParse的虚函数onPacket,所以我们再看LoginModule_Impl::onPacket
在这里插入图片描述
onPacket只处理了CID_LOGIN_KICK_USER命令,调用的_kickUserResponse,首先通过protobuf进行数据解析(这里是IM::Login::IMKickUser),取出kick_reason(踢用户的原因)字段,通过module::getLoginModule()->asynNotifyObserver将结果传递到上层进行UI界面展示(这一个回调机制在本系列文章的第一篇有介绍过,详见TeamTalk客户端源码分析一之回调机制)

分析到这里,network模块以及登录流程就全部介绍完了,更多的细节还是需要大家去看代码调试跟踪。

发表评论

表情:
评论列表 (有 0 条评论,803人围观)

还没有评论,来说两句吧...

相关阅读