文档章节

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

笨笨_蛋蛋
 笨笨_蛋蛋
发布于 2015/08/02 11:52
字数 2361
阅读 654
收藏 4

最近把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确实成功的奇怪现象。多线程真要命啊。。。

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

© 著作权归作者所有

笨笨_蛋蛋
粉丝 34
博文 6
码字总数 11619
作品 0
南京
私信 提问
加载中

评论(1)

笨笨_蛋蛋
笨笨_蛋蛋
突然想到一个跟文章标题有关的问题,很多人可能不清楚框架和库的关系和区别,解释一下,框架是定义好一堆代码的架子,然后让你往里面填东西的,库是定义好一组接口函数给你调用的,一句话来讲就是框架是调用你定义的函数,库是你调用它的函数。
用libevent改造teamtalk底层网络框架

(没想到文章被oschina置顶推荐了,赶紧来加个广告,欢迎加入我们的teamtalk qq群437335108,此群是原蘑菇街teamtalk官方群管理者蓝狐离开蘑菇街后新开辟的分支群。) teamtalk是蘑菇街推出的...

笨笨_蛋蛋
2015/09/02
0
14
开源IM工程“蘑菇街TeamTalk”的现状:一场有始无终的开源秀

1、前言 随着云IM的发展,已吸引越来越多有IM需求的APP接入。但考虑到云IM无论从商业模式还是运营模式上,还需经过多年的沉淀,才可能真正实现客户与服务商的运营和服务良性循环的双赢局面。...

JackJiang-
2016/07/28
2.4K
4
TeamTalk 牵涉网易泡泡版权,被 Github 下架

TeamTalk 是蘑菇街发布的一款开源软件,该项目托管在 Github 平台上。不过你可能注意到了目前该账号下所有跟 TeamTalk 相关的软件仓库都已经被 Github 禁用了,目前访问这些项目会看到提示:...

oschina
2014/11/05
20.6K
71
TeamTalk 的公开声明

TeamTalk关于“TT牵涉POPO版权,被 Github 下架”一事的公开声明 TeamTalk系蘑菇街技术团队几位工程师利用业余时间开发的一套IM软件,一直被蘑菇街用于公司内部沟通使用。今年9月26日,我们决...

张远浩
2014/11/05
18.2K
97
关于TeamTalk IM服务器上的登录、消息部分的接口

由于工作原因,需要整理一份TeamTalk IM服务器的接口文档,但是现在基本找不到TeamTalk相关的资料了,跪求大佬们支招

湖居散人
2018/08/17
326
0

没有更多内容

加载失败,请刷新页面

加载更多

Linux learn(一)

参考书:鸟哥Linux私房菜-第四版 4.3 Linux的在线求助man page与info page man man: manual(操作说明)的简写。作用是查看某个文件或者指令的文档,操作手册,q退出 eg: man date 上图中的DAT...

lazy~
16分钟前
0
0
微信,QQ这类IM app怎么做——谈谈Websocket

前言 关于我和WebSocket的缘:我从大二在计算机网络课上听老师讲过之后,第一次使用就到了毕业之后的第一份工作。直到最近换了工作,到了一家是含有IM社交聊天功能的app的时候,我觉得我现在...

tantexian
17分钟前
0
0
Dubbo 支持哪些序列化协议?

面试题 dubbo 支持哪些通信协议?支持哪些序列化协议?说一下 Hessian 的数据结构?PB 知道吗?为什么 PB 的效率是最高的? 面试官心理分析 上一个问题,说说 dubbo 的基本工作原理,那是你必...

李红欧巴
21分钟前
10
0
Hyperledger Fabric Node.js如何使用基于通道的事件服务

本教程说明了基于通道的事件的使用。这些事件与现有事件类似,但是特定于单个通道。在设置侦听器时,客户端处理基于通道的事件有一些新选项。从v1.1开始,基于通道的事件是Hyperledger Fabri...

geek12345
27分钟前
0
0
Java中print、printf、println的区别

printf主要是继承了C语言的printf的一些特性,可以进行格式化输出 print就是一般的标准输出,但是不换行 println和print基本没什么差别,就是最后会换行

hellation_
42分钟前
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部