【高并发服务器】六、日志宏的实现
文章目录
- 日志宏封装

日志宏封装
目的就是为了实现一些宏函数,让它们辅助我们进行日志信息的打印,我们想要的格式像下面这样子:
[2023/6/21 21:28:30 main.c:28] 文件打开失败
也就是这样子:
[时间 出现错误的文件:行号] 错误信息
对于出现错误的文件和行号来说其实不难,因为 C
语言本身就给我们提供了对应的宏,分别是 __FILE__
和 __LINE__
。
而对于时间来说,这得用几个函数来帮忙:
// 获取系统时间戳
time_t time(NULL); // 通过系统时间戳参数来获取本地时间的结构体tm
struct tm* localtime(time_t* t); // 将结构体tm通过format形式根据max大小存放到buf中去
char* strftime(char* buf, int max, char* format, struct tm* tm); // 通过format格式将可变参数写到文件指针所指的文件中去
int fprintf(FILE* fp, char* format, ...);
我们的日志宏和服务器头文件 server.hpp
放在一起。因为我们要用到宏,那么就得用 <cstdio>
头文件,所以要将其包含进来。
接着我们写一个简单宏定义:
#ifndef __MY_LOG_H__
#define __MY_LOG_H__
#include <cstdio>#define LOG(format, ...) fprintf(stdout, "[%s:%d] " format, __FILE__, __LINE__, __VA_ARGS__)#endif
这里的 LOG
就是一个日志宏,它的第一个参数是一个格式化字符串 format
,用于指定输出信息的格式,注意 format
只是我们在宏定义常常起的参数名,并不是一个关键字或预定义标识符。
LOG
的第二个参数 ...
是 可变参数列表的一种表示,而 fprintf
函数中的 __VA_ARGS__
是 C
语言中的一个预处理器宏,也是用于表示一个可变参数列表,当我们调用 LOG
宏的时候,...
中的多个可变参数都会在预处理阶段传递给 __VA_ARGS__
。
比如说下面的例子:
LOG("%s: %d", "liren", 10);
最后在预处理阶段会被替换为如下形式:
fprintf(stdout, "[%s:%d] %s: %d", __FILE, __LINE, "liren", 10);
至于具体 __VA_ARGS__
和 ...
的区别可以看下面这段理解!
__VA_ARGS__
和...
的区别: 它们用来表示可变参数列表的语法元素,但它们的使用方式和作用范围有所不同。
...
是 C99 标准引入的 语法,用于表示函数或宏定义中的可变参数列表。在函数定义或宏定义中,...
必须放在参数列表的最后一个位置,用来表示后面还有一些可变数量的参数。例如,下面是一个使用...
表示可变参数的函数定义:void my_printf(const char* format, ...);
在函数调用时,可以使用类似于
printf
函数的方式传递可变数量的参数,例如:my_printf("The value of x is %d\n", x); my_printf("Hello, %s!\n", name);
在这种情况下,编译器会将可变参数列表转换为一个类型为
va_list
的对象,然后可以使用stdarg.h
中定义的函数和宏如va_start()
和vsnprintf()
等来访问和处理这些参数。
__VA_ARGS__
则是一个预处理器宏,用于表示宏定义中的可变参数列表。在宏定义中,__VA_ARGS__
可以出现在参数列表的任意位置,用来表示可变数量的参数。例如,下面是一个使用__VA_ARGS__
表示可变参数的宏定义:#define LOG(format, ...) printf(format, __VA_ARGS__)
在这种情况下,预处理器会将
__VA_ARGS__
展开为一系列逗号分隔的参数,然后将它们传递给宏定义中的printf
函数进行输出。但是一般防止不传可变列表参数报错,我们会 在__VA_ARGS__
前面加上##
表示展开后的可变参数列表,如下所示:#define LOG(format, ...) printf(format, ##__VA_ARGS__)
并且 上面的
printf
中使用的时候是不能用...
的,只能使用__VA_ARGS__
来表示接收到的可变参数列表!
总的来说,
...
和__VA_ARGS__
都是用来表示可变参数列表的语法元素,但是 前者用于函数定义和函数调用中,后者只能用于宏定义中。它们的作用和使用方式有所不同,但都可以方便地处理可变数量的参数。
接下来我们再来加入时间等信息,让宏日志更完善一点!一般我们如果想要在宏定义的时候写多行代码,都会使用 do while(0)
语句来配合,涉及到换行的话要使用反斜杠 \
在语句最后面,下面给出结合打印时间的代码的完善日志宏:
#include <cstdio>
#include <time.h>#define LOG(format, ...) do{\char timebuffer[128];\time_t timestamp = time(NULL);\struct tm* timeinfo = localtime(×tamp);\strftime(timebuffer, sizeof(timebuffer), "%Y-%m-%d %H:%M:%S", timeinfo);\fprintf(stdout, "[%s %s:%d] " format, timebuffer, __FILE__, __LINE__, __VA_ARGS__);\
}while(0)
调用的结果如下:
LOG("%s-%d\n", "liren", 100);
结果:
[2023-06-21 23:17:03 gobang.cc:5] liren-100
这样子就结束了吗❓❓❓
当然不是,因为还有 bug
,因为如果我们 使用 LOG
宏的时候不传可变参数的话,那么预处理时候就会报错,如下所示:
LOG("liren");
编译时候会报错:[liren@VM-8-7-centos source]$ make
g++ -ogobang gobang.cc logger.hpp
In file included from gobang.cc:1:0:
gobang.cc: In function ‘int main()’:
logger.hpp:11:86: error: expected primary-expression before ‘)’ tokenintf(stdout, "[%s %s:%d] " format, timebuffer, __FILE__, __LINE__, __VA_ARGS__);\^
gobang.cc:5:5: note: in expansion of macro ‘LOG’LOG("liren");^~~
make: *** [makefile:2: gobang] Error 1
解决这个问题很简单,只需要使用 ##__VA_ARGS__
来表示展开后的参数列表。这个语法中的 ##
表示将 __VA_ARGS__
前面的逗号去掉,避免在展开后出现语法错误!需要注意的是,##
的使用在不同的编译器和平台上可能有所不同。在使用 ##
时需要注意平台兼容性和语法规则。
所以修改完代码如下:
#include <cstdio>
#include <time.h>#define LOG(format, ...) do{\char timebuffer[128] = {0};\time_t timestamp = time(NULL);\struct tm* timeinfo = localtime(×tamp);\strftime(timebuffer, sizeof(timebuffer), "%Y-%m-%d %H:%M:%S", timeinfo);\fprintf(stdout, "[%s %s:%d] " format "\n", timebuffer, __FILE__, __LINE__, ##__VA_ARGS__);\
}while(0)
这样子就结束了吗❓❓❓还是没结束,因为我们到时候项目中会打印很多日志,如果我们不对日志分等级的话,那么可能会导致日志比较乱,下面我们定义一些等级的宏:
#define INF 0 // 提示型等级
#define DEBUG 1 // 调试型等级
#define ERROR 2 // 错误型等级
#define DEFAULT_LOG_LEVEL DEBUG // 默认的日志等级
然后我们再将这些等级宏和我们刚才写的日志宏封装起来:
#define ILOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DLOG(format, ...) LOG(DEBUG, format, ##__VA_ARGS__)
#define ELOG(format, ...) LOG(ERROR, format, ##__VA_ARGS__)
此时看到 LOG
宏的第一个参数传入的是对应的等级,那我们想要去改一下原来的日志宏的参数,多加一个参数 level
,并且我们判断一下当前的等级也就是 DEFAULT_LOG_LEVEL
是否小于传入进来的等级,是的话我们就不需要去打印,因为我们此时程序说明不需要上升到这种级别的日志打印!
#define LOG(level, format, ...) do{\if(DEFAULT_LOG_LEVEL > level) break\char timebuffer[128] = {0};\time_t timestamp = time(NULL);\struct tm* timeinfo = localtime(×tamp);\strftime(timebuffer, sizeof(timebuffer), "%Y-%m-%d %H:%M:%S", timeinfo);\fprintf(stdout, "[%s %s:%d] " format "\n", timebuffer, __FILE__, __LINE__, ##__VA_ARGS__);\
}while(0)
所以完整的代码是这样子的:
#include <cstdio>
#include <time.h>#define INF 0 // 提示型等级
#define DEBUG 1 // 调试型等级
#define ERROR 2 // 错误型等级
#define DEFAULT_LOG_LEVEL DEBUG // 默认的日志等级#define LOG(level, format, ...) do{\if(DEFAULT_LOG_LEVEL < level) break;\char timebuffer[128] = {0};\time_t timestamp = time(NULL);\struct tm* timeinfo = localtime(×tamp);\strftime(timebuffer, sizeof(timebuffer), "%Y-%m-%d %H:%M:%S", timeinfo);\fprintf(stdout, "[%s %s:%d] " format "\n", timebuffer, __FILE__, __LINE__, ##__VA_ARGS__);\
}while(0)// 将等级和日志打印封装起来
#define ILOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DLOG(format, ...) LOG(DEBUG, format, ##__VA_ARGS__)
#define ELOG(format, ...) LOG(ERROR, format, ##__VA_ARGS__)
下面我们测试一下:
ILOG("this is INF");
DLOG("this is DEBUG");
ELOG("this is ERROR");// 运行结果:
[liren@VM-8-7-centos source]$ ./gobang
[2023-06-21 23:41:55 gobang.cc:5] this is INF
[2023-06-21 23:41:55 gobang.cc:6] this is DEBUG