参考学习:从零开始实现 C++ TinyWebServer 异步日志系统 Log类详解_tiny webserver-CSDN博客

本文用于加强学习记忆,原文讲解很清晰。

 

日志服务

对于一个服务器而言,需要日志服务来记录调试或者运行的记录,便于查看运行情况。

日志分为两种类型:

  • 同步日志:日志写入函数与工作线程串行执行,由于涉及到I/O操作,当日志比较大的时候,日志模式会阻塞整个处理流程,服务器所能处理的并发能力有所下降,尤其是在峰值的时候,写日志可能成为系统的瓶颈。
  • 异步日志:将所写的日志内容先存入阻塞队列中,写线程从阻塞队列中取出内容,写入日志。

image

 日志运行流程:

  1. 获取日志实例:
    使用单例模式(具体采用局部静态变量的实现方式)来获取Log类的唯一实例,调用方式为Log::GetInstance()。单例模式确保了整个程序运行期间,Log类只有一个实例存在,避免了多个实例可能带来的资源浪费和数据不一致问题。
  2. 初始化日志系统:
    通过获取的Log实例调用init()函数来完成日志系统的初始化工作。在初始化过程中,会根据设置的阻塞队列大小来决定采用同步日志还是异步日志。如果阻塞队列的大小大于0,就会选择异步日志模式;如果等于0,则选择同步日志模式。同时,会更新is_async变量来标记当前的日志模式。
  3. 写入日志:
    当需要记录日志时,通过Log实例调用Write()函数。在写入日志之前,会根据当前的时间信息创建一个新的日志文件,日志文件按的命名规则是:前缀为当前时间,后缀为.log。同时,会更新记录当前的today_变量和记录当前日志行数的line_count变量。
  4. 判断日志写入方式:
    在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, ...)

  • #define:C/C++ 宏定义关键字,用于创建 “文本替换规则”。
  • LOG_BASE:宏的名称,后续可通过这个名称调用。
  • (level, format, ...):宏的参数列表:
    • level:日志等级(0=DEBUG,1=INFO,等)。
    • format:日志格式字符串(如 "fd=%d")。
    • ...:可变参数(格式字符串中 %d%s 对应的实际参数,如 fdpath)。
    • __VA_ARGS__:是 C99 标准的关键字,用于在宏中表示 “所有可变参数”(即 ... 传递的内容)。

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 }
步骤 1:Log* log = Log::GetInstance();
获取 Log 类的单例实例(全局唯一),确保所有日志操作都通过同一个实例进行,避免多实例操作日志文件导致冲突。
步骤 2:if(log->IsOpen() && log->GetLevel() <= level)
  • log->IsOpen():检查日志是否已初始化(Init 被调用且成功),未开启则不输出。
  • log->GetLevel() <= level:日志等级过滤。例如:
    • 若当前设置的日志等级是 2(WARN),则 level=2(WARN)、3(ERROR)、4(FATAL)会通过判断(输出);level=0(DEBUG)、1(INFO)会被过滤(不输出)。
      这就是 “小于等于当前等级才输出” 的逻辑。
步骤 3:log->Write(level, format, ##__VA_ARGS__)
调用 Log 类的 Write 方法,实际写入日志内容:
  • level:传递日志等级(用于在日志中标记 [DEBUG]/[ERROR] 等)。
  • format:日志格式字符串(如 "fd=%d")。
  • ##__VA_ARGS__:传递可变参数(如 fd),## 的作用是当没有可变参数时,自动删除前面的逗号,避免编译错误。
    例如:
    • 有参数时:LOG_DEBUG("fd=%d", fd) → 展开为 log->Write(0, "fd=%d", fd)(正确)。
    • 无参数时:LOG_DEBUG("连接成功") → 展开为 log->Write(0, "连接成功")## 去掉了多余的逗号,否则会变成 log->Write(0, "连接成功", ),导致语法错误)。
步骤 4:log->Flush();
强制刷新日志缓冲区到文件。因为日志通常会先写入内存缓冲区(Buffer buff_),积累到一定量再写入磁盘(减少 IO 次数),Flush() 确保日志立即写入磁盘(避免程序崩溃时丢失缓存中的日志)。
 

等级宏:LOG_DEBUG/LOG_INFO 等的拆解

这些宏是 LOG_BASE 的 “快捷方式”,通过固定 level 参数实现不同等级的日志输出:
 1 // 调试日志(level=0)
 2 #define LOG_DEBUG(format, ...) do {LOG_BASE(0, format, ##__VA_ARGS__)}while(0);
 3 // 信息日志(level=1)
 4 #define LOG_INFO(format, ...) do{LOG_BASE(1, format, ##__VA_ARGS__)}while(0);
 5 // 警告日志(level=2)
 6 #define LOG_WARN(format, ...) do{LOG_BASE(2, format, ##__VA_ARGS__)}while(0);
 7 // 错误日志(level=3)
 8 #define LOG_ERROR(format, ...) do{LOG_BASE(3, format, ##__VA_ARGS__)}while(0);
 9 // 致命错误日志(level=4)
10 #define LOG_FATAL(format, ...) do{LOG_BASE(4, format, ##__VA_ARGS__)}while(0);
  • 本质:将 level 硬编码为 0~4,用户无需记忆数字,直接用 LOG_DEBUG 等语义化名称即可。
  • 外层的 do{...}while(0):和 LOG_BASE 同理,确保这些宏在任何场景下展开都不会破坏语法。