最近把teamtalk的conn_map改成了智能指针,但改了总要多方面试试有没有问题,总不能编译通过,能正常启动就万事大吉了。所以就写了一个shell client客户端来进行功能的测试。
tt的官方上一次发布版本里有一个test目录,里面写了一个简易的测试客户端。不过这个test根本不可用,因为不是代码写的有错误,就是功能缺失,所以我只好亲自动手重做了。
在做的过程中总是发现client发起的connect偶尔会有连接不上,这个偶尔的概率非常低,但既然发生了,那肯定是有问题的。于是就查啊查啊。。。
test的测试客户端相当于是一个命令行shell的客户端,也就是没有图形界面,你的功能通过在终端上输入命令来完成。目前我仅实现了注册和登陆。未来打算把聊天等各种功能也做了,这样就差不多相当于实现了一个命令行式的客户端。有人也许会问,TT有windows,mac,ios,android全平台客户端,做个命令行的客户端有什么用?当然有用了,测试功能方便啊,你不用考虑折腾界面就能把各种功能给测了。未来添加功能也方便写测试,比如我现在新增一个注册功能,在这个命令行上面输入reg xx oo,那么一个用户名叫xx的用户便以密码为oo注册进了数据库。对命令的解析可比做界面的事件响应函数方便多了。
好了,现在问题来了,shell命令的输入是需要一个死循环来反复等待用户输入的,而tt的异步网络框架又需要另一个死循环,如果两个死循环放在同一线程里显然不行,所以就把接受用户输入的死循环放到了另一个线程里面。那么当用户输入reg xx oo时,将这条命令解析出用户名和密码,并开始启动注册流程,这一切都是在另一个线程里做的。
注册流程是怎么样的呢?这里先讲一讲TT的conn框架。
TT的底层异步网络库是将socket和epoll封装成一个netlib库,你要做的任何有关异步网络的操作都是通过调用netlib来实现的。但netlib只是一个原始的对tcp报文发送接收的异步库,你要做即时通讯,还需要在此基础上实现一套通讯协议,并且封装一组接口来完成对这些协议的操作。
于是TT就定义了一个叫
CImConn的类,这个类定义在imconn.h里面。
为了方便大家阅读,这里摘入部分代码
class CImConn : public CRefObject { public: CImConn(); virtual ~CImConn(); int Send(void* data, int len); virtual void OnRead(); virtual void OnWrite(); bool IsBusy() { return m_busy; } int SendPdu(CImPdu* pPdu) { return Send(pPdu->GetBuffer(), pPdu->GetLength()); } virtual void OnConnect(net_handle_t handle) { m_handle = handle; } virtual void OnConfirm(){} virtual void OnClose(){} virtual void OnTimer(uint64_t){} virtual void OnWriteCompelete(){} virtual void HandlePdu(CImPdu*){}
这里为了方便你理解,做个类比,如果你做过android开发,想一下每次你写一个应用的最常用的流程是什么样的?定义一个类继承Activity,然后override里面的onCreate等xx方法,是不是很相似?当然如果你没有安卓开发经验,类比一下ios吧,ios也是这样的,如果ios也没做过,那也没关系,继续往下看。
这里CImConn其实就是留给你继承的,当你继承后,请实现里面对应的成员函数。
如果你做的是服务端,那么需要实现OnConnect来响应用户的接入,如果是客户端,就需要OnConfirm来定义连接上服务器后的操作。其他几个接口服务端和客户端是通用的。
所以,看完这里你就会理解msg_server目录下为什么有DBServConn,FileServConn, LoginServConn, RouteServConn, PushServConn以及MsgConn。
前面几个都是消息服务器主动向其他几个服务器发起的客户端连接,最后一个是消息服务器自己的服务端Conn,用来等待用户接入,所以需要实现OnConnect函数。
而login_server里面的HttpConn和LoginConn含义也显而易见了,一个是用来响应http请求的,另一个是响应消息服务器login信息登记请求的。其他几个服务器里的conn也以此类推。
之前对TT感到很凌乱的朋友是不是突然感觉自己顿悟了?感谢我吧。
另外一个疑问,TT的imconn框架是如何把这个CImConn和netlib连接起来的?
这里以DBServConn为例做一个解释,看代码
void CDBServConn::Connect(const char* server_ip, uint16_t server_port, uint32_t serv_idx) { log("Connecting to DB Storage Server %s:%d ", server_ip, server_port); m_serv_idx = serv_idx; m_handle = netlib_connect(server_ip, server_port, imconn_callback, (void*)&g_db_server_conn_map); if (m_handle != NETLIB_INVALID_HANDLE) { g_db_server_conn_map.insert(make_pair(m_handle, this)); } }
这两个参数就是连接imconn和netlib的关键。g_db_server_conn_map是定义在CDBServConn里的一个static全局map映射表,用来保存什么呢?下面一句
g_db_server_conn_map.insert(make_pair(m_handle, this))很明显,这个映射表保存了每次连接的socket句柄(m_handle)和imconn对象(this)的映射关系。
当TT底层的事件分发器产生事件后,便会调用imconn_callback,里面有一个FindImConn会反查到对应的Conn,然后再调用Conn对象的OnConfirm等函数,这些函数就是你之前继承CImConn自己实现的。运行时多态有木有?是不是觉得TT的框架做的还挺不错的。conn对象的OnRead其实是最重要的一个函数,因为你的业务代码都将在这里面自行实现。
void imconn_callback(void* callback_data, uint8_t msg, uint32_t handle, void* pParam) { NOTUSED_ARG(handle); NOTUSED_ARG(pParam); if (!callback_data) return; ConnMap_t* conn_map = (ConnMap_t*)callback_data; CImConn* pConn = FindImConn(conn_map, handle); //这里将会通过socket句柄反查到对于的imconn if (!pConn) return; //log("msg=%d, handle=%d ", msg, handle); switch (msg) { case NETLIB_MSG_CONFIRM: pConn->OnConfirm(); //connect连接成功后会调此pConn的OnConfirm()函数 break; case NETLIB_MSG_READ: pConn->OnRead(); //业务代码会在这里面执行 break; case NETLIB_MSG_WRITE: pConn->OnWrite(); break; case NETLIB_MSG_CLOSE: pConn->OnClose(); break; default: log("!!!imconn_callback error msg: %d ", msg); break; } pConn->ReleaseRef(); }
看看OnRead代码,里面有一个HandlePdu
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_recv_bytes += ret; m_in_buf.IncWriteOffset(ret); m_last_recv_tick = get_tick_count(); } CImPdu* pPdu = NULL; try { while ( ( pPdu = CImPdu::ReadPdu(m_in_buf.GetBuffer(), m_in_buf.GetWriteOffset()) ) ) { uint32_t pdu_len = pPdu->GetLength(); HandlePdu(pPdu); //这里面将会完成各种业务代码 m_in_buf.Read(NULL, pdu_len); delete pPdu; pPdu = NULL; // ++g_recv_pkt_cnt; } } catch (CPduException& ex) { log("!!!catch exception, sid=%u, cid=%u, err_code=%u, err_msg=%s, close the connection ", ex.GetServiceId(), ex.GetCommandId(), ex.GetErrorCode(), ex.GetErrorMsg()); if (pPdu) { delete pPdu; pPdu = NULL; } OnClose(); } }
void CDBServConn::HandlePdu(CImPdu* pPdu) { switch (pPdu->GetCommandId()) { case CID_OTHER_HEARTBEAT: break; case CID_OTHER_VALIDATE_RSP: _HandleValidateResponse(pPdu ); break; case CID_LOGIN_RES_DEVICETOKEN: _HandleSetDeviceTokenResponse(pPdu); break; case CID_MSG_UNREAD_CNT_RESPONSE: _HandleUnreadMsgCountResponse( pPdu ); break; case CID_MSG_LIST_RESPONSE: _HandleGetMsgListResponse(pPdu); break; case CID_MSG_GET_BY_MSG_ID_RES: _HandleGetMsgByIdResponse(pPdu); break; case CID_MSG_DATA: _HandleMsgData(pPdu); break; case CID_MSG_GET_LATEST_MSG_ID_RSP: _HandleGetLatestMsgIDRsp(pPdu); break;
TT的conn框架简介就到此为止了,其实还有很多细节需要你自己去抠代码,慢慢来。
现在回到一开始说的在另一个线程里发起注册流程,你应该会很清楚整个过程是怎么做的了,其实就是继承CImConn,然后在里面发起连接和接受连接处理。这里摘一段我代码
net_handle_t CClientConn::Connect(const char* ip, uint16_t port, uint32_t idx) { m_handle = netlib_connect(ip, port, imconn_callback_sp, (void*)&s_client_conn_map); log("connect handle %d", m_handle); if (m_handle != NETLIB_INVALID_HANDLE) { log("in invalid %d", m_handle); s_client_conn_map.insert(make_pair(m_handle, shared_from_this()));//这里!!! } return m_handle; }
这个操作是在子线程里进行的,所以netlib_connect会把imconn_callback_sp加入到底层事件分发器里进行监听,而事件分发器是在主线程里运行的一个循环,这个循环会在socket文件句柄发生读写事件后对你加入的函数进行回调。所以netlib_connect会里面把imconn_callback加入主线程的监听器,主线程一旦监听到事件发生就会立刻调用此函数,而此函数里的
CImConn* pConn = FindImConn(conn_map, handle);
conn_map是在netlib_connect后insert的,所以就有可能出现FindImConn时,conn_map里面还没有来得及insert这对关系,也就造成了偶尔会发生connect后没有继续调用后续的OnConfirm函数,而你跑到服务端看,connect确实成功的奇怪现象。多线程真要命啊。。。
那么如何解决这个问题呢?这个不是本文的要讲的,各位有兴趣请自行考虑解决的方法,这里友情提示,加锁是没有用的。