C++ 仿 muduo 高并发服务器:日志模块

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;
}

这段代码定义了一套简单的日志宏,支持:

  • 日志级别:INFDBGERR
  • 可变参数:像 printf 一样格式化输出
  • 自动附加时间、文件名、行号
  • 通过 LOG_LEVEL 控制输出级别

2. 为什么需要日志宏?

在服务器开发中,日志是排查问题的重要手段。

但直接写 printf 有几个问题:

  1. 无法控制级别:调试信息、错误信息混在一起。
  2. 缺少上下文:不知道日志来自哪个文件、哪一行。
  3. 代码冗余:每次都要手写时间、文件、行号。
  4. 性能问题:低级别日志在发布版本中不应该输出。

日志宏的目标:

  • 一行代码输出日志。
  • 自动带时间、文件、行号。
  • 支持级别过滤。
  • 支持可变参数格式化。

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 日志被过滤,只输出 DBGERR

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 不输出,DBGERR 输出。

这种设计在编译期决定,低级别日志可以被编译器优化掉,减少运行时开销。


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__
  • 时间处理timelocaltimestrftime
  • 格式化输出fprintf
  • 日志级别:编译期过滤

一句话记忆:

日志宏 = 级别过滤 + 时间/文件/行号 + 可变参数格式化 + do...while(0) 安全包裹。

掌握它,你就理解了服务器开发中最基础的日志模块实现。

相关推荐
Linux-lucky2 小时前
39-41-Linux学习之旅之redis缓存基础与NFS基础
linux·运维·mysql·ubuntu
whyutianict_vv3 小时前
云计算运维培训课程技术栈拆解:从 Linux 网络到 AI 推理的五层能力分层
linux·云计算
狂师3 小时前
cloudflared,不需要服务器和公网 IP 的免费内网穿透,一条命令上手
服务器·程序员·开源
Wang's Blog3 小时前
Java 项目实战: 外卖平台-启用禁用员工账号与Long精度丢失修复
java·服务器
xiaoye-duck3 小时前
《Linux 网络编程》深入理解数据链路层:从MAC帧到ARP协议的底层原理详解
linux·网络
渡我白衣4 小时前
HttpRequest与HttpResponse的实现
服务器·数据结构·c++·人工智能·tcp/ip·机器学习·caffe
刃神太酷啦4 小时前
前端入门第一课:HTML 基础语法 + 常用标签 + 实战全解
服务器·c语言·前端·javascript·css·c++·html
j7~4 小时前
【Linux 网络】四十.《网络基础(传输层协议:UDP、TCP)》--中篇
linux·tcp·tcp协议·确认应答机制·连接管理机制·tcp流量控制
甜到心里的蛋糕4 小时前
如何用企业微信实现让微信收到通知实时通知
服务器·python·微信小程序·fastapi