tars开源框架地址:https://github.com/Tencent/Tars
系列文章:
鹅厂开源框架tars之日志服务
鹅厂开源框架tars之运营监控服务
鹅厂开源框架tars之基础组件
鹅厂开源框架tars之网络层实现
简介:Tars是腾讯从2008年到今天一直在使用的后台逻辑层的统一应用框架TAF(Total Application Framework),目前支持C++,Java,PHP,Nodejs语言。该框架为用户提供了涉及到开发、运维、以及测试的一整套解决方案,帮助一个产品或者服务快速开发、部署、测试、上线。 它集可扩展协议编解码、高性能RPC通信框架、名字路由与发现、发布监控、日志统计、配置管理等于一体,通过它可以快速用微服务的方式构建自己的稳定可靠的分布式应用,并实现完整有效的服务治理。目前该框架在腾讯内部,各大核心业务都在使用,颇受欢迎,基于该框架部署运行的服务节点规模达到上万个。
tars开源框架库里面用cpp实现了比较多的公用组件,这些组件一般统一放在util文件夹,在应用层也可以自由使用,工欲善其事必先利其器,所以有必要把这些工具组件做了解,更好的使用,提高效率
一、线程安全队列TC_ThreadQueue
先看下框架对TC_ThreadQueue类的使用如下:
typedef TC_ThreadQueue
typedef TC_ThreadQueue
TC_ThreadQueue的实现比较简单继承了TC_ThreadLock,从之前文章
《鹅厂开源框架tars之网络层实现》实现的介绍可以看到这个类比较重要,因为从框架中收到的网络包都会加入到这个缓存队列里面,然后多业务线程ServantHandle会调用waitForRecvQueue从该队列里面取网络数据包,然后调用dispatch调用协议消息对应的处理函数,先看下框架对TC_ThreadQueue的实现:
TC_ThreadQueue继承于TC_ThreadLock用于实现线程锁和wait如下(TC_ThreadLock 为普通线程锁,在下面介绍),看下队列的成员函数:push_front在队列前面加入数据,
如上图调用push_front函数的时候调用Lock lock(*this)加锁 ,避免网络层接收数据和业务层取同一队列的数据冲突,notify()通知等待在该锁上某一个线程醒过来 ,调用该函数之前必须加锁, ,因为有数据过来了,例如网络层有线程需要取包并进行分发处理
再看一个成员函数pop_front,从头部获取数据, 没有数据则等待.millsecond 阻塞等待时间(ms)
* 0 表示不阻塞
* -1 永久等待
template
{
Lock lock(*this);
if (_queue.empty())
{
if(millsecond == 0)
{
return false;
}
if(millsecond == (size_t)-1)
{
wait();
}
else
{
//超时了
if(!timedWait(millsecond))
{
return false;
}
}
}
if (_queue.empty())
{
return false;
}
t = _queue.front();
_queue.pop_front();
assert(_size > 0);
--_size;
return true;
}
BindAdapter::waitForRecvQueue的函数就是调用了pop_front函数,用于等待接收队列,函数原型如下:
这里BindAdapter::waitForRecvQueue调用waitForRecvQueue用于业务线程在等待服务器监听的适配器收到网络包后进行业务包的处理,这里传入的iWaitTime是0表示不阻塞等待数据,立即返回
二、TC_ThreadLock普通线程锁
第一点TC_ThreadQueue继承的TC_ThreadLock类的定义如下
typedef TC_Monitor
TC_Monitor 线程锁监控模板类.通常线程锁,都通过该类来使用,而不是直接用TC_ThreadMutex、TC_ThreadRecMutex 类的定义template
T _mutex; //互斥锁
mutable P _cond;//条件变量
typedef TC_LockT
typedef TC_TryLockT
第一个参数TC_ThreadMutex代表线程锁:同一个线程不可以重复加锁 ,包含成员变量mutable pthread_mutex_t _mutex;互斥锁(延伸阅读,这里TC_ThreadMutex.h还包括另外一个类:TC_ThreadRecMutex: *循环锁(一个线程可以加多次锁),使用和定义场景如下:)
typedef TC_Monitor
第二个参数TC_ThreadCond代表线程信号条件类:所有锁可以在上面等待信号发生成功变量mutable pthread_cond_t _cond;控制条件变量wait.结合实际的使用场景,TC_Monitor::timedWait()会调用TC_ThreadCond对象的timedWait函数,下一步调用posix线程库的pthread_cond_wait;TC_ThreadCond::signal()实现发送信号, 等待在该条件上的一个线程会醒
TC_LockT类定义:template
TC_LockT构造函数,传入互斥量初始化成员变量_mutex,TC_LockT构造函数实现:TC_LockT(const T& mutex) : _mutex(mutex)。分析到这里就可以推导出TC_Monitor 定义的typedef TC_LockT
Lock lock(*this);
因为TC_ThreadQueue继承于TC_Monitor类,所以这里等于定义了TC_LockT
TC_LockT的构造函数,传入参数this为TC_Monitor的子类对象,TC_LockT的构造函数调用_mutex.lock();实际就是调用了TC_Monitor对象的lock函数,TC_Monitor的lock函数实现:{_mutex.lock(); _nnotify = 0;} 由上文的分析可知这里_mutex为TC_ThreadRecMutex对象,进一步调用了TC_ThreadRecMutex::lock()成员函数:核心实现:{ int rc = pthread_mutex_lock(&_mutex);} 调用了POSIX线程的pthread_mutex_lock函数,函数参数定义为posix互斥锁pthread_mutex_t _mutex。然后上面定义的lock栈变量退出函数的时候调用~TC_LockT的析构函数:实现如下:
virtual ~TC_LockT()
{
if (_acquired)
{
_mutex.unlock(); //这里会调用TC_Monitor的unlock函数
}
}
下一步TC_Monitor的unlock函数实现:
void unlock() const
{
notifyImpl(_nnotify);
_mutex.unlock(); //这里会调用posix线程库函数pthread_mutex_unlock(&_mutex);
}
下一步notifyImpl实现,这里为什么要调用 notifyImpl(_nnotify);函数是因为TC_Monitor类不只可以实现简单的互斥锁功能,还可以实现条件变量Condition功能,这里解锁了需要通知其他等待锁资源的线程,至于notifyImpl函数的实现见下文,例如从第一点的线程安全队列TC_ThreadQueue类,调用pop_front函数可以传入第二个参数millsecond等待时间参数:实现如下
template
{ ....
Lock lock(*this); //这里加互斥锁
if(!timedWait(millsecond)) //如果缓存队列为空,则根据millsecond决定是否阻塞等待
{
return false;
}
....
}
下一步timedWait实现
bool timedWait(int millsecond) const
{
notifyImpl(_nnotify);
bool rc;
try
{
rc = _cond.timedWait(_mutex, millsecond); //这里就实现了线程的等待指定时间
}
catch(...)
{
_nnotify = 0;
throw;
}
_nnotify = 0;
return rc;
}
分析了pop_front知道线程如果等待数据没到,根据传入的millsecond参数有可能阻塞等待数据,所以缓存队列TC_ThreadQueue必然在收到数据的时候,有一个通知的机制,看下push数据的函数实现:
template
{
Lock lock(*this);
notify(); //这里调用TC_ThreadQueue父类TC_Monitor的成员函数通知等待在该锁上某一个线程醒过来:++_nnotify
_queue.push_back(t);
++_size;
}
互斥锁TC_Monitor::unlock的时候,会调用notifyImpl函数,通知其他线程解锁
void notifyImpl(int nnotify) const
{
if(nnotify != 0) //nnotify上锁的次数
{
if(nnotify == -1)
{
_cond.broadcast();
return;
}
else
{
while(nnotify > 0)
{
_cond.signal();
--nnotify;
}
}
}
}
三、TC_Thread线程基类
还是老样子,先看下项目实际对BasicThread类的使用,实际项目使用中,我们对TC_Thread又封装了一下,实现了一个BasicThread 类,下面看下BasicThread 的定义:
class BasicThread : public taf::TC_Thread, public taf::TC_ThreadLock
{
...
void terminate()
{
_bTerm = true;
{
Lock lock(*this);
notifyAll();
}
getThreadControl().join();
}
}
BasicThread类,继承了TC_Thread和TC_ThreadLock,其中TC_ThreadLock第二点已经说明过了,所以这里重点看下TC_Thread类的使用,TC_Thread的定义
class TC_Thread : public TC_Runable
{
...
TC_ThreadControl start();//调用posix的pthread_create 参数threadEntry线程函数,返回TC_ThreadControl(_tid)
static void threadEntry(TC_Thread *pThread); //静态函数, 线程入口
virtual void run() = 0;
....
}
下一步看下TC_ThreadControl的定义: 线程控制类
class TC_ThreadControl
{
TC_ThreadControl::TC_ThreadControl() : _thread(pthread_self()) //构造,传入pthread_self(): 查询线程自身线程标识号
join() //调用posix的pthread_join阻塞当前的线程,直到另外一个线程运行结束
sleep() 调用nanosleep函数线程将暂停执行
}
下一步看下TC_Runable的定义:
class TC_Runable
{
public:
virtual ~TC_Runable(){};
virtual void run() = 0; //定义了run纯虚函数
};
最后看下实际项目中对线程类的使用
class AntiSdkSyncThread : public BasicThread //这里等于多继承了C_Thread和TC_ThreadLock两个类
{
void run() //实现基类的纯虚函数
{
Lock lock(*this);
timedWait(10 * 1000); (间隔执行时间,实现了线程的定时执行功能)
if(NULL != g_busi_interf)
{
Int32 ret = g_busi_interf->proc_(); //需要定期执行的函数
}
}
}
定义好了AntiSdkSyncThread g_antiSdkSyncThread;类,那么需要启动线程的时候执行g_antiSdkSyncThread.start();就会自然创建线程,并且hreadEntry线程函数会调用pThread->run()多态函数,进程退出的时候调用g_antiSdkSyncThread.terminate();
四、智能指针
这里的智能指针可以放在容器中,且线程安全的智能指针,CPP11标准库的auto_ptr是不能放在容器中的,貌似已经被淘汰了,目前多数使用CPP11标准库的shared_ptr,不过需要编译器支持cpp11.
TC_HandleBaseT 智能指针基类.所有需要智能指针支持的类都需要从该对象继承
template
class TC_HandleBaseT
{
...
atomic_type _atomic; 用于计数
void decRef()
{
if(_atomic.dec_and_test() && !_bNoDelete)
{
_bNoDelete = true;
delete this;
}
}
...
}
typedef TC_HandleBaseT
下一步看 TC_Atomic实现原子操作类,对int做原子操作
class TC_Atomic
{
...TC_Atomic& operator++()
TC_Atomic& operator--()
}
下一步看TC_AutoPtr智能指针模板类. 可以放在容器中,且线程安全的智能指针,该智能指针通过引用计数实现
template
{
TC_AutoPtr(T* p = 0)
{
_ptr = p;
if(_ptr)
{
_ptr->incRef(); //构造函数 引用计算加1
}
}
~TC_AutoPtr()
{
if(_ptr)
{
_ptr->decRef();//引用计算减1
}
}
}
例子:实战项目使用:
struct ConnStruct : public TC_HandleBase{...}
typedef TC_AutoPtr
TC_AutoPtr拷贝构造调用_ptr->incRef();这里ptr为ConnStruct,ConnStruct继承于TC_HandleBase,等于调用了TC_HandleBaseT
所以等于调用了TC_Atomic类的++操作符重载函数,下面看下taf_atomic_add_return的实现
static __inline__ int taf_atomic_add_return(int i, taf_atomic_t *v)
{
/* Modern 486+ processor */
int __i = i;
__asm__ __volatile__(
TAF_LOCK "xaddl %0, %1;"
:"=r"(i)
:"m"(v->counter), "0"(i));
return i + __i;
}
引用计数原子操作加1、析构引用计数原子操作减1,当引用计数减少到0时根据设置的开关是否要进行删除来决定是否触发delete
例子: 这是tar使用异步rpc回调的典型例子,这里回调类使用了智能指针
typedef TC_AutoPtr
//创建回调类SessionCallbackPtr,并传入初始化参数uin gameid等;
SessionCallbackPtr cb = new SessionCallback(iUin, iGameId, iSeqID, iCmd, sSessionID, theServant, current, cs, this);
getSessionPrx()->async_getSession(cb, iUin, iGameId); //异步调用sessionserver远程接口
接口返回完成,回调SessionCallback::callback_getSession(taf::Int32 ret, const MGComm::SessionValue& retValue)函数
接收sessionserver接口的返回的SessionValue结构
因为SessionCallbackPtr使用了智能指针,所以业务不需要去手动释放前面new出来的SessionCallbackPtr,还是比较方便的
五、mysql操作类:TC_Mysql
TC_Mysql封装好的mysql操作类,非线程安全,对于insert/update可以有更好的函数封装,防止SQL注入
使用方式:
TC_Mysql mysql;
//初始化mysql,init时不链接,请求时自动建立链接;
//数据库可以为空;
//端口默认为3306
mysql.init("10.1.36.39", "pc", "pc@sn", "db_dmqq_system");
通常用:void init(const TC_DBConf& tcDBConf);直接初始化数据库:例如:stDirectMysql.init(_stZoneDirectDBConf);
看下TC_DBConf的定义
//进一步看下获取数据的使用
TC_Mysql::MysqlData data;
data = mysql.queryRecord("select * from t_app_users");
for(size_t i = 0; i < data.size(); i++)
{
//如果不存在ID字段,则抛出异常
cout << data[i]["ID"] << endl;
}
查询出来的mysql数据用MysqlData封装
class MysqlData
{ ...
vector
...
}
//插入数据,指定数据的类型:数值 或 字符串,对于字符串会自动转义
map
m["ID"] = make_pair(TC_Mysql::DB_INT, "2334");
m["USERID"] = make_pair(TC_Mysql::DB_STR, "abcttt");
m["APP"] = make_pair(TC_Mysql::DB_STR, "abcapbbp");
m["LASTTIME"] = make_pair(TC_Mysql::DB_INT, "now()");
mysql.replaceRecord("t_user_logs", m);
六、网络组件
整个tars核心就提供一个很完善的网络框架,包括rpc功能,这里只介绍几个常用的网络组件,详细的网络层实现在另外一篇文章单独介绍:鹅厂开源框架tars之网络层实现
6.1 TC_Socket 封装了socket的基本方法
支持本地域套接字;
再下一层tars封装了TC_TCPClient和TC_UDPClient两个类用于实际操作tcp和udp应用
使用方式:
例如:tcp客户端
TC_TCPClient stRouterClient;
stRouterClient.init(sIP, iPort, iTimeOut); 这里传入ip和端口然后调用sendRecv进行消息的收发
Int32 ret = stRouterClient.sendRecv(request.c_str(), request.length(), recvBuf, iRecvLen);
注意多线程使用的时候,不能多线程同时send/recv,小心串包
6.2TC_Epoller :
提供网络epoll的操作类
默认是ET模式,当状态发生变化的时候才获得通知
提供add、mod、del、wait等基础操作
使用方式:(见tars之网络层实现CommunicatorEpoll部分)
6.3 TC_ClientSocket 客户端socket相关操作基类:
提供关键成员函数init(const string &sIp, int iPort, int iTimeout); 传入ip 端口 和 超时时间
TC_TCPClient继承于TC_ClientSocket 提供成员函数:
sendRecv(发送到服务器, 从服务器返回不超过iRecvLen的字节)
sendRecvBySep( 发送倒服务器, 并等待服务器直到结尾字符, 包含结尾字符)
例子:
stRouterClient.init(sIP, iPort, iTimeOut);
size_t iRecvLen = sizeof(recvBuf)-1;
Int32 ret = stRouterClient.sendRecv(request.c_str(), request.length(), recvBuf, iRecvLen);
同理还有TC_UDPClient实现UDP客户端
七、 命令解析、配置文件
8.1TC_Config
读取配置文件是线程安全的,insert域等函数非线程安全
例子:
TC_Config config;
config.parseFile(ServerConfig::BasePath + ServerConfig::ServerName + ".conf");
stTmpGameServerConfig.iGameId = TC_Common::strto
配置文件样例
使用get方法例子:如果读不到该配置,则返回默认值:sDefault
stTmpGameServerConfig.iMaxRegNum = TC_Common::strto
8.2 TC_Option
分析main的输入参数,支持以下形式的参数:
./main.exe --name=value --param1 param2 param3
TC_Option op;
//解析命令行
op.decode(argc, argv);
//获取成对的参数,即获取 - - 表示的所有参数对
map
//表示非 – 的参数:即 param2, param3
vector
如果value,param有空格或者--,用引号括起来就可以了
九、原子计数类
TC_Atomic
例子:
TC_Atomic g_loginSeqNo;
pc.ServiceSeq = g_loginSeqNo.inc();
pc.SeqNo = g_loginSeqNo.inc();
十、通用仿函数类
TC_Functor 参考loki库的设计
1.仿函数对象调用方式, 即对上述的几种方式都可以在右侧添加一对圆括号,并在括号内部放一组合适的参数来调用,例如a(p1,p2);
2.把整个调用(包括参数)封装一个函数对象, 调用对象建立时就传入了参数,调用的时候不用传入参数,例如A a(p1, p2); a();
简单又好用的封装,具体见下面使用例子自然明白:
10.1 C函数调用
void TestFunction3(const string &s, int i){
cout << "TestFunction3('" << s << "', '" << i << "')" << endl; }
//采用函数指针构造对象
TC_Functor
string s3("s3");
cmd3(s3, 10);
C函数调用用wrapper封装:
//调用封装,构造的时候传入参数
TC_Functor
fwrapper3(); //参数已经在构造的时候传入,调用的时候不用传参数了
说明:
对于调用的封装,注意对于传引用类型,具体的调用时候要保证引用的对象存在
10.2 C++指向类成员函数的调用
struct TestMember
{
void mem3(const string &s, int i)
{
cout << "TestMember::mem3(" << s << "," << i << ") called" << endl;
}
}
TC_Functor
cmd3("a", 33);
指向类成员函数的调用用wrapper封装:
TC_Functor
fwrapper3();
实际例子:注册协议解析器
服务初始化initialize的时候,一般会调用addServantProtocol(sRouterObj, AppProtocol::parseStream<0, uint16_t, false>,iHeaderLen);
这里设置BindAdapter的协议解析函数protocol_functor _pf为parseStream函数,如下:
注册好解析函数之后,网络层收包调用parseProtocol函数
TC_EpollServer::Connection::parseProtocol(recv_queue::queue_type &o)
{
int b = _pBindAdapter->getProtocol()(_recvbuffer, ro); //这里回调前面设置好的协议解析函数,从而实现协议解析
}
一十一、强大的loki库
11.1 TypeTraits
在编译期间对类型的特性进行提取和判断
例子:
bool b;
b = TL::TypeTraits
cout << "vector
b = TL::TypeTraits
cout << "char* is " << b << endl;
b = TL::TypeTraits
cout << "char& is " << b << endl;
11.2.TypeSelect 类型选择器 在编译期从两个类型中选择适当类型
例子:字符串转换模板
template
T TC_Common::strto(const string &sStr)
{
typedef typename TL::TypeSelect
return strto_type()(sStr);
}
模板使用例子:字符串转整形
stTmpGameServerConfig.iGameId = TC_Common::strto
一十二、util/tc_hash_fun.h
*可以对输入
的字节流进行hash得到相当均匀的hash值 ;使用例子:
一十三、TC_Exception 异常类
class TC_Exception : public exception
{
* @brief 构造函数,提供了一个可以传入errno的构造函数,
*
* 异常抛出时直接获取的错误信息
*
* @param buffer 异常的告警信息
* @param err 错误码, 可用strerror获取错误信息
*/
TC_Exception(const string &buffer, int err);
}