teamtalk的conn框架简介及netlib线程安全问题

最近把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*){}



接口的含义是显而易见的,OnConnect就是有连接接入事件的响应函数, OnConfirm这个含义有点含糊,其实就是你发起netlib_connect,当这个connect连接建立完成时调用的函数。

这里为了方便你理解,做个类比,如果你做过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));
	}
}



CDBServConn是消息服务器像数据库代理发起连接时需要继承的一个CImConn类,里面的Connect函数是发起连接时调用的。看里面有调到netlib_connect,并传入imconn_callback和g_db_server_conn_map。

这两个参数就是连接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();
	}
}



摘一段CDBServConn的HandlePdu
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;



里面的handler就是对应不同协议的处理器。所以到此,你就会差不多明白,大部分时候,你要做的就是继承CImConn然后写handler。

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;
}



注意这里我自己的代码跟之前给出的TT源码略有不同,imconn_callback_sp是我改成智能指针的版本,插入conn_map表的不是原始this指针,而是shared_ptr。

这个操作是在子线程里进行的,所以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确实成功的奇怪现象。多线程真要命啊。。。

那么如何解决这个问题呢?这个不是本文的要讲的,各位有兴趣请自行考虑解决的方法,这里友情提示,加锁是没有用的。

你可能感兴趣的:(teamtalk的conn框架简介及netlib线程安全问题)