参考学习:从零开始实现 C++ TinyWebServer 异步日志系统 Log类详解_tiny webserver-CSDN博客
本文用于加强学习记忆,原文讲解很清晰。
日志服务
对于一个服务器而言,需要日志服务来记录调试或者运行的记录,便于查看运行情况。
日志分为两种类型:
- 同步日志:日志写入函数与工作线程串行执行,由于涉及到I/O操作,当日志比较大的时候,日志模式会阻塞整个处理流程,服务器所能处理的并发能力有所下降,尤其是在峰值的时候,写日志可能成为系统的瓶颈。
- 异步日志:将所写的日志内容先存入阻塞队列中,写线程从阻塞队列中取出内容,写入日志。

日志运行流程:
- 获取日志实例:
使用单例模式(具体采用局部静态变量的实现方式)来获取Log类的唯一实例,调用方式为Log::GetInstance()。单例模式确保了整个程序运行期间,Log类只有一个实例存在,避免了多个实例可能带来的资源浪费和数据不一致问题。 - 初始化日志系统:
通过获取的Log实例调用init()函数来完成日志系统的初始化工作。在初始化过程中,会根据设置的阻塞队列大小来决定采用同步日志还是异步日志。如果阻塞队列的大小大于0,就会选择异步日志模式;如果等于0,则选择同步日志模式。同时,会更新is_async变量来标记当前的日志模式。 - 写入日志:
当需要记录日志时,通过Log实例调用Write()函数。在写入日志之前,会根据当前的时间信息创建一个新的日志文件,日志文件按的命名规则是:前缀为当前时间,后缀为.log。同时,会更新记录当前的today_变量和记录当前日志行数的line_count变量。 - 判断日志写入方式:
在Write()函数内部,会根据is_async变量的值来决定具体的日志写入方式。如果is_async为true,表示当前处于异步日志模式,工作线程会将需要写入的日志内容放入阻塞队列中,然后由专门的写线程从阻塞队列中取出数据并写入日志文件;如果is_async为false,则表示处于同步日志模式,日志内容会直接写入到日志文件中。
Log类
私有成员:
1 private: 2 // 单例模式:构造和析构私有,禁止外部创建/销毁实例 3 Log(); 4 ~Log(); 5 6 // 向缓冲区添加日志等级字符串(如 "[DEBUG]:") 7 void AppendLogLevel(int level); 8 9 // 异步日志的实际写操作(由写线程调用) 10 void AsyncWrite(); 11 12 // 常量:日志文件名最大长度、单个文件最大行数 13 static const int Log_NAME_LENGTH = 256; // 最长文件名 14 static const int MAX_LINES = 50000; //最长日志条数 15 16 // 日志文件路径和后缀(如 path="./log", suffix=".log" → 生成 ./log/2024_07_25.log) 17 const char* path_; //路径名 18 const char* suffix_; //后缀名 19 20 // 状态标记:是否开启日志、当前日志等级、是否异步模式 21 bool is_open_; //是否开启 22 int level_; //日志等级 23 bool is_async_; //是否开启异步日志 24 25 // 日志文件管理:当天日期(用于按日切分文件)、当前文件行数(用于按行数切分) 26 int today_; //当天日期 27 int line_count_; //日志行数 28 29 30 // 缓冲区(减少IO次数,提高性能)、文件指针(操作日志文件) 31 Buffer buff_; //输出缓冲区 32 FILE* fp_; //文件指针 33 34 35 // 线程安全:互斥锁(保护共享资源) 36 std::mutex mtx_; 37 38 // 异步日志:阻塞队列(存放待写日志)、写线程(从队列取日志写入文件) 39 std::unique_ptr<BlockQueue<string>> deque_; 40 std::unique_ptr<thread> write_thread_;
公共成员:对外提供的功能接口
1 public: 2 // 初始化日志:设置等级、路径、后缀、队列容量(异步用) 3 void Init(int level, const char* path = "./log", 4 const char* suffix = ".log", 5 int max_capacity = 1024); 6 7 // 单例模式:获取唯一实例 8 static Log* GetInstance(); 9 10 // 异步日志的刷新线程函数(静态函数,适配线程创建) 11 static void FlushLogThread(); 12 13 // 刷新日志到文件 14 void Flush(); 15 16 // 写日志(核心函数,处理可变参数) 17 void Write(int level, const char* format, ...); 18 19 // 获取/设置日志等级 20 int GetLevel(); 21 void SetLevel(int level); 22 23 // 检查日志是否开启 24 bool IsOpen();
日志宏定义:简化调用
1 //日志宏定义(简化调用) 2 //小于等于当前level才输出 3 // 核心宏:根据日志等级输出,仅当等级满足条件时才写日志 4 #define LOG_BASE(level, format, ...) \ 5 do { \ 6 Log* log = Log::GetInstance(); \ 7 if(log->IsOpen() && log->GetLevel() <= level){\ 8 log->Write(level, format, ##__VA_ARGS__); \ 9 log->Flush(); \ 10 } \ 11 }while(0); 12 13 // 不同等级的日志宏,对应 level 0-4 14 #define LOG_DEBUG(format, ...) do {LOG_BASE(0, format, ##__VA_ARGS__)}while(0); 15 #define LOG_INFO(format, ...) do{LOG_BASE(1, format, ##__VA_ARGS__)}while(0); 16 #define LOG_WARN(format, ...) do{LOG_BASE(2, format, ##__VA_ARGS__)}while(0); 17 #define LOG_ERROR(format, ...) do{LOG_BASE(3, format, ##__VA_ARGS__)}while(0); 18 #define LOG_FATAL(format, ...) do{LOG_BASE(4, format, ##__VA_ARGS__)}while(0); 19 20 #endif //Log_H
1. #define LOG_BASE(level, format, ...)
2. 换行符 \
3. do { ... } while(0) 的作用
4. 宏内部逻辑拆解
1 Log* log = Log::GetInstance(); // 步骤1:获取日志单例实例 2 if(log->IsOpen() && log->GetLevel() <= level){ // 步骤2:条件判断 3 log->Write(level, format, ##__VA_ARGS__); // 步骤3:写入日志 4 log->Flush(); // 步骤4:刷新缓存到文件 5 }