截止Tars-C++ 揭秘篇:链接管理,我们已经解析了RPC的大部分代码(promise部分以后单独再说)。可能有同学说没还看到epoll模型和协程部分的介绍啊,其实这部分在TarsRPC源码解读篇:使用C++重写Tars-RPC主逻辑框架中的第一节和第三节有所提及,所以剩下的的可能只有日志部分了
Tars的日志部分是自己实现的,翻看了代码,额,怎么说,日志部分的代码对初学者有一点点不友好。
- 日志部分代码不够“单纯”,为了支持远程写日志,内嵌了一些“额外”的代码。这样不能直接把这部分代码拿出来在别的项目使用(为了方便一些同学想单独调试日志部分代码,笔者把那些“额外代码”去掉了,做了一个可以单独运行的模块,你可以在这里获取代码:Tars-log demo)
- 日志部分的类继承关系要复杂一些,且涉及到std::basic_streambuf< char >的自定义实现,一不小心掉坑里,可能就爬不出来了
本着“死贫道,不死道友”的精神,笔者对日志类进行了简要梳理,希望对想要了解Tars日志类的同学有些帮助
13.1 找准入口
如果想自己先梳理逻辑,可以按照下面的代码顺序进行
// setLogInfo中,初始化了线程池,放入具体任务,循环等待输入内容
TarsRollLogger::getInstance()->setLogInfo(ServerConfig::Application, ServerConfig::ServerName, ServerConfig::LogPath, ServerConfig::LogSize, ServerConfig::LogNum, _communicator, ServerConfig::Log);
//设置日志等级
TarsRollLogger::getInstance()->logger()->setLogLevel("DEBUG");
//放入具体内容, setLogInfo中等待的线程写入文件
TarsRollLogger::getInstance()->logger()->debug() << "[TARS]" << "HelloWorld" << endl;
13.2 逻辑梳理
13.2Tars日志.jpg
这张图画的的确有点乱。。。请担待
- 日志的入口类为TarsRollLogger,里面有两个最主要的类TC_LoggerThreadGroup和TC_Logger< RollWriteT, TC_RollBySize >,分别位于图中左上角和最右边
- TC_LoggerThreadGroup中有一个TC_ThreadPool的线程池,线程池里有一个ThreadWorker线程从_jobqueue中等待任务。
- TC_LoggerThreadGroup::_LoggerThreadGroup::run中,一直在循环执行flush动作。flush在遍历logger_set,每次遍历会调用TC_LoggerRoll::flush(),将TC_ThreadQueue<pair<int, string> > _buffer中的内容写入文件。由此可以看出,弄清Tars日志工作机理,一是要追溯logger_set中的内容是从哪儿来的;二是_buffer中的内容是从哪儿来的,_buffer是logger_set的成员变量,所以先来看logger_set
- logger_set的线索是蓝色框。我们已经对蓝色框进行了编号,1号位于TC_Logger< RollWriteT, TC_RollBySize >类中(右上角)。
- 先看下TC_Logger的继承关系,TC_Logger继承:RollWrapperI,:RollWrapperI继承RollWrapperBase,1号中的setupThread具体实现就是在RollWrapperBase中,注意这里setupThread是通过调用2号蓝框中的_roll->setupThread实现的。而_roll是TC_RollBySize类(见绿色框),TC_RollBySize类继承TC_LoggerRoll,所以_roll->setupThread具体实现是在3号蓝色框中,即TC_LoggerRoll类中。
- 在3号蓝色框中可以比较清晰的看到是怎样调用4号TC_LoggerThreadGroup::registerLogger方法,将TC_LoggerRoll放入logger_set中的
- 继续看_buffer(TC_ThreadQueue),它的线索是黄色框。核心思想是重要的事儿是析构函数干的。
- TC_Logger< RollWriteT, TC_RollBySize >在构造函数里偷偷干了两件事儿,一是将_roll(绿色框)作为变量传入LoggerBuffer中(1号黄色框),二是将LoggerBuffer变量传入了std::ostream _stream(2号黄色框)
- 在进行“TarsRollLogger::getInstance()->logger()->debug()”的调用时,会调用3号黄色框LoggerStream stream(int level)方法,LoggerStream是一个临时类,调用完debug函数会析构,析构时候调用4号黄色框中stream->flush,而stream->flush将调用LoggerBuffer的sync(5号黄色框)将内容写入到_buffer(TC_ThreadQueue)中
贴一段验证代码,有助于理解5号黄色框__stream->flush调用LoggerBuffer的sync的逻辑
#include <iostream>
#include <string>
using namespace std;
class LoggerBuffer : public std::basic_streambuf<char>
{
public:
LoggerBuffer(int num){cout<<"LoggerBuffer init"<<endl;}
~LoggerBuffer(){cout<<"LoggerBuffer xigou"<<endl;}
virtual int sync(){cout<<"sync"<<endl;}
};
class TC_Logger
{
public:
TC_Logger()
:_buffer(1)
, _stream(&_buffer)
{}
~TC_Logger(){cout<<"TC_Logger xigou"<<endl;}
void stream(int level)
{
ostream *ost = NULL;
ost = &_stream;
//调用flush后会调用LoggerBuffer中的sync方法
ost->flush();
}
LoggerBuffer _buffer;
std::ostream _stream;
};
int main()
{
TC_Logger logger;
logger.stream(1);
cout<<"main fun end"<<endl;
return 0;
}
g++ main.cpp
执行./a.out,会看到执行过程
下面这张stream关系图,对理解上面LoggerBuffer变量和ostream之间关系有些帮助
13.3stream.jpg
13.3 小结
- 线程池部分逻辑很清晰,接收任务,循环等待,常规套路
- LoggerBuffer继承了std::basic_streambuf< char >,重写了sync方法。ostream类型变量在调用flush时候,会调用重写的sync方法
- Tars日志不仅支持按大小写入(TC_RollBySize),貌似也支持按时间写入(TC_RollByTime),没再仔细研究,感兴趣的小伙伴可自行探索