teamtalk网络库类作用说明
win客户端和linux服务端共用一套网络库,但是客户端为了与服务端共用一套网络库,做了一些额外的封装,本章会对两者共同之处的网络库进行源码分析,后面会对客户端和服务端的细微不同之处进行单独分析,因为这个系列是阅读源码过程中为理清思路所做的笔记,仅作为阅读源码时的参考,不足之处请及时指正
一、下面是通用网络库的一些类的用法说明,具体功能请阅读源码
-
CRefObject :: 引用类
-
CBaseSocket :: 套接字操作类,保存套接字状态和操作的方法、回调函数(读写关闭回调),listen和connect时都设置回调,
等事件分发器检测有事件的时候,调用相关的回调函数 -
CCondition :: 条件变量封装类,具有超时等待,用于线程之间的同步操作
-
CConfigFileReader :: 配置文件读写类,封装配置文件的读写操作
-
CAes :: 加密解密方法类,封装openssl加密解密方法
-
CEventDispatch :: 事件分发器类,封装处理事件循环,处理io操作,和检测定时任务,处理事件回调(调用CBaseSocket中的响应函数)
-
http_parser :: http基础操作结构体
-
CHttpParserWrapper :: 封装http有关方法的类
-
CHttpClient :: http客户端类,封装post, get 方法
-
CImConn :: 封装对套接字连接的操作,有自己的接收发送缓冲区,可以设置回调
-
CImPdu :: 消息操作类,封装有关protobuf消息结构的操作方法
-
CLock :: 互斥锁类 CRWLock :: 读写锁类, CAutoLock :: 自动锁类
-
netlib :: 封装有关socket操作的函数,提供外部调用的接口
-
public_define.h :: 公共定义
-
ServInfo :: 服务器通用操作函数
-
Singleton :: 模板类,作为单例使用
-
CTask :: 任务基类
-
CTread :: 线程基类
-
CThreadNotify :: 条件变量封装类,无超时等待
-
CWorkerThread :: 工作线程,有任务就执行任务
-
CThreadPool :: 线程池,拥有多个工作线程,分配任务给工作线程
-
CStrExplode :: 字符串分割类
-
CPduException :: 消息异常类
-
CSimpleBuffer :: 缓冲区类,封装了对自定义缓冲区的操作方法
-
CByteStream :: 字节流类,封装了对simpleBuffer的操作方法
二、teamtalk I/O框架重要类说明
teamtalk基于Reactor模式,跟一般框架一样,有句柄,事件多路分发器,事件处理器和具体的事件处理器,Reactor,teamtalk的客户端和服务端网络库是共用的,中间有些差别,为不混淆,两边单独分开分析,如只看客户端或服务端的,不影响阅读,挑其中一个看,另一个忽略即可,后续章节也是,客户端和服务端可以分开来看,此篇因为有大量共同的东西,放在一起
2.1 CBaseSocket类
CBaseSocket中拥有句柄,还有事件处理器(注册的回调函数m_callback)
2.2 CEventDispatch事件分发器和Reactor,该类中调用select或epoll循环等待事件发生并处理,添加和删除事件
2.3 事件处理器和具体事件处理器
事件处理器为CImConn,通过注册imconn_callback到CBaseSocket中,进行回调事件处理器,而具体事件处理器,客户端和服务端有些不太一样,服务端:事件处理器为继承CImConn的类,重写相关业务逻辑函数。客户端:CImConn中增加ITcpSocketCallback*成员变量,实际是指向TcpClientModule_Impl的指针,主要是为了与客户端做一个组件化操作
2.4 网络库数据收发的关系
从高层到底层是这样,只举发送数据调用层级ImCore::send()->CImConn::send()->netlib_send()->CSocket::send()->(::send)中,netlib和ImCore都封装了一些提供访问和操作网络库的接口,客户端是通过ImCore来进行调用,服务端则是在继承ImCore后直接使用netlib的接口进行数据的发送接收
三、网络库数据包流程
3.1 公共事件分发器
void CEventDispatch::StartDispatch(uint32_t wait_timeout)
{
fd_set read_set, write_set, excep_set;
timeval timeout;
timeout.tv_sec = 1; //wait_timeout 1 second
timeout.tv_usec = 0;
while (running)
{
//_CheckTimer();
//_CheckLoop();
if (!m_read_set.fd_count && !m_write_set.fd_count && !m_excep_set.fd_count)
{
Sleep(MIN_TIMER_DURATION);
continue;
}
m_lock.lock();
FD_ZERO(&read_set);
FD_ZERO(&write_set);
FD_ZERO(&excep_set);
memcpy(&read_set, &m_read_set, sizeof(fd_set));
memcpy(&write_set, &m_write_set, sizeof(fd_set));
memcpy(&excep_set, &m_excep_set, sizeof(fd_set));
m_lock.unlock();
if (!running)
break;
//for (int i = 0; i < read_set.fd_count; i++) {
// LOG__(NET, "read fd: %d\n", read_set.fd_array[i]);
//}
int nfds = select(0, &read_set, &write_set, &excep_set, &timeout);
if (nfds == SOCKET_ERROR)
{
//LOG__(NET, "select failed, error code: %d\n", GetLastError());
Sleep(MIN_TIMER_DURATION);
continue; // select again
}
if (nfds == 0)
{
continue;
}
for (u_int i = 0; i < read_set.fd_count; i++)
{
//LOG__(NET, "select return read count=%d\n", read_set.fd_count);
SOCKET fd = read_set.fd_array[i];
CBaseSocket* pSocket = FindBaseSocket((net_handle_t)fd);
if (pSocket)
{
pSocket->OnRead();
pSocket->ReleaseRef();
}
}
for (u_int i = 0; i < write_set.fd_count; i++)
{
//LOG__(NET, "select return write count=%d\n", write_set.fd_count);
SOCKET fd = write_set.fd_array[i];
CBaseSocket* pSocket = FindBaseSocket((net_handle_t)fd);
if (pSocket)
{
pSocket->OnWrite();
pSocket->ReleaseRef();
}
}
for (u_int i = 0; i < excep_set.fd_count; i++)
{
LOG__(NET, _T("select return exception count=%d"), excep_set.fd_count);
SOCKET fd = excep_set.fd_array[i];
CBaseSocket* pSocket = FindBaseSocket((net_handle_t)fd);
if (pSocket)
{
pSocket->OnClose();
pSocket->ReleaseRef();
}
}
}
}
3.2 上述分发器代码是window下的,linux下的类似,在这个函数中select等待事件的发生,有事件则调用对应句柄所对应的事件处理器,如pSocket->OnRead()
void CBaseSocket::OnRead()
{
if (m_state == SOCKET_STATE_LISTENING)
{
//这个服务端才有,如果socket句柄是lisent时使用的句柄,检测到客户端有新连接过来会调用
_AcceptNewSocket();
}
else
{
u_long avail = 0;
if ( (ioctlsocket(m_socket, FIONREAD, &avail) == SOCKET_ERROR) || (avail == 0) )
{
//m_callback保存imconn.h中的imconn_callback地址
m_callback(m_callback_data, NETLIB_MSG_CLOSE, (net_handle_t)m_socket, NULL);
}
else
{
m_callback(m_callback_data, NETLIB_MSG_READ, (net_handle_t)m_socket, NULL);
}
}
}
3.3 _AcceptNewSocket();在客户端有新链接过来时会调用,并增加新的会话链接CImConn,并将回调函数imconn_callback保存CBaseSocket的m_callback中,如果该链接是非监听状态,则调用之前注册的回调imconn_callback
void imconn_callback(void* callback_data, uint8_t msg, uint32_t handle, void* pParam)
{
NOTUSED_ARG(handle);
NOTUSED_ARG(pParam);
CImConn* pConn = TcpSocketsManager::getInstance()->get_client_conn(handle);
if (!pConn)
{
//LOG__(NET, _T("connection is invalied:%d"), handle);
return;
}
pConn->AddRef();
// LOG__(NET, "msg=%d, handle=%d\n", msg, handle);
switch (msg)
{
case NETLIB_MSG_CONFIRM:
pConn->onConnect();
break;
case NETLIB_MSG_READ:
pConn->OnRead();
break;
case NETLIB_MSG_WRITE:
pConn->OnWrite();
break;
case NETLIB_MSG_CLOSE:
pConn->OnClose();
break;
default:
LOG__(NET, _T("!!!imconn_callback error msg: %d"), msg);
break;
}
pConn->ReleaseRef();
}
3.5 上述代码为windows下的,linux下服务端代码类似,,这里举有数据到来举例,有数据的过来的时候调用这个回调,通过选择相应的分支 pConn->OnRead();
void CImConn::OnRead()
{
for (;;)
{
uint32_t free_buf_len = m_in_buf.GetAllocSize() - m_in_buf.GetWriteOffset();
if (free_buf_len < READ_BUF_SIZE)
m_in_buf.Extend(READ_BUF_SIZE);
int ret = netlib_recv(m_handle, m_in_buf.GetBuffer() + m_in_buf.GetWriteOffset(), READ_BUF_SIZE);
if (ret <= 0)
break;
m_in_buf.IncWriteOffset(ret);
while (m_in_buf.GetWriteOffset() >= imcore::HEADER_LENGTH)
{
uint32_t len = m_in_buf.GetWriteOffset();
uint32_t length = CByteStream::ReadUint32(m_in_buf.GetBuffer());
if (length > len)
break;
try
{
imcore::TTPBHeader pbHeader;
pbHeader.unSerialize((byte*)m_in_buf.GetBuffer(), imcore::HEADER_LENGTH);
LOG__(NET, _T("OnRead moduleId:0x%x,commandId:0x%x"), pbHeader.getModuleId(), pbHeader.getCommandId());
if (m_pTcpSocketCB)
//在这个进行相应的业务逻辑处理
m_pTcpSocketCB->onReceiveData((const char*)m_in_buf.GetBuffer(), length);
LOGBIN_F__(SOCK, "OnRead", m_in_buf.GetBuffer(), length);
}
catch (std::exception& ex)
{
assert(FALSE);
LOGA__(NET, "std::exception,info:%s", ex.what());
if (m_pTcpSocketCB)
m_pTcpSocketCB->onReceiveError();
}
catch (...)
{
assert(FALSE);
LOG__(NET, _T("unknown exception"));
if (m_pTcpSocketCB)
m_pTcpSocketCB->onReceiveError();
}
m_in_buf.Read(NULL, length);
}
}
}
3.6 客户端和服务端的CImConn::OnRead()流程类似,先解包,然后再进行调用业务逻辑处理函数,只不过客户端是m_pTcpSocketCB->onReceiveData,服务端是重写CImConn::HandlePdu()
3.6.1 客户端:
void TcpClientModule_Impl::onReceiveData(const char* data, int32_t size)
{
if (m_pServerPingTimer)
m_pServerPingTimer->m_bHasReceivedPing = TRUE;
imcore::TTPBHeader header;
header.unSerialize((byte*)data, imcore::HEADER_LENGTH);
if (IM::BaseDefine::CID_OTHER_HEARTBEAT == header.getCommandId() && IM::BaseDefine::SID_OTHER == header.getModuleId())
{
//模块器端过来的心跳包,不跳到业务层派发
return;
}
LOG__(NET, _T("receiveData message moduleId:0x%x,commandId:0x%x")
, header.getModuleId(), header.getCommandId());
if (g_seqNum == header.getSeqNumber())
{
m_pImLoginResp->ParseFromArray(data + imcore::HEADER_LENGTH, size - imcore::HEADER_LENGTH);
::SetEvent(m_eventReceived);
return;
}
//将网络包包装成任务放到逻辑任务队列里面去
_handlePacketOperation(data, size);
}
m_pTcpSocketCB->onReceiveData客户端一般数据是TcpClientModule_Impl::onReceiveData这收取,文件传输数据使用的是文件传输模块的onReceiveData,暂且不提,在这个函数中,获取数据包中的serviceID和cmdID,根据不同serviceID选择相应模块的实例
void TcpClientModule_Impl::_handlePacketOperation(const char* data, UInt32 size)
{
std::string copyInBuffer(data, size);
imcore::IMLibCoreStartOperationWithLambda(
[=]()
{
imcore::TTPBHeader header;
header.unSerialize((byte*)copyInBuffer.data(),imcore::HEADER_LENGTH);
module::IPduPacketParse* pModule
= (module::IPduPacketParse*)__getModule(header.getModuleId());
if (!pModule)
{
assert(FALSE);
LOG__(ERR, _T("module is null, moduleId:%d,commandId:%d")
, header.getModuleId(), header.getCommandId());
return;
}
std::string pbBody(copyInBuffer.data() + imcore::HEADER_LENGTH, size - imcore::HEADER_LENGTH);
pModule->onPacket(header, pbBody);
});
}
不同模块都重写了onPacket函数,根据cmdID的不同来选择相应的业务处理函数,如
void GroupListModule_Impl::onPacket(imcore::TTPBHeader& header, std::string& pbBody)
{
switch (header.getCommandId())
{
case IM::BaseDefine::CID_GROUP_NORMAL_LIST_RESPONSE :
_groupNormalListResponse(pbBody);
break;
case IM::BaseDefine::CID_GROUP_INFO_RESPONSE:
_groupInfoResponse(pbBody);
break;
case IM::BaseDefine::CID_GROUP_CREATE_RESPONSE:
_groupCreateDiscussGroupResponse(pbBody);
break;
case IM::BaseDefine::CID_GROUP_CHANGE_MEMBER_RESPONSE:
_groupChangedGroupMembersResponse(pbBody);
break;
case IM::BaseDefine::CID_GROUP_SHIELD_GROUP_RESPONSE:
_groupShieldResponse(pbBody);
break;
case IM::BaseDefine::CID_GROUP_CHANGE_MEMBER_NOTIFY:
_groupChangeMemberNotify(pbBody);
break;
default:
break;
}
}
3.6.2 服务端
回到上面服务端重写CImConn::HandlePdu()流程,例如
void CLoginConn::HandlePdu(CImPdu* pPdu)
{
switch (pPdu->GetCommandId()) {
case CID_OTHER_HEARTBEAT:
break;
case CID_OTHER_MSG_SERV_INFO:
_HandleMsgServInfo(pPdu);
break;
case CID_OTHER_USER_CNT_UPDATE:
_HandleUserCntUpdate(pPdu);
break;
case CID_LOGIN_REQ_MSGSERVER:
_HandleMsgServRequest(pPdu);
break;
default:
log("wrong msg, cmd id=%d ", pPdu->GetCommandId());
break;
}
}
四、收包发包流程总结
4.1 客户端收发包流程
4.1.1 发包
- TcpClientModule_Impl::sendPacket()->_sendPacket()组包头和包体,包头有对应的serviceID和cmid还有包的总长度,包体是真正的业务结构数据,序列化后发送
- 调用IMLibCoreWrite()将组好的包给CImConn发送
- CImConn::Send()发送不出去就在自己的缓存中保存,监听写事件,可写时将数据发送出去,就用netlib_send()
- netlib_send()找出对应socket的CBaseSocket,调用CBaseSocket::Send()发送数据
- CBaseSocket::Send()则调用相应系统底层的send()函数,进行数据发送
4.1.2 收包
- 事件分发器监听事件的发生,有读事件发生,根据socket句柄,调用拥有该句柄的CBaseSocket的OnRead()函数
- CBaseSocket::OnRead()有数据调用回调函数imconn_callback()(函数中的_AcceptNewSocket上面已做说明,不再解释)
- imconn_callback()中判断是读是写数据,调用CImConn::OnRead()
- CImConn::OnRead()中调用onReceiveData()进行整个包的数据的读取,并调用TcpClientModule_Impl::_handlePacketOperation()
5._handlePacketOperation()中封装一个操作函数到lambda操作类中放入逻辑处理队列(逻辑队列的概念后续章节再讲,目前先清楚这个lambda表达式会被调用即可)
6.lambda表达式中根据header.getModuleId调用相应的模块,并将包体从包中解出,将包头和包体都传给相应模块的业务函数onPacket()
7.onPacket在各个模块中根据命令号,来进行相应的业务处理
4.2 服务端收发包流程
4.2.1 发包
举消息服务器回应心跳的例子,都其他服务器都差不多
- CMsgConn::_HandleHeartBeat(CImPdu *pPdu)中直接将客户端发过来的心跳数据发回去,调用父类CImConn::SendPdu()
- CImConn::SendPdu()调用CimConn::Send(),后面发送的流程就跟客户端一样
- CImConn::Send()发送不出去就在自己的缓存中保存,监听写事件,可写时将数据发送出去,就用netlib_send()
- netlib_send()找出对应socket的CBaseSocket,调用CBaseSocket::Send()发送数据
- CBaseSocket::Send()则调用相应系统底层的send()函数,进行数据发送
4.2.2 收包
服务端收包前三步跟客户端一样,直接复制
- 事件分发器监听事件的发生,有读事件发生,根据socket句柄,调用拥有该句柄的CBaseSocket的OnRead()函数
- CBaseSocket::OnRead()有数据调用回调函数imconn_callback()(函数中的_AcceptNewSocket上面已做说明,不再解释)
- imconn_callback()中判断是读是写数据,调用CImConn::OnRead()
- CImConn::OnRead()根据调用子类中重写的HandlePdu()函数,然后根据各自业务逻辑选择相应函数
本章主要针对客户端和服务端共同的网络库,进行分析,还有些缓冲区等也想放在这个章节分析,但两端采用的是不同的事件触发模式,客户端采用select水平触发模式(LT),服务端采用epoll的边沿触发模式(ET),所以就放到后面分别单独讲述
三、客户端登录流程
- LoginDialog::OnClick()点击登录按钮后调用LoginDialog::_DoLogin(),生产连接登录服务器请求msg ip信息的操作任务DoLoginServerHttpOperation() ,该任务设置任务完成的回调OnHttpCallbackOperation()后放入http任务队列中
- http任务线程从http任务队列中取出该http请求任务任务,执行该任务的processOpertion()进行http请求,并解析出返回的json结果,并给代理窗口发送事件消息,通知代理窗口进行执行之前注册的回调函数OnHttpCallbackOperation
- 代理窗口线程调用OnHttpCallbackOperation(),并生产LoginOperation操作任务,设置任务完成回调OnOperationCalllback()
- 逻辑任务队列取出该任务,执行该任务的processOpertion(),进行连接消息服务器 module::getTcpClientModule()->doLogin()
- 在doLogin中连接消息服务器,等待5s超时建立连接,然后进行登录请求,在此等待10s,有回应则返回
- 将回应的数据解析出来,并创建自己的信息
- 通知代理窗口线程调用OnPoerationCallback(),进行登录成功和登录失败的一系列处理
四、服务端登录流程
- 登录服务端netlib_listen(cal, cal_data)设置回调函数m_calback
- 初始化该socket并设置状态为SOCKET_STATE_LISTENING
- 当有客户端连接时,CBaseSocket::OnRead()中检测到该socket状态为监听状态,则调用_AcceptNewSocket(),否则调用m_callback()
- 对于处于监听状态的socket,将 接受的新连接置于已连接状态,并加入到全局map中管理,并将新的socket加入事件分发器中监听读事件,且调用之前设置的回调函数m_callback()
- m_callback()调用CHttpConn::OnConnect(handle),新建CHttpConn连接实例,加入到全局conn_map中管理,并设置会话类回调httpConn_callback()
- 对于已连接状态的socket,调用建立连接时设置的回调函数httpConn_callback()
- 调用会话类中的读写反应函数
- CHttpConn::OnRead()是请求msg服务器ip,则调用_HandleMsgServRequest,将msg服务器ip信息,封装成json格式数据发送出去,数据不能发送出去先放缓冲区,发送完成调用OnWriteComlete(),进行关闭http连接
9.客户端获取到msg信息后,发送登录包给msg - msg处理链接流程与上同,在处理消息时,先解析,再与数据库做比对,再回应客户端