webserver--6.同步/异步日志系统


同步/异步日志系统

同步/异步日志系统主要涉及了两个模块,一个是日志模块,一个是阻塞队列模块,其中加入阻塞队列模块主要是解决异步写入日志做准备.

  • 自定义阻塞队列
  • 单例模式创建日志
  • 同步日志
  • 异步日志
  • 实现按天、超行分类

共两个个class,分别是阻塞队列block_queue和日志log

一、阻塞队列 block_queue

循环数组实现的阻塞队列,m_back = (m_back + 1) % m_max_size;
为了线程安全,每个操作前都要先加互斥锁,操作完后,再解锁

1.头文件

#include <iostream>
#include <stdlib.h>
#include <pthread.h>
#include <sys/time.h>
#include "../lock/locker.h"
using namespace std;

2.类的定义与方法实现

阻塞队列类直接定义在.h头文件中并对方法直接进行实现

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

    //增加了超时处理
    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;
};

二、日志

在这里插入图片描述

1.头文件

#include <stdio.h>
#include <iostream>
#include <string>
#include <stdarg.h>
#include <pthread.h>
#include "block_queue.h"

using namespace std;

2.类的定义

class Log
{
public:
    //C++11以后,使用局部变量懒汉不用加锁
    static Log *get_instance()
    {
        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; //关闭日志
};

3.宏定义

#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();}

4.构造函数与析构函数

构造函数, 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);
    }
}

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

6.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);
}

7. flush()方法

强制刷新写入流缓冲区,确保之前的输出内容被写入到文件中

void Log::flush(void)
{
    m_mutex.lock();
    //强制刷新写入流缓冲区
    fflush(m_fp);
    m_mutex.unlock();
}
  • 22
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值