C++ 仿 muduo 高并发服务器:日志模块
1. 代码总览
cpp
#define INF 0
#define DBG 1
#define ERR 2
#define LOG_LEVEL INF
#define LOG(level, format, ...) do { \
if (level < LOG_LEVEL) break; \
time_t t = time(NULL); \
struct tm* ltm = localtime(&t); \
char tmp[32] = {0}; \
strftime(tmp, 31, "%H:%M:%S", ltm); \
fprintf(stdout, "[%s %s:%d] " format "\n", tmp, __FILE__, __LINE__, ##__VA_ARGS__); \
} while(0)
#define INF_LOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DBG_LOG(format, ...) LOG(DBG, format, ##__VA_ARGS__)
#define ERR_LOG(format, ...) LOG(ERR, format, ##__VA_ARGS__)
int main() {
INF_LOG("s");
return 0;
}
这段代码定义了一套简单的日志宏,支持:
- 日志级别:
INF、DBG、ERR - 可变参数:像
printf一样格式化输出 - 自动附加时间、文件名、行号
- 通过
LOG_LEVEL控制输出级别
2. 为什么需要日志宏?
在服务器开发中,日志是排查问题的重要手段。
但直接写 printf 有几个问题:
- 无法控制级别:调试信息、错误信息混在一起。
- 缺少上下文:不知道日志来自哪个文件、哪一行。
- 代码冗余:每次都要手写时间、文件、行号。
- 性能问题:低级别日志在发布版本中不应该输出。
日志宏的目标:
- 一行代码输出日志。
- 自动带时间、文件、行号。
- 支持级别过滤。
- 支持可变参数格式化。
3. 逐行拆解
3.1 日志级别定义
cpp
#define INF 0
#define DBG 1
#define ERR 2
#define LOG_LEVEL INF
INF:普通信息,级别最低。DBG:调试信息。ERR:错误信息,级别最高。LOG_LEVEL:当前编译期日志级别,只有level >= LOG_LEVEL的日志才会输出。
这里 LOG_LEVEL = INF,表示所有日志都输出。
如果改成 LOG_LEVEL = DBG,则 INF 日志被过滤,只输出 DBG 和 ERR。
3.2 核心宏 LOG
cpp
#define LOG(level, format, ...) do { \
if (level < LOG_LEVEL) break; \
time_t t = time(NULL); \
struct tm* ltm = localtime(&t); \
char tmp[32] = {0}; \
strftime(tmp, 31, "%H:%M:%S", ltm); \
fprintf(stdout, "[%s %s:%d] " format "\n", tmp, __FILE__, __LINE__, ##__VA_ARGS__); \
} while(0)
逐部分解释:
3.2.1 do { ... } while(0)
宏体用 do { ... } while(0) 包裹,是 C/C++ 宏的经典技巧。
好处:
- 保证宏展开后是一个语句块,可以安全地用在
if/else中。 - 避免分号问题。
- 内部可以使用
break提前退出。
例如:
cpp
if (cond)
LOG(INF, "hello");
else
...
如果没有 do...while(0),展开后可能出错。
3.2.2 日志级别过滤
cpp
if (level < LOG_LEVEL) break;
- 如果当前日志级别低于设定级别,直接
break跳出do...while,不输出。 - 因为是编译期常量比较,编译器可能优化掉低级别日志。
3.2.3 获取当前时间
cpp
time_t t = time(NULL);
struct tm* ltm = localtime(&t);
char tmp[32] = {0};
strftime(tmp, 31, "%H:%M:%S", ltm);
time(NULL):获取当前时间戳。localtime(&t):转换为本地时间结构体struct tm。strftime:格式化时间为HH:MM:SS字符串,存入tmp。tmp[32]:足够容纳时间字符串。
注意:localtime 不是线程安全的,多线程环境建议用 localtime_r。
但在简单日志中,通常可以接受。
3.2.4 格式化输出
cpp
fprintf(stdout, "[%s %s:%d] " format "\n", tmp, __FILE__, __LINE__, ##__VA_ARGS__);
stdout:输出到标准输出。"[%s %s:%d] " format "\n":把固定格式和用户传入的format拼接。tmp:时间字符串。__FILE__:当前源文件名,预定义宏。__LINE__:当前行号,预定义宏。##__VA_ARGS__:GNU 扩展,处理可变参数为空的情况,删除前面的逗号。
输出格式类似:
[14:23:45 server.cpp:10] hello
3.3 三个封装宏
cpp
#define INF_LOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DBG_LOG(format, ...) LOG(DBG, format, ##__VA_ARGS__)
#define ERR_LOG(format, ...) LOG(ERR, format, ##__VA_ARGS__)
INF_LOG:输出普通信息。DBG_LOG:输出调试信息。ERR_LOG:输出错误信息。
使用时:
cpp
INF_LOG("server start");
DBG_LOG("client fd = %d", fd);
ERR_LOG("accept failed: %s", strerror(errno));
4. 关键知识点
4.1 可变参数宏
C++11 起支持可变参数宏:
cpp
#define LOG(format, ...) printf(format, ##__VA_ARGS__)
...表示可变参数。__VA_ARGS__在宏展开时替换为实际参数。##__VA_ARGS__是 GNU 扩展,当可变参数为空时删除前面的逗号,避免printf(format, )语法错误。
C++20 提供了 __VA_OPT__,但很多项目仍使用 ##__VA_ARGS__。
4.2 预定义宏
__FILE__:当前源文件名。__LINE__:当前行号。__func__:当前函数名(C++11)。__DATE__、__TIME__:编译日期和时间。
这些宏由编译器自动提供,非常适合日志。
4.3 do...while(0) 技巧
为什么不用 {} 而用 do...while(0)?
{}展开后可能和外部if/else产生悬空 else 问题。do...while(0)是一个完整语句,末尾需要分号,行为更一致。- 内部可以用
break跳出,相当于提前返回。
4.4 日志级别过滤
日志级别数值越大越严重:
INF=0:信息DBG=1:调试ERR=2:错误
LOG_LEVEL 控制最低输出级别。
例如 LOG_LEVEL=DBG,则 INF 不输出,DBG 和 ERR 输出。
这种设计在编译期决定,低级别日志可以被编译器优化掉,减少运行时开销。
5. 使用示例
cpp
#include <cstdio>
#include <ctime>
#include <cstring>
#define INF 0
#define DBG 1
#define ERR 2
#define LOG_LEVEL INF
#define LOG(level, format, ...) do { \
if (level < LOG_LEVEL) break; \
time_t t = time(NULL); \
struct tm* ltm = localtime(&t); \
char tmp[32] = {0}; \
strftime(tmp, 31, "%H:%M:%S", ltm); \
fprintf(stdout, "[%s %s:%d] " format "\n", tmp, __FILE__, __LINE__, ##__VA_ARGS__); \
} while(0)
#define INF_LOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DBG_LOG(format, ...) LOG(DBG, format, ##__VA_ARGS__)
#define ERR_LOG(format, ...) LOG(ERR, format, ##__VA_ARGS__)
int main() {
INF_LOG("server start");
DBG_LOG("client fd = %d", 10);
ERR_LOG("accept failed: %s", strerror(2));
return 0;
}
输出示例:
[14:23:45 server.cpp:20] server start
[14:23:45 server.cpp:21] client fd = 10
[14:23:45 server.cpp:22] accept failed: No such file or directory
6. 总结
这段日志宏虽然简单,但包含了多个 C/C++ 重要知识点:
- 可变参数宏 :
...、__VA_ARGS__、##__VA_ARGS__ - do...while(0):宏定义的安全技巧
- 预定义宏 :
__FILE__、__LINE__ - 时间处理 :
time、localtime、strftime - 格式化输出 :
fprintf - 日志级别:编译期过滤
一句话记忆:
日志宏 = 级别过滤 + 时间/文件/行号 + 可变参数格式化 + do...while(0) 安全包裹。
掌握它,你就理解了服务器开发中最基础的日志模块实现。