本文源自herman的系列文章之一《鹅厂开源框架TARS之基础组件(文末附链接)》。相关代码已按TARS开源社区最新版本更新。
TARS开源框架库里面用C 实现了比较多的公用组件,这些组件一般统一放在 `util` 文件夹,在应用层也可以自由使用,工欲善其事必先利其器,所以有必要把这些工具组件做了解,更好的使用,提高效率。接下来,本文将对如下TarsCpp组件进行分析:
• 线程操作
o 线程安全队列: TC_ThreadQueue
o 普通线程锁: TC_ThreadLock
o 线程基类: TC_Thread
• 智能指针
o 智能指针类: TC_AutoPtr
• DB操作
o MySQL操作类: TC_Mysql
• 网络操作
o 网络组件
• 服务配置
o 命令解析类: TC_Option
o 配置文件类: TC_Config
• 仿函数
o 通用仿函数类: TC_Functor
• Hash
o hash算法
• 异常处理
o 异常类: TC_Exception
先看下框架对TC_ThreadQueue类的使用如下:
`TC_ThreadQueue` 的实现比较简单,在TARS的网络层实现中可以发现这个类比较重要,因为从框架中收到的网络包都会加入到这个缓存队列里面,然后多业务线程 `ServantHandle` 会调用 `waitForRecvQueue` 从该队列里面取网络数据包,然后调用 `dispatch` 调用协议消息对应的处理函数,先看下框架对 `TC_ThreadQueue` 的实现:
TC_ThreadQueue使用了C 11标准库中的<mutex>和<condition_variable>用于实现线程锁和 wait,如下,看下队列的成员函数:push_front 在队列前面加入数据,
如上图调用`push_front`函数的时候调用 `std::unique_lock<std::mutex> lock(_mutex)`加锁 ,避免网络层接收数据和业务层取同一队列的数据冲突,`_cond.notify_one()` 通知等待在该锁上某一个线程醒过来,调用该函数之前必须加锁,因为有数据过来了,例如网络层有线程需要取包并进行分发处理。
再看一个成员函数`pop_front`,从头部获取数据,没有数据则等待。`millisecond` 阻塞等待时间(ms)
• `0` 表示不阻塞
• `-1` 永久等待
BindAdapter::waitForRecvQueue的函数就是调用了pop_front函数,用于等待接收队列,函数原型如下:
这里BindAdapter::waitForRecvQueue用于业务线程在等待服务器监听的适配器收到网络包后进行业务包的处理,这里传入的handleIndex表示接收队列索引,获取对应的_rbuffer。
TC_ThreadLock 类的定义如下
TC_Monitor 线程锁监控模板类。通常线程锁,都通过该类来使用,而不是直接用TC_ThreadMutex、TC_ThreadRecMutex。
类的定义template <class T, class P> class TC_Monitor 需要传入两个模板参数,TC_Monitor 包括以下成员变量:
第一个参数 TC_ThreadMutex 代表线程锁:同一个线程不可以重复加锁 ,包含成员变量
• 延伸阅读,这里 tc_thread_mutex.h 还包括另外一个循环锁类 TC_ThreadRecMutex,即一个线程可以加多次锁,定义如下:
第二个参数 TC_ThreadCond 代表线程信号条件类:所有锁可以在上面等待信号发生,包含线程条件成员变量:
结合实际的使用场景,TC_Monitor::timedWait() 会调用 TC_ThreadCond 对象的 timedWait 函数,下一步调用 chrono 库的 milliseconds;TC_ThreadCond::signal() 实现发送信号,等待在该条件上的一个线程会醒。
TC_LockT类定义: template <typename T> class TC_LockT锁模板类,与其他具体锁配合使用,构造时候加锁,析够的时候解锁。
TC_LockT 构造函数,传入互斥量初始化成员变量 _mutex,TC_LockT构造函数实现:
到这里就可以看出 TC_Monitor 定义的 typedef TC_LockT<TC_Monitor<T, P> > Lock,这里 Lock 类型的模板参数用的是 TC_Monitor 类。
实际使用场景如下:
TC_LockT 的构造函数,传入参数 this 为 TC_Monitor 的子类对象,TC_LockT 的构造函数调用_mutex.lock();实际就是调用了 TC_Monitor 对象的 lock 函数,TC_Monitor 的 lock 函数实现:
这里 _mutex 为 TC_ThreadMutex 对象,进一步调用了 TC_ThreadRecMutex::lock() 成员函数,实现如下:
然后上面定义的lock栈变量退出函数的时候调用 TC_LockT 的析构函数:实现如下:
TC_Monitor 的 unlock 函数实现:
这里调用 notifyImpl 函数是因为 TC_Monitor 类不只可以实现简单的互斥锁功能,还可以实现条件变量Condition功能,其中 notifyImpl 的实现为
还是老样子,先看下项目实际对线程基类的使用。实际项目使用中,我们对 TC_Thread 又封装了一下,实现了一个BasicThread 类,下面看下 BasicThread 的定义:
BasicThread 类,继承了 TC_Thread 和 TC_ThreadLock ,其中 TC_ThreadLock 第二点已经说明过了,所以这里重点看下 TC_Thread 类的使用,TC_Thread 的定义
下一步看下线程控制类 TC_ThreadControl 的定义:
下一步看下 TC_Runable 的定义:
最后看下实际项目中对线程类的使用
定义好了 AntiSdkSyncThread g_antiSdkSyncThread; 类,那么需要启动线程的时候执行g_antiSdkSyncThread.start(); 就会自然创建线程,并且 threadEntry 线程函数会调用 pThread->run() 多态函数,进程退出的时候调用 g_antiSdkSyncThread.terminate();。
这里的智能指针可以放在容器中,且线程安全的智能指针,CPP11标准库的auto_ptr是不能放在容器中的,貌似已经被淘汰了,目前多数使用CPP11标准库的shared_ptr,不过需要编译器支持CPP11。
TC_HandleBase智能指针基类的定义如下,所有需要智能指针的类都需要从该对象继承,其中使用了C 11标准库中的<atomic>进行原子计数。
例子:实战项目使用
TC_AutoPtr 拷贝构造调用 _ptr->incRef(); 这里 ptr 为 ConnStruct,ConnStruct继承于TC_HandleBase,等于调用了TC_HandleBaseT<int>::incRef() { _atomic;}
引用计数原子操作加1、析构引用计数原子操作减1,当引用计数减少到0时根据设置的开关是否要进行删除来决定是否触发delete。
例子:这是TARS使用异步rpc回调的典型例子,这里回调类使用了智能指针
接口返回完成,回调SessionCallback::callback_getSession(tars::Int32 ret, const MGComm::SessionValue& retValue)函数,接收sessionserver接口的返回的SessionValue结构。
因为 SessionCallbackPtr 使用了智能指针,所以业务不需要去手动释放前面 new 出来的 SessionCallbackPtr ,还是比较方便的。
TC_Mysql封装好的mysql操作类,非线程安全,对于 insert/update 可以有更好的函数封装,防止SQL注入
使用方式:
通常用:void init(const TC_DBConf& tcDBConf); 直接初始化数据库。例如:stDirectMysql.init(_stZoneDirectDBConf);
看下TC_DBConf的定义
查询出来的mysql数据用MysqlData封装
整个TARS核心就提供一个很完善的网络框架,包括RPC功能,这里只介绍几个常用的网络组件。
TC_Socket : 封装了socket的基本方法
提供socket的操作类;支持tcp/udp socket;支持本地域套接字。
再下一层TARS封装了TC_TCPClient和TC_UDPClient两个类用于实际操作tcp和udp应用。
使用方式:
例如:tcp客户端
注意多线程使用的时候,不能多线程同时send/recv,小心串包。
TC_Epoller
提供网络epoll的操作类,默认是ET模式,当状态发生变化的时候才获得通知,提供add、mod、del、wait等基础操作。
TC_ClientSocket : 客户端socket相关操作基类
提供关键成员函数`init(const string &sIp, int iPort, int iTimeout)`,传入 IP 端口 和 超时时间
`TC_TCPClient` 继承于 `TC_ClientSocket` 提供成员函数:
- `sendRecv`(发送到服务器, 从服务器返回不超过iRecvLen的字节)
- `sendRecvBySep`( 发送倒服务器, 并等待服务器直到结尾字符, 包含结尾字符)
例子:
同理还有TC_UDPClient实现UDP客户端。
1. 命令解析类;
2. 通常用于解析命令行参数;
3. 只支持双—的参数形式
4. 分析`main`的输入参数,支持以下形式的参数:
如果value,param有空格或者 -- ,用引号括起来就可以了。
1. 配置文件解析类(兼容wbl模式);
2. 支持从string中解析配置文件;
3. 支持生成配置文件;
4. 解析出错抛出异常;
5. 采用[]获取配置,如果无配置则抛出异常;
6. 采用get获取配置,不存在则返回空;
7. 读取配置文件是线程安全的,insert域等函数非线程安全
例子:
配置文件样例
使用get方法例子:如果读不到该配置,则返回默认值 sDefault,即下面例子中的 20000000
`TC_Functor` 参考`loki`库的设计
1. 仿函数对象调用方式, 即对上述的几种方式都可以在右侧添加一对圆括号,并在括号内部放一组合适的参数来调用,例如 `a(p1,p2);`
2. 把整个调用(包括参数)封装一个函数对象, 调用对象建立时就传入了参数,调用的时候不用传入参数,例如 `A a(p1, p2); a();`
简单又好用的封装,具体见下面使用例子自然明白:
C函数调用
C函数调用用wrapper封装:
说明:
• `void` : 函数的返回值
• `TL::TLMaker<const string&, int>::Result` : 代表参数类型
对于调用的封装,注意对于传引用类型,具体的调用时候要保证引用的对象存在。
C 指向类成员函数的调用
指向类成员函数的调用用wrapper封装:
实际例子:注册协议解析器
服务初始化initialize的时候,一般会调用
这里设置BindAdapter的协议解析函数 protocol_functor _pf 为 parseStream 函数,如下:
注册好解析函数之后,网络层收包调用parseProtocol函数
util/tc_hash_fun.h中包含了对hash算法的实现,使用 hash_new ,可以对输入的字节流进行hash得到相当均匀的hash值,使用方式如下
本文介绍分析了TARS框架中用C 实现的公用基础组件,加深对这些工具类基础组件的理解,减少在使用这些组件过程中产生的问题,提高开发效率。
TARS可以在考虑到易用性和高性能的同时快速构建系统并自动生成代码,帮助开发人员和企业以微服务的方式快速构建自己稳定可靠的分布式应用,从而令开发人员只关注业务逻辑,提高运营效率。多语言、敏捷研发、高可用和高效运营的特性使 TARS 成为企业级产品。
附文中所有链接:
鹅厂开源框架TARS之基础组件:https://blog.csdn.net/cyblueboy83/article/details/82817269
TARS基金会是Linux基金会下的非营利性、微服务基金会,致力于建设一个强大而灵活的微服务生态系统。无论你在哪个行业,无论你使用什么技术栈,这里能助你快速实现你的创意。