本文是对github上万star项目源码解析,供大家学习交流
项目地址:
https://github.com/qinguoyi/TinyWebServer
五、同步/异步日志系统
同步/异步日志系统
===============
同步/异步日志系统主要涉及了两个模块,一个是日志模块,一个是阻塞队列模块,其中加入阻塞队列模块主要是解决异步写入日志做准备
=>自定义阻塞队列
=>单例模式创建日志
=>同步日志
=>异步日志
=>实现按天、超行分类
===============
共两个个class,分别是阻塞队列block_queue和日志log
===============
1、阻塞队列
头文件
#include <iostream>
#include <stdlib.h>
#include <pthread.h>
#include <sys/time.h>
#include "../lock/locker.h"
1.1 类的定义与方法实现
阻塞队列类直接定义在.h头文件中并对方法直接进行实现
using namespace std;
template <class T>
class block_queue{
public:
// 构造函数
block_queue(int max_size = 1000){
if (max_size <= 0){
exit(-1);
}
m_max_size = max_size;
// 在堆上动态分配了一个大小为max_size的数组m_array来存储队列元素
m_array = new T[max_size];
m_size = 0;
// 由于刚创建时队列为空,所以m_front和m_back初始化为-1
m_front = -1;
m_back = -1;
}
// 清空队列中的所有元素
void clear(){
// 由于这是一个多线程环境下的代码,所以需要使用互斥锁来保证多个线程之间对队列的操作是同步的,以避免出现竞态条件
m_mutex.lock();
m_size = 0;
m_front = -1;
m_back = -1;
m_mutex.unlock();
}
// 析构函数
~block_queue(){
m_mutex.lock();
// 检查m_array指针是否为NULL,如果不为NULL,则使用delete[]运算符删除m_array所指向的动态分配的数组,释放内存空间
if (m_array != NULL)
delete [] m_array;
m_mutex.unlock();
}
//判断队列是否满了
bool full(){
m_mutex.lock();
if (m_size >= m_max_size){
m_mutex.unlock();
return true;
}
m_mutex.unlock();
return false;
}
//判断队列是否为空
bool empty(){
m_mutex.lock();
if (0 == m_size){
m_mutex.unlock();
return true;
}
m_mutex.unlock();
return false;
}
//返回队首元素
bool front(T &value) {
m_mutex.lock();
if (0 == m_size){
m_mutex.unlock();
return false;
}
value = m_array[m_front];
m_mutex.unlock();
return true;
}
//返回队尾元素
bool back(T &value) {
m_mutex.lock();
if (0 == m_size){
m_mutex.unlock();
return false;
}
value = m_array[m_back];
m_mutex.unlock();
return true;
}
// 返回队列中元素的个数
int size() {
int tmp = 0;
m_mutex.lock();
tmp = m_size;
m_mutex.unlock();
return tmp;
}
// 返回队列的最大容量
int max_size(){
int tmp = 0;
m_mutex.lock();
tmp = m_max_size;
m_mutex.unlock();
return tmp;
}
// 往队列添加元素,需要将所有使用队列的线程先唤醒
// 当有元素push进队列,相当于生产者生产了一个元素
// 若当前没有线程等待条件变量,则唤醒无意义
bool push(const T &item){
m_mutex.lock();
if (m_size >= m_max_size){
// 队列已满,调用条件变量m_cond的broadcast()方法,通知所有等待在该条件变量上的线程
m_cond.broadcast();
m_mutex.unlock();
return false;
}
// 将新元素存储在队尾位置
m_back = (m_back + 1) % m_max_size;
m_array[m_back] = item;
// 将队列中元素的个数m_size加1
m_size++;
// 调用条件变量m_cond的broadcast()方法,通知所有等待在该条件变量上的线程,即通知有可能在pop操作中等待的线程可以执行了
m_cond.broadcast();
m_mutex.unlock();
return true;
}
// 用于从队列中取出元素,pop时如果当前队列没有元素,将会等待条件变量
bool pop(T &item){
m_mutex.lock();
while (m_size <= 0){
// 如果队列为空,则调用条件变量m_cond的wait()方法,等待其他线程向队列中插入元素
if (!m_cond.wait(m_mutex.get())){
m_mutex.unlock();
return false;
}
}
// 当取出了队列中的一个元素时,该函数将队头下标m_front加1
m_front = (m_front + 1) % m_max_size;
// 并将队头元素m_array[m_front]赋值给输出参数item
item = m_array[m_front];
// 将队列中元素的个数m_size减1,表示队列中的元素数量减少了一个
m_size--;
m_mutex.unlock();
return true;
}
// 增加了超时处理的pop
bool pop(T &item, int ms_timeout){
struct timespec t = {0, 0};
struct timeval now = {0, 0};
gettimeofday(&now, NULL);
m_mutex.lock();
if (m_size <= 0){
t.tv_sec = now.tv_sec + ms_timeout / 1000;
t.tv_nsec = (ms_timeout % 1000) * 1000;
if (!m_cond.timewait(m_mutex.get(), t)){
m_mutex.unlock();
return false;
}
}
if (m_size <= 0){
m_mutex.unlock();
return false;
}
m_front = (m_front + 1) % m_max_size;
item = m_array[m_front];
m_size--;
m_mutex.unlock();
return true;
}
private:
locker m_mutex;
cond m_cond;
T *m_array;
int m_size;
int m_max_size;
int m_front;
int m_back;
};
2、日志
头文件以及宏定义
#include <stdio.h>
#include <iostream>
#include <string>
#include <stdarg.h>
#include <pthread.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include "block_queue.h"
#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();}
2.1 类的定义
using namespace std;
class Log{
public:
// C++11以后,使用局部变量懒汉不用加锁
static Log *get_instance(){
// static Log表示instance是一个静态变量,并且它的类型是Log
static Log instance;
return &instance;
}
// 是一个线程函数,用于异步写日志
static void *flush_log_thread(void *args){
// 通过调用静态成员函数Log::get_instance()获取Log类的单例实例,并通过该实例调用 async_write_log()方法进行日志输出操作
Log::get_instance()->async_write_log();
}
//可选择的参数有日志文件、日志缓冲区大小、最大行数以及最长日志条队列
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(){
// 使用string类型的变量single_log来存储要写入文件的单个日志记录
string single_log;
//从阻塞队列中取出一个日志string,写入文件
while (m_log_queue->pop(single_log)){
m_mutex.lock();
fputs(single_log.c_str(), m_fp);
m_mutex.unlock();
}
}
private:
char dir_name[128]; //路径名
char log_name[128]; //log文件名
int m_split_lines; //日志最大行数
int m_log_buf_size; //日志缓冲区大小
long long m_count; //日志行数记录
int m_today; //因为按天分类,记录当前时间是那一天
FILE *m_fp; //打开log的文件指针
char *m_buf;
block_queue<string> *m_log_queue; //阻塞队列
bool m_is_async; //是否同步标志位
locker m_mutex;
int m_close_log; //关闭日志
};
2.2 构造函数与析构函数
构造函数, m_count 成员变量初始化为0,并将 m_is_async 成员变量初始化为false
Log::Log(){
m_count = 0; //日志行数记录
m_is_async = false; //是否同步标志位
}
析构函数,用于释放资源
Log::~Log(){
if (m_fp != NULL){
// 调用fclose函数来关闭日志文件,并释放文件指针相关的资源
fclose(m_fp);
}
}
2.3 init()方法
日志类的初始化函数,用于对日志对象进行初始化。该函数接受一些参数,包括日志文件名、是否关闭日志、缓冲区大小、每个日志文件最大行数和最大队列大小等。函数返回布尔类型的值,表示初始化是否成功。
//异步需要设置阻塞队列的长度,同步不需要设置
bool Log::init(const char *file_name, int close_log, int log_buf_size, int split_lines, int max_queue_size){
// 如果设置了max_queue_size,则设置为异步
if (max_queue_size >= 1){
m_is_async = true;
// 创建一个阻塞队列对象m_log_queue
m_log_queue = new block_queue<string>(max_queue_size);
// 创建一个线程来异步写入日志
pthread_t tid;
// flush_log_thread为回调函数,这里表示创建线程异步写日志
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;
// 根据传入的日志文件路径和名称,使用strrchr()函数找到最后一个斜杠字符'/'的位置
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成员变量设置为当前日期
m_today = my_tm.tm_mday;
// 使用fopen()函数打开日志文件,以追加方式写入日志
m_fp = fopen(log_full_name, "a");
// 如果打开文件失败,则返回false
if (m_fp == NULL){
return false;
}
return true;
}
2.4 write_log()方法
// level代表日志级别(0-DEBUG,1-INFO,2-WARNING,3-ERROR),format代表格式化字符串
void Log::write_log(int level, const char *format, ...){
struct timeval now = {0, 0};
gettimeofday(&now, NULL); // 使用 gettimeofday() 获取当前时间
time_t t = now.tv_sec;
struct tm *sys_tm = localtime(&t); // 使用 localtime() 将当前时间转换成本地时间
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;
}
//写入一个log,对m_count++, m_split_lines最大行数
m_mutex.lock();
m_count++;
// 对日志文件进行写入操作,实现了按照日期和行数分割日志文件的功能。如果发现当前日期与之前记录的日期不同,或者已经写入的行数达到某个阈值 m_split_lines,则创建新的日志文件
if (m_today != my_tm.tm_mday || m_count % m_split_lines == 0) //everyday log{
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);
// 使用vsnprintf()将可变长参数列表中的数据按照format格式化,并将格式化后的字符串添加到日志缓冲区中
int m = vsnprintf(m_buf + n, m_log_buf_size - n - 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()关闭可变长参数列表
va_end(valst);
}
2.5 flush()方法
强制刷新写入流缓冲区,确保之前的输出内容被写入到文件中
void Log::flush(void){
m_mutex.lock();
//强制刷新写入流缓冲区
fflush(m_fp);
m_mutex.unlock();
}
本文介绍了GitHub上一个获得万星的项目的日志系统设计,包括同步/异步日志模块和自定义阻塞队列。阻塞队列用于解决异步写入日志的问题,而日志模块采用了单例模式和按天、超行分类的策略。文章详细讲解了阻塞队列的实现和日志写入的逻辑。
1195

被折叠的 条评论
为什么被折叠?



