TARS学习笔记(二)

背景

一直说Tars是使用epoll模型的,而且之前校招面试的时候也老会被问到epoll,所以这次打算先把epoll看一下,看看tars是怎么做epoll的,但由于对网络编程没有什么了解,所以会先看一些基础的其他类。

在网上找了一个系列博客,感觉还挺多的,可以看看。

https://blog.csdn.net/stpeace/category_7554860.html

大概会照着他这个目录看一看。

他的目录中第二篇看的是一个tc_loki.h,但我看了下和epoll暂时没有什么关系,所以先跳过这个文件,从tc_commom.h开始看。

tc_commom

这个文件是基础工具类,主要是tars提供的一些common函数,能够在开发中直接使用的,能节省很多时间,都是一些有用的小函数,而且都是static的,可以直接用,工作中使用过很多。

大致扫了一眼头文件,发现原来提供了这么多基础函数啊,以后类似在C++中遇到分割字符串等基础工作再也不用手动造轮子了,用这里面的基础函数既省事也放心,函数类型大致可以分为以下几类:

  • sleep函数
  • 浮点数比较
  • 字符串相关操作
  • 时间相关
  • 数值和字符串或者时间转换
  • 其他

针对其中一些函数,还使用了模板特化

tc_exception

tars中的异常类,主要包装了一个buffer和一个错误码code,并且针对不同平台做了一些兼容处理。

tc_socket

封装了socket编程,看源码里注释都很清晰了,这里挑几个函数看一看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
* 这里面用到了几个没见过的值,简单说明一下
* SOCK_STREAM表示基于TCP,SOCK_DGRAM则是基于UDP,表示tars是支持这两种协议的,而且默认是SOCK_STREAM
* iDomain的默认值是AF_INET,查了下就理解成基于ipv4的地址就行,就是我们常用的那种host:port
*/
void TC_Socket::createSocket(int iSocketType, int iDomain)
{
assert(iSocketType == SOCK_STREAM || iSocketType == SOCK_DGRAM);
close();

_iDomain = iDomain;
_sock = socket(iDomain, iSocketType, 0);

if(_sock < 0)
{
_sock = INVALID_SOCKET;
THROW_EXCEPTION_SYSCODE(TC_Socket_Exception, "[TC_Socket::createSocket] create socket error");
// throw TC_Socket_Exception("[TC_Socket::createSocket] create socket error! :" + string(strerror(errno)));
}
else
{
ignoreSigPipe();//这个是用来忽视什么信号的,系列2里有
}

}

对socket编程完全不了解呀,看下来云里雾里,但感觉就是把socket抽象封装了一层,以后再看。

tc_epoll

epoll操作封装类。

首先有一个平平无奇的TC_Epoller_Exception类。

TC_Epoller默认采用了ET方式触发,epoll初始化不是线程安全的,需要在一开始全局设置一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//创建epoll,可以看到这个size其实最终并没有赋值给_max_connections,
//这里和博文里的代码不一样,想必是tars源码做过改动,size要大于零
void TC_Epoller::create(int size)
{
#if TARGET_PLATFORM_IOS
_iEpollfd = kqueue();
#else
_iEpollfd = epoll_create(size);
#endif
if (nullptr != _pevs)
{
delete[] _pevs;
}

_max_connections = 1024;

_pevs = new epoll_event[_max_connections];
}


//这个方法再往底层都加了锁,不管是add、mod还是del
void TC_Epoller::ctrl(SOCKET_TYPE fd, uint64_t data, uint32_t events, int op)
{
struct epoll_event ev;
ev.data.u64 = data;

#if TARGET_PLATFORM_WINDOWS
ev.events = events;
#else
ev.events = events | EPOLLET;//可以看到linux下默认都会加上ET的,所以默认是ET模式
#endif

epoll_ctl(_iEpollfd, op, fd, &ev);
}

tc_autoptr.h

这是tars里自己实现的自动指针类,底层利用atomic实现了引用计数,所有需要智能指针的类都需要从TC_HandleBase类继承。

除了atomic引用计数外,该类还有一个表示是否删除的标志位,看起来是为了防止del多次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class UTIL_DLL_API TC_HandleBase
{
public:

/**
* @brief 复制.
*
* @return TC_HandleBase&
*/
TC_HandleBase& operator=(const TC_HandleBase&)
{
return *this;
}

/**
* @brief 增加计数
*/
void incRef() { ++_atomic; }

/**
* @brief 减少计数, 当计数==0时, 且需要删除数据时, 释放对象
*/
void decRef()
{
if((--_atomic) == 0 && !_bNoDelete)
{
_bNoDelete = true;
delete this;
}
}

/**
* @brief 获取计数.
*
* @return int 计数值
*/
int getRef() const { return _atomic; }

/**
* @brief 设置不自动释放.
*
* @param b 是否自动删除,true or false
*/
void setNoDelete(bool b) { _bNoDelete = b; }

protected:

/**
* @brief 构造函数
*/
TC_HandleBase() : _atomic(0), _bNoDelete(false)
{
}

/**
* @brief 拷贝构造
*/
TC_HandleBase(const TC_HandleBase&) : _atomic(0), _bNoDelete(false)
{
}

/**
* @brief 析够
*/
virtual ~TC_HandleBase()
{
}

protected:

/**
* 计数
*/
std::atomic<int> _atomic;

/**
* 是否自动删除
*/
bool _bNoDelete;
};

接下来是一个智能指针模板类TC_AutoPtr,方法没什么好说的,只是要注意模板参数T必须继承于TC_HandleBase

还有个要注意点的点是这些智能指针类之间的等于、不等于、小于比较全都是直接比较指针本身,而不是比较指针指向的值。

tc_config

配置文件读取类。

配置文件能实现配置参数和代码逻辑的分离,如果需要修改配置参数,仅仅修改配置即可。

配置文件说白了,就是把数据从外存导到内存来使用(我觉得这个理解挺好的)。

tars的配置文件在我使用过程中就是类似于读取/a/b/c<d>这种格式,就能从第一层a开始取到最后一层d所指的数据,对应下面这种格式的配置文件:

1
2
3
4
5
6
7
<a>
<b>
<c>
d=hello
</c>
</b>
</a>

tars的配置文件就长这样,所以这个tc_config类主要就是用来解析这种格式的文件,知道了作用,接下来开始看源码。

Tars读取配置文件是线程安全的,将整个配置文件分为了域和参数两部分,其中kv键值对的k是参数,而<>表示的是域,用以下宏定义的字符来分割。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 域分隔符
*/
const char TC_CONFIG_DOMAIN_SEP = '/';

/**
* 参数开始符
*/
const char TC_CONFIG_PARAM_BEGIN = '<';

/**
* 参数结束符
*/
const char TC_CONFIG_PARAM_END = '>';

有几个类:

  1. 定义了两个异常类
  2. TC_ConfigDomain类,定义配置文件中域的类。上层的TC_Config类就包含一个 TC_ConfigDomain类的成员变量,域类里会有子域,感觉实现上面像个广度优先的树,TC_Config类就是root节点。这种用树的结构来实现的例子也要记住。

看几个函数吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
    //tc_config.h
/**
* @brief 解析一个domain
* 对形如"/Main/Domain<Name>"的path进行解析,解析的结果为一个
* DomainPath 类型,包括路径_domains和路径中对应的配置项_param.
*
* @param path 一个经过处理的字符串,必须符合一定的要求
* @param bWithParam "/Main/Domain<Name>"时bWithParam为ture
* "/Main/Domain"时bWithParam为false
* @return DomainPath 一个DomainPath对象,解析出域中的域名和参数
*/
static DomainPath parseDomainName(const string& path, bool bWithParam);

//定义
//tc_config.cpp
TC_ConfigDomain::DomainPath TC_ConfigDomain::parseDomainName(const string& path, bool bWithParam)
{
TC_ConfigDomain::DomainPath dp;

if(bWithParam)
{
string::size_type pos1 = path.find_first_of(TC_CONFIG_PARAM_BEGIN);
if(pos1 == string::npos)
{
throw TC_Config_Exception("[TC_Config::parseDomainName] : param path '" + path + "' is invalid!" );
}

if(path[0] != TC_CONFIG_DOMAIN_SEP)
{
throw TC_Config_Exception("[TC_Config::parseDomainName] : param path '" + path + "' must start with '/'!" );
}

string::size_type pos2 = path.find_first_of(TC_CONFIG_PARAM_END);
if(pos2 == string::npos)
{
throw TC_Config_Exception("[TC_Config::parseDomainName] : param path '" + path + "' is invalid!" );
}

dp._domains = TC_Common::sepstr<string>(path.substr(1, pos1-1), TC_Common::tostr(TC_CONFIG_DOMAIN_SEP));
dp._param = path.substr(pos1 + 1, pos2 - pos1 - 1);
}
else
{
// if(path.length() <= 1 || path[0] != TC_CONFIG_DOMAIN_SEP)
if(path[0] != TC_CONFIG_DOMAIN_SEP)
{
throw TC_Config_Exception("[TC_Config::parseDomainName] : param path '" + path + "' must start with '/'!" );
}

dp._domains = TC_Common::sepstr<string>(path.substr(1), TC_Common::tostr(TC_CONFIG_DOMAIN_SEP));
}

return dp;
}
//之所以要看这个方法,是因为我想了一下,
//这个简答的字符串处理如果换成是我来写,那我肯定是for循环来处理了,
//所以看到这个实现的时候有种恍然大悟的感觉,啥叫优雅的写法,这就是呀……
//但是在setParamValue方法里不知道为啥就用了for循环,感觉代码不是一个人写的呀。

递归搜索子域的方法getSubTcConfigDomain怎么感觉没明白呢?它搜了个寂寞?既然是搜索,但参数里没有个要搜索的目标啊,就给了两个迭代器?好奇怪。后需要再看下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//20200701联合tc_config::operator[]再看,感觉理解了一些,
//TC_ConfigDomain::getSubTcConfigDomain这个方法搜的其实并不是别的,就是`itEnd`对应的*TC_ConfigDomain
const TC_ConfigDomain *TC_ConfigDomain::getSubTcConfigDomain(vector<string>::const_iterator itBegin, vector<string>::const_iterator itEnd) const
{
//递归结束条件,我是理解了上面注释里说的这个方法所要搜索的就是iEnd对应的域之后才看明白了这个递归条件
//但其实我感觉看到递归条件就应该明白要搜索的是啥了,还是太菜了,没看懂一开始。
if(itBegin == itEnd)
{
return this;
}

map<string, TC_ConfigDomain*>::const_iterator it = _subdomain.find(*itBegin);

//根据匹配规则找不到匹配的子域
if(it == _subdomain.end())
{
return NULL;
}

//继续在子域下搜索
return it->second->getSubTcConfigDomain(itBegin + 1, itEnd);
}

tars的配置里配置项如果没写=,看起来是会把整行当做一个key,然后对应值是空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
    /**
* @brief 转换成配置文件的字符串格式.
*
* @param i tab的层数
* @return string类型配置字符串
*/
string tostr(int i) const;

//该方法将整个类转换成配置文件格式的字符串输出,同样适用了递归方法,好好看,好好学
string TC_ConfigDomain::tostr(int i) const
{
string sTab;
for(int k = 0; k < i; ++k)
{
sTab += " ";
}

ostringstream buf;

buf << sTab << "<" << reverse_parse(_name) << ">" << endl;;

for(size_t n = 0; n < _key.size(); n++)
{
map<string, string>::const_iterator it = _param.find(_key[n]);

assert(it != _param.end());

//值为空, 则不打印出=
if(it->second.empty())
{
buf << " " << sTab << reverse_parse(_key[n]) << endl;
}
else
{
buf << " " << sTab << reverse_parse(_key[n]) << "=" << reverse_parse(it->second) << endl;
}
}

++i;

for(size_t n = 0; n < _domain.size(); n++)
{
map<string, TC_ConfigDomain*>::const_iterator itm = _subdomain.find(_domain[n]);

assert(itm != _subdomain.end());

buf << itm->second->tostr(i);
}


buf << sTab << "</" << reverse_parse(_name) << ">" << endl;

return buf.str();
}

tc_config的operator[]方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
string TC_Config::operator[](const string &path) const
{
TC_ConfigDomain::DomainPath dp = TC_ConfigDomain::parseDomainName(path, true);

//这里所搜索的域其实就是dp._domains的最后一个元素对应的域
//和上面的getSubTcConfigDomain结合起来看更好理解
const TC_ConfigDomain *pTcConfigDomain = searchTcConfigDomain(dp._domains);

if(pTcConfigDomain == NULL)
{
throw TC_ConfigNoParam_Exception("[TC_Config::operator[]] path '" + path + "' not exits!");
}

return pTcConfigDomain->getParamValue(dp._param);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
    /**
* @brief 合并配置文件到当前配置文件.
*
* @param cf
* @param bUpdate true-冲突项更新本配置, false-冲突项不更新
*/
void joinConfig(const TC_Config &cf, bool bUpdate);


//上述方法内部实现调用了parse方法,parse方法里会把字符串解析成配置
//parse核心代码,主要靠一个栈实现,这种字符串解析代码看上去还是很优雅呀:
void TC_Config::parse(istream &is)
{
_root.destroy();

stack<TC_ConfigDomain*> stkTcCnfDomain;
stkTcCnfDomain.push(&_root);

string line;
while(getline(is, line))
{
line = TC_Common::trim(line, " \r\n\t");

if(line.length() == 0)
{
continue;
}

if(line[0] == '#')//以#开头的是注释,忽略
{
continue;
}
else if(line[0] == '<')
{
string::size_type posl = line.find_first_of('>');

if(posl == string::npos)
{
throw TC_Config_Exception("[TC_Config::parse]:parse error! line : " + line);
}

if(line[1] == '/')
{
string sName(line.substr(2, (posl - 2)));

if(stkTcCnfDomain.size() <= 0)
{
throw TC_Config_Exception("[TC_Config::parse]:parse error! <" + sName + "> hasn't matched domain.");
}

if(stkTcCnfDomain.top()->getName() != sName)
{
throw TC_Config_Exception("[TC_Config::parse]:parse error! <" + stkTcCnfDomain.top()->getName() + "> hasn't match <" + sName +">.");
}

//弹出
stkTcCnfDomain.pop();
}
else
{
string name(line.substr(1, posl - 1));

stkTcCnfDomain.push(stkTcCnfDomain.top()->addSubDomain(name));
}
}
else
{
stkTcCnfDomain.top()->setParamValue(line);
}
}

if(stkTcCnfDomain.size() != 1)
{
throw TC_Config_Exception("[TC_Config::parse]:parse error : hasn't match");
}
}