TinywebServer代码详解– 日志系统-下(10)
该blog内容转自:最新版Web服务器项目详解 - 10 日志系统(下)
该blog对上述内容进行补充(在本人的角度)
结合此前记录的blog一起学习:牛客WebServer项目实战(点击跳转)
结合此前记录的blog一起学习:多线程与线程同步(点击跳转)
原项目地址(点击跳转)
博主添加注释后项目地址(点击跳转)
一、基础知识
1.本文内容
日志系统分为两部分,其一是单例模式与阻塞队列的定义,其二是日志类的定义与使用。
本篇将介绍日志类的定义与使用,具体的涉及到基础API,流程图与日志类定义,功能实现。
基础API,描述fputs
,可变参数宏__VA_ARGS__,fflush
流程图与日志类定义,描述日志系统整体运行流程,介绍日志类的具体定义
功能实现,结合代码分析同步、异步写文件逻辑,分析超行、按天分文件和日志分级的具体实现
2.基础API
(1)fputs
fputs
函数的原型定义在 <cstdio>
(C++ 中)或 <stdio.h>
(C 中)头文件中
1
| int fputs(const char *str, FILE *stream);
|
参数:
const char *str
:指向一个以空字符终止的字符串的指针,该字符串将被写入
FILE *stream
:指向 FILE
对象的指针,这个 FILE
对象代表一个已打开的文件流,该字符串将被写入到这个文件流中
返回值:
- 成功:返回一个非负值。
- 失败:返回
EOF
,表示发生了一个错误
(2)可变参数宏__VA_ARGS__
在 C 和 C++ 中,__VA_ARGS__
是一个预处理宏,它允许宏定义接收和使用可变数量的参数。__VA_ARGS__
使得宏可以灵活处理不定数量的参数,非常适合用于编写如日志、错误处理等需要接受多个参数的功能
使用 __VA_ARGS__
的宏定义通常遵循以下格式:
1
| #define 宏名(参数列表, ...) 替换列表
|
其中,...
表示可变部分的开始,__VA_ARGS__
用于在宏的替换部分引用所有的可变参数
下面是一个使用 __VA_ARGS__
的示例,定义一个简单的宏,用于打印信息到标准输出:
1 2 3 4 5 6 7 8 9 10
| #include <stdio.h>
#define PRINTF(fmt, ...) printf(fmt, __VA_ARGS__)
int main() { PRINTF("Hello, world!\n"); PRINTF("Number: %d\n", 42); PRINTF("Float and integer: %.2f and %d\n", 3.14159, 7); return 0; }
|
宏 PRINTF
被定义为接受一个格式化字符串和任意数量的后续参数,它将这些参数传递给 printf
函数
可以看到 PRINTF
在没有可变参数时(如 “Hello, world!\n”)和有一个或多个可变参数时(如 “%d” 和 “%.2f and %d”)都能正常工作
__VA_ARGS__宏前面加上##
##
操作符通常用于连接两个令牌,但在与__VA_ARGS__
结合使用时,它的行为稍有不同。当使用##__VA_ARGS__
时,如果__VA_ARGS__
是空的,那么##
操作符将会消除它前面的逗号,避免语法错误。这在创建接受可选参数的宏时非常有用
实例:
1 2 3 4 5 6 7 8 9 10
| #include <stdio.h>
#define PRINTF(fmt, ...) printf(fmt, ##__VA_ARGS__)
int main() { PRINTF("Hello, world!\n"); PRINTF("Number: %d\n", 42); PRINTF("Float and integer: %.2f and %d\n", 3.14159, 7); return 0; }
|
如果调用PRINTF
时没有提供除fmt
以外的其他参数(如"Hello, world!\n"
),##
操作符会移除fmt
后面的逗号,使得调用变为printf("Hello, world!\n")
如果提供了额外的参数,##
操作符不会影响它们的使用
(3)fflush
fflush
函数定义在 <stdio.h>
(C)或 <cstdio>
(C++)头文件中,主要用途是将缓冲区内的数据强制写入与流相关联的文件中,或者刷新(清空)输出缓冲区
1
| int fflush(FILE *stream);
|
参数:
- **
stream
**:指向 FILE
对象的指针,该 FILE
对象表示一个打开的文件流。如果 stream
是 NULL
,则刷新所有输出流的缓冲区
返回值:
- 成功:如果刷新成功,返回
0
- 错误:如果发生错误,返回
EOF
(通常是 -1
),并设置错误指示器
在使用多个输出函数连续进行多次输出到控制台时,有可能下一个数据再上一个数据还没输出完毕,还在输出缓冲区中时,下一个printf
就把另一个数据加入输出缓冲区,结果冲掉了原来的数据,出现输出错误
在prinf()
后加上fflush(stdout)
; 强制马上输出到控制台,可以避免出现上述错误
二、流程图与日志类定义
1.流程图
日志文件
- 局部变量的懒汉模式获取实例
- 生成日志文件,并判断同步和异步写入方式
同步
- 判断是否分文件
- 直接格式化输出内容,将信息写入日志文件
异步
- 判断是否分文件
- 格式化输出内容,将内容写入阻塞队列,创建一个写线程,从阻塞队列取出内容写入日志文件

2.日志类定义
通过局部变量的懒汉单例模式创建日志实例,对其进行初始化生成日志文件后,格式化输出内容,并根据不同的写入方式,完成对应逻辑,写入日志文件
log/log.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| #pragma once #include <stdio.h> #include <iostream> #include <string> #include <stdarg.h> #include <pthread.h> #include "block_queue.h"
using namespace std;
class Log { public: static Log *get_instance(); static void *flush_log_thread(void *args);
bool init(const char *file_name, int close_log, int log_buf_size = 8192, int split_lines = 5000000, int max_queue_size = 0); void write_log(int level, const char *format, ...); void flush(void);
private: Log(); virtual ~Log(); void *async_write_log();
private: char dir_name[128]; char log_name[128]; int m_split_lines; int m_log_buf_size; long long m_count; int m_today; FILE *m_fp; char *m_buf; block_queue<string> *m_log_queue; bool m_is_async; locker m_mutex; int m_close_log; };
#define LOG_DEBUG(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(0, format, ##__VA_ARGS__); Log::get_instance()->flush();} #define LOG_INFO(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(1, format, ##__VA_ARGS__); Log::get_instance()->flush();} #define LOG_WARN(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(2, format, ##__VA_ARGS__); Log::get_instance()->flush();} #define LOG_ERROR(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(3, format, ##__VA_ARGS__); Log::get_instance()->flush();}
|
日志类中的方法都不会被其他程序直接调用,末尾的四个可变参数宏提供了其他程序的调用方法
前述方法对日志等级进行分类,包括DEBUG
,INFO
,WARN
和ERROR
四种级别的日志
2.基础函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| #include <string.h> #include <time.h> #include <sys/time.h> #include <stdarg.h> #include "log.h" #include <pthread.h> using namespace std;
Log::Log() { m_count = 0; m_is_async = false; }
Log::~Log() { if (m_fp != NULL) { fclose(m_fp); } }
Log* Log::get_instance() { static Log instance; return &instance; }
void * Log::flush_log_thread(void *args) { Log::get_instance()->async_write_log(); }
void *Log::async_write_log() { string single_log; while (m_log_queue->pop(single_log)) { m_mutex.lock(); fputs(single_log.c_str(), m_fp); m_mutex.unlock(); } }
void Log::flush(void) { m_mutex.lock(); fflush(m_fp); m_mutex.unlock(); }
|
3.功能实现
init
函数实现日志创建、写入方式的判断。
write_log
函数完成写入日志文件中的具体内容,主要实现日志分级、分文件、格式化输出内容
(1)生成日志文件&判断写入方式
通过单例模式获取唯一的日志类,调用init
方法,初始化生成日志文件,服务器启动按当前时刻创建日志,前缀为时间,后缀为自定义log
文件名,并记录创建日志的时间day
和行数count
写入方式通过初始化时是否设置队列大小(表示在队列中可以放几条数据)来判断,若队列大小为0,则为同步,否则为异步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
|
bool Log::init(const char *file_name, int close_log, int log_buf_size, int split_lines, int max_queue_size) { if (max_queue_size >= 1) { m_is_async = true; m_log_queue = new block_queue<string>(max_queue_size); pthread_t tid; pthread_create(&tid, NULL, flush_log_thread, NULL); }
m_close_log = close_log; m_log_buf_size = log_buf_size; m_buf = new char[m_log_buf_size]; memset(m_buf, '\0', m_log_buf_size); m_split_lines = split_lines;
time_t t = time(NULL); struct tm *sys_tm = localtime(&t); struct tm my_tm = *sys_tm;
const char *p = strrchr(file_name, '/'); char log_full_name[256] = {0};
if (p == NULL) { snprintf(log_full_name, 255, "%d_%02d_%02d_%s", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, file_name); } else { strcpy(log_name, p + 1); strncpy(dir_name, file_name, p - file_name + 1); snprintf(log_full_name, 255, "%s%d_%02d_%02d_%s", dir_name, my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, log_name); }
m_today = my_tm.tm_mday;
m_fp = fopen(log_full_name, "a"); if (m_fp == NULL) { return false; }
return true; }
|
(2)日志分级&按天区分日志文件
日志分级的实现大同小异,一般的会提供五种级别,具体的
Debug
,调试代码时的输出,在系统实际运行时,一般不使用。
Warn
,这种警告与调试时终端的warning类似,同样是调试代码时使用。
Info
,报告系统当前的状态,当前执行的流程或接收的信息等。
Error
和Fatal
,输出系统的错误信息
超行、按天分文件逻辑,具体的
日志写入前会判断当前day
是否为创建日志的时间,行数是否超过最大行限制
- 若为创建日志时间,写入日志,否则按当前时间创建新
log
,更新创建时间和行数
- 若行数超过最大行限制,在当前日志的末尾加
count/max_lines
为后缀创建新log
将系统信息格式化后输出,具体为:格式化时间 + 格式化内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
|
void Log::write_log(int level, const char *format, ...) { struct timeval now = {0, 0}; gettimeofday(&now, NULL); time_t t = now.tv_sec; struct tm *sys_tm = localtime(&t); struct tm my_tm = *sys_tm; char s[16] = {0}; switch (level) { case 0: strcpy(s, "[debug]:"); break; case 1: strcpy(s, "[info]:"); break; case 2: strcpy(s, "[warn]:"); break; case 3: strcpy(s, "[erro]:"); break; default: strcpy(s, "[info]:"); break; } m_mutex.lock(); m_count++;
if (m_today != my_tm.tm_mday || m_count % m_split_lines == 0) {
char new_log[256] = {0}; fflush(m_fp); fclose(m_fp); char tail[16] = {0};
snprintf(tail, 16, "%d_%02d_%02d_", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday);
if (m_today != my_tm.tm_mday) { snprintf(new_log, 255, "%s%s%s", dir_name, tail, log_name); m_today = my_tm.tm_mday; m_count = 0; } else { snprintf(new_log, 255, "%s%s%s.%lld", dir_name, tail, log_name, m_count / m_split_lines); } m_fp = fopen(new_log, "a"); }
m_mutex.unlock();
va_list valst; va_start(valst, format);
string log_str; m_mutex.lock();
int n = snprintf(m_buf, 48, "%d-%02d-%02d %02d:%02d:%02d.%06ld %s ", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, my_tm.tm_hour, my_tm.tm_min, my_tm.tm_sec, now.tv_usec, s);
int m = vsnprintf(m_buf + n, m_log_buf_size - 1, format, valst); m_buf[n + m] = '\n'; m_buf[n + m + 1] = '\0'; log_str = m_buf;
m_mutex.unlock();
if (m_is_async && !m_log_queue->full()) { m_log_queue->push(log_str); } else { m_mutex.lock(); fputs(log_str.c_str(), m_fp); m_mutex.unlock(); }
va_end(valst); }
|