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
Logging.h封装了类Logger。如笔记十二所述,封装LogStream类之后我们可以将待输出的信息缓存到LogStream的缓冲区内,而LogStream是由Logger调用的,最终由Logger将信息输出到标准输出或文件中。看一下Logger的代码:
class Logger
{
public:
enum LogLevel
{
TRACE,
DEBUG,
INFO,
WARN,
ERROR,
FATAL,
NUM_LOG_LEVELS,
};
class SourceFile
{
public:
template
inline SourceFile(const char (&arr)[N])
: data_(arr),
size_(N-1)
{
const char* slash = strrchr(data_,'/');
if(slash)
{
data_ = slash + 1;
size_ -= static_cast(data_ - arr);
}
}
explicit SourceFile(const char* filename)
: data_(filename)
{
const char* slash = strrchr(data_,'/');
if(slash)
{
data_ = slash + 1;
}
size_ = static_cast(strlen(data_));
}
const char* data_;
int size_;
};
Logger(SourceFile file,int line);
Logger(SourceFile file,int line,LogLevel level);
Logger(SourceFile file,int line,LogLevel level,const char* func);
Logger(SourceFile file,int linei,bool toAbort);
~Logger();
LogStream& stream() {return impl_.stream_;}
static LogLevel logLevel();
static void setLogLevel(LogLevel level);
typedef void (*OutputFunc)(const char* msg,int len);
typedef void (*FlushFunc)();
static void setOutput(OutputFunc);
static void setFlush(FlushFunc);
private:
class Impl
{
public:
typedef Logger::LogLevel LogLevel;
Impl(LogLevel level,int old_errno,const SourceFile& file,int line);
void formatTime();
void finish();
Timestamp time_;
LogStream stream_;
LogLevel level_;
int line_;
SourceFile basename_;
};
Impl impl_;
};
在Logger内部,封装了两个类Source和Impl,还有enum LogLevel与若干构造函数、成员函数。下面详细说明一下。
SourceFile类用于在输出信息的时候指明是在具体的出错文件。这个类不难,是一个简单的字符封装。它的私有成员有data_和size_。在Source的构造函数中,data_会被出错文件的路径初始化,然后经过strrchr处理,指向出错文件的basename(如/muduo/base/Thread.cc到Thread.cc)。size_就是basename的长度。
muduo使用枚举的方式指出了输出错误信息的形式。其中TRACE,DEBUG用于调试,INFO,WARN,ERROR,FATAL在程序运行过程中输出,警告的程度依次上升,出现FATAL时程序将强制退出。NUM_LOG_LEVELS是这个枚举结构体中错误形式的数目,为6。
Impl类由Logger调用,输出出错信息。它有若干成员,下面依次分析。
formatTime函数的作用是将出错的时间格式化问年-月-日-时-分-秒-毫秒的形式,并将格式化的时间字符串输入到stream_的缓冲区中。主要是利用了gmtime_r函数。
void Logger::Impl::formatTime()
{
int64_t microSecondsSinceEpoch = time_.microSecondsSinceEpoch();
time_t seconds = static_cast(microSecondsSinceEpoch / 1000000);
int microseconds = static_cast(microSecondsSinceEpoch % 1000000);
if(seconds != t_lastSecond)
{
t_lastSecond = seconds;
struct tm tm_time;
::gmtime_r(&seconds,&tm_time);
int len = snprintf(t_time,sizeof t_time,"%4d%02d%02d %02d:%02d:%02d",
tm_time.tm_year+1900,tm_time.tm_mon+1,tm_time.tm_mday,tm_time.tm_hour,
tm_time.tm_min,tm_time.tm_sec);
assert(len == 17);
(void)len;
}
Fmt us(".%06dZ ",microseconds);
assert(us.length() == 9);
stream_ << T(t_time,17) << T(us.data(),9);
}
构造函数代码如下:构造函数中,我们用现在的时间初始化time_,line_是出错处的文件行号,basename就是出错文件的文件名。初始化接触后,执行formatTime函数,将当前线程号和LogLevel输入stream_的缓冲区。saveErrno我们一般传入errno(录系统的最后一次错误代码),若不等于0,将它错误原因输入到缓冲区。
Logger::Impl::Impl(LogLevel level,int savedErrno,const SourceFile& file,int line)
: time_(Timestamp::now()),
stream_(),
level_(level),
line_(line),
basename_(file)
{
formatTime();
CurrentThread::tid();
stream_ << T(CurrentThread::tidString(),6);
stream_ << T(LogLevelName[level],6);
if(savedErrno != 0)
{
stream_ << strerror_tl(savedErrno) << "(errno=" << savedErrno << ")";
}
}
Logger构造函数大同小异,基本都是用参数来初始化impl_,如
Logger::Logger(SourceFile file,int line,LogLevel level,const char* func)
: impl_(level,0,file,line)
{
impl_.stream_ << func << ' ';
}
Logger的析构函数代码如下:在一个Logger的生命周期结束之时,析构函数先调用finish函数将出错文件的行号与文件号加载到stream_中,然后使用g_output将缓冲区中的内容输出到标准输出或文件中,g_output默认使用fwrite将错误打印到标准输出。如果loglevel是FATAL,就要退出程序。
Logger::~Logger()
{
impl_.finish();
const LogStream::Buffer& buf(stream().buffer());
g_output(buf.data(),buf.length());
if(impl_.level_ == FATAL)
{
g_flush();
abort();
}
}
在Logger类中定义了一系列宏来实现错误输出。我们使用Logger来输出错误信息时,只需调用这些宏即可。举例来说,当我们使用LOG_INFO << "info…"时,相当于用__FILE__ (文件的完整路径和文件名)与 LINE(当前行号的整数 )初始化了一个Logger类,出错时间、线程、文件、行号初始化号,接着将"infor"输入到缓冲区,在Logger生命周期结束时调用析构函数输出错误信息。
#define LOG_TRACE if(muduo::Logger::logLevel() <= muduo::Logger::TRACE) \
muduo::Logger(__FILE__,__LINE__,muduo::Logger::TRACE,__func__).stream()
#define LOG_DEBUG if(muduo::Logger::logLevel() <= muduo::Logger::DEBUG) \
muduo::Logger(__FILE__,__LINE__,muduo::Logger::DEBUG,__func__).stream()
#define LOG_INFO if(muduo::Logger::logLevel() <= muduo::Logger::INFO) \
muduo::Logger(__FILE__,__LINE__).stream()
#define LOG_WARN muduo::Logger(__FILE__,__LINE__,muduo::Logger::WARN).stream()
#define LOG_ERROR muduo::Logger(__FILE__,__LINE__,muduo::Logger::ERROR).stream()
#define LOG_FATAL muduo::Logger(__FILE__,__LINE__,muduo::Logger::FATAL).stream()
#define LOG_SYSERR muduo::Logger(__FILE__,__LINE__,false).stream()
#define LOG_SYSFATAL muduo::Logger(__FILE__,__LINE__,true).stream()