muduo 是一个基于 Reactor 模式的现代 C++ 网络库,作者陈硕。它采用非阻塞 IO 模型,基于事件驱动和回调,原生支持多核多线程,适合编写 Linux 服务端多线程网络应用程序。
muduo网络库的核心代码只有数千行,在网络编程技术学习的进阶阶段,muduo是一个非常值得学习的开源库。目前我也是刚刚开始学习这个网络库的源码,希望将这个学习过程记录下来。这个网络库的源码已经发布在GitHub上,可以点击这里阅读。目前Github上这份源码已经被作者用c++11重写,我学习的版本是没有使用c++11版本的。不过二者大同小异,核心思想是没有变化的。点这里可以看我的源代码,如果你对我之前的博客有兴趣,可以点击下面的连接:
muduo网络库源码复现笔记(一):base库的Timestamp.h
muduo网络库源码复现笔记(二):base库的Atomic.h
muduo网络库源码复现笔记(三):base库的Exception.h
muduo网络库源码复现笔记(四):base库的Thread.h和CurrentThread.h
muduo网络库源码复现笔记(五):base库的Mutex.h和Condition.h和CoutntDownLatch.h
muduo网络库源码复现笔记(六):base库的BlockingQueue.h和BoundedBlockingQueue.h
muduo网络库源码复现笔记(七):base库的ThreadPool.h
muduo网络库源码复现笔记(八):base库的Singleton.h
muduo网络库源码复现笔记(九):base库的ThreadLocalSingleton.h
muduo网络库源码复现笔记(十):base库的ThreadLocalSingleton.h
muduo网络库源码复现笔记(十一):base库的StringPiece.h
muduo网络库源码复现笔记(十二):base库的LogStream.h
muduo网络库源码复现笔记(十三):base库的Logging.h
muduo网络库源码复现笔记(十四):base库的FileUtil.h
muduo网络库源码复现笔记(十五):base库的ProcessInfo.h
LogFile.h头文件中的LogFile类实现的是滚动日志的功能。所谓滚动日志,它具有两个功能:(1)当日志大小达到一定数目时如1G,自动开启一个新日志;(2)当到达每天零点时,开启一个新日志。同时我们对日志的命名也有明确的规范,是按运行程序名称.年-月-日-时-分-秒-微秒.主机名称.log的格式来命名的。下面我们来看看具体的实现过程。
LogFile类能实现滚动日志要借助于File类。Logfile类有个成员boost::scoped_ptr file_(scoped_ptr就是c++11里的unique_ptr智能指针),当我们要写入新日志时只需要new出一个File类初始化它。下面分析一些File的关键函数。
class LogFile::File : boost::noncopyable
{
public:
explicit File(const string& filename)
: fp_(::fopen(filename.data(),"ae")),
writtenBytes_(0)
{
assert(fp_);
::setbuffer(fp_,buffer_,sizeof buffer_);
}
~File()
{
::fclose(fp_);
}
void append(const char* logline,const size_t len)
{
size_t n = write(logline,len);
size_t remain = len - n;
while(remain > 0)
{
size_t x = write(logline+n,remain);
if(x == 0)
{
int err = ferror(fp_);
if(err)
{
fprintf(stderr,"LogFile::FIle::append() failed %s\n",
strerror_tl(err));
}
break;
}
n += x;
remain = len - n;
}
writtenBytes_ += len;
}
void flush()
{
::fflush(fp_);
}
size_t writtenBytes() const {return writtenBytes_;}
private:
size_t write(const char* logline,size_t len)
{
#undef fwrite_unlocked
return ::fwrite_unlocked(logline,1,len,fp_);
}
FILE* fp_;
char buffer_[64*1024];
size_t writtenBytes_;
};
在构造函数中,使用fopen打开文件,用返回的文件指针初始化fp_;writtenBytes_是这个文件已经写入的字节数,初始化为0;在构造函数的逻辑中,将文件缓冲区设置为buffer_,buffer_大小是64k。
析构函数中关闭文件指针即可。
append函数用于向文件写入细节。若写入字节后remain > 0,接着在while中写入。注意append中使用的write函数不是fcntl.h的write函数,而是File类中封装的write函数。这里的write函数封装了fwrite_locked,不使用锁也不判断其他函数是否使用锁,所以不是线程安全的,当然如果有线程安全考虑的话也可以在外部调用它时加锁。
接下来进入正题,看LogFile类如何实现滚动日志。LogFile的私有成员变量中
basename_是指运行程序的basename,rollSize_是指字节数达到多少时开启一个新日志,flushInterval_是flush间隔。startPeriod_是指一个日志开启的当天0点的时刻(距离UTC 1970-1-1-0:00的秒数),lastRoll是上次开启新日志的时刻,lastFlush是上次flush的时刻。这些变量在构造函数中初始化。还有两个常量kCheckTimeRoll_与count_配合检查是否达到第二天,后面在append_unlocked函数中讲。kRollPerSeconds_就是一天的秒数。
lass LogFile : boost::noncopyable
{
public:
LogFile(const string& basename,size_t rollSize,
bool threadSafe = true,int flushInterval = 3);
~LogFile();
void append(const char* logline,int len);
void flush();
private:
void append_unlocked(const char* logline,int len);
static string getLogFileName(const string& basename,time_t* now);
void rollFile();
const string basename_;//basename of logfile;
const size_t rollSize_; //maxsize of a roll logfile
const int flushInterval_; //
int count_;
boost::scoped_ptr mutex_;
time_t startOfPeriod_;
time_t lastRoll_;
time_t lastFlush_;
class File;
boost::scoped_ptr file_;
const static int kCheckTimeRoll_ = 1024;
const static int kRollPerSeconds_ = 60 * 60 * 24;
};
LogFile函数的构造函数如下,计数count_被初试化为0, startOfPeriod_、lastRoll_、lastFlush_同样都是0。初始化完毕,使用rollFile()函数开启一个新日志。
LogFile::LogFile(const string& basename,size_t rollSize,
bool threadSafe,int flushInterval)
: basename_(basename),rollSize_(rollSize),
flushInterval_(flushInterval),
count_(0),
mutex_(threadSafe ? new MutexLock : NULL),
startOfPeriod_(0),
lastRoll_(0),
lastFlush_(0)
{
assert(basename.find('/') == string::npos);
rollFile();
}
在rollFile中,首先使用getLogFileName函数(代码忽略,不难,我的github有)获取日志的规范名称(前面提到过),并且讲now置为现在的时刻。注意start的赋值,它将now进行了取整,得到的结果就是now当天的零点时刻。随后进行赋值与file_指针初始化。rollFile函数除了在构造函数中使用,还在append函数中使用以实现滚动功能
void LogFile::rollFile()
{
time_t now = 0;
string filename = getLogFileName(basename_,&now);
time_t start = now / kRollPerSeconds_ * kRollPerSeconds_;
if(now > lastRoll_)
{
lastRoll_ = now;
lastFlush_ = now;
startOfPeriod_ = start;
file_.reset(new File(filename));
}
}
append函数是公开以被调用向日志写入字节。append函数有两个参数,一个是要写入的内容,一个是内容长度。根据是否线程安全的考虑来决定是否加锁,而真正进行字节写入的函数是私有的append_unlocked;
void LogFile::append(const char* logline,int len)
{
if(mutex_)
{
MutexLockGuard lock(*mutex_);
append_unlocked(logline,len);
}
else
{
append_unlocked(logline,len);
}
}
append_unlocked是一个私有函数,它的函数参数和公有的append一样,这个函数实现的前面提到的日志滚动的效果。如果我们要向日志中写入字节,直接调用file_的append即可。写完之后检查目前日志的字节数是否大于rollSize_,若是,开启新日志,这样实现了滚动日志的第一个功能要求。接下来检查计数器count_是否大于kChekTimeRoll_。只有count_ > kCheckTimeRoll_时,才会检查目前是否到了第二天。检查是否到第二天的方法依旧是将当前时刻取整与startPeriod比较,这样就实现了第二个功能。
void LogFile::append_unlocked(const char* logline,int len)
{
file_ -> append(logline,len);
if(file_->writtenBytes() > rollSize_)
{
rollFile();
}
else
{
if(count_ > kCheckTimeRoll_)
{
count_ = 0;
time_t now = ::time(NULL);
time_t thisPeriod_ = now / kRollPerSeconds_ * kRollPerSeconds_;
if(thisPeriod_ != startOfPeriod_)
{
rollFile();
}
else if(now - lastFlush_ > flushInterval_)
{
lastFlush_ = now;
file_ -> flush();
}
}
else
{
++count_;
}
}
}
我们可以使用Logging定义的宏来实现日志输入。如
boost::scoped_ptr g_logfile;
void outputFunc(const char* msg,int len)
{
g_logfile -> append(msg,len);
}
g_logfile.reset(new muduo::LogFile(::basename(filename),150 * 1024));//日志容量上限150k
muduo::Logger::setOutput(outputFunc);//设定Logger的输出逻辑。
在主函数中可以使用LOG*来向日志输入字节信息了。
LOG_INFO<<"messages";