webserver-日志系统

1. 日志基础知识

  1. 日志由服务器自动创建,并记录运行状态,错误信息,访问数据的文件。分为同步日志和异步日志。
  2. 同步日志:日志写入函数与工作线程串行执行,由于涉及到I/O操作,当单条日志比较大的时候,同步模式会阻塞整个处理流程,服务器所能处理的并发能力将有所下降,尤其是在峰值的时候,写日志可能成为系统的瓶颈。
  3. 异步日志:将所写的日志内容先存入阻塞队列,写线程从阻塞队列中取出内容,写入日志。
  4. 单例模式:最简单也是被问到最多的设计模式之一,保证一个类只创建一个实例,同时提供全局访问的方法。
  5. 生产者-消费者模型:并发编程中的经典模型。以多线程为例,为了实现线程间数据同步,生产者线程与消费者线程共享一个缓冲区,其中生产者线程往缓冲区中push消息,消费者线程从缓冲区中pop消息。
  6. 阻塞队列:将生产者-消费者模型进行封装,使用循环数组实现队列,作为两者共享的缓冲区。
  7. 本项目使用单例模式创建日志系统,对服务器运行状态、错误信息和访问数据进行记录,该系统可以实现按天分类,超行分类功能,可以根据实际情况分别使用同步和异步写入两种方式。
  8. 其中异步写入方式,将生产者-消费者模型封装为阻塞队列,创建一个写线程,工作线程将要写的内容push进队列,写线程从队列中取出内容,写入日志文件。

2. 单例模式

  1. 单例模式作为最常用的设计模式之一,保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

  2. 实现思路:私有化它的构造函数,以防止外界创建单例类的对象;使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例。

  3. 单例模式有两种实现方法,分别是懒汉和饿汉模式。顾名思义,懒汉模式,即非常懒,不用的时候不去初始化,所以在第一次被使用时才进行初始化;饿汉模式,即迫不及待,在程序运行时立即初始化。

  4. 懒汉模式
    C++11之后的标准

class single{
private:
    single(){}
    ~single(){}

public:
    static single* getinstance();

};

single* single::getinstance(){
    static single obj;
    return &obj;
}
  1. 饿汉模式

饿汉模式不需要用锁,就可以实现线程安全。原因在于,在程序运行时就定义了对象,并对其初始化。之后,不管哪个线程调用成员函数getinstance(),都只不过是返回一个对象的指针而已。所以是线程安全的,不需要在获取实例的成员函数中加锁。

class single{
private:
    static single* p;
    single(){}
    ~single(){}

public:
    static single* getinstance();

};
single* single::p = new single();
single* single::getinstance(){
    return p;
}

//测试方法
int main(){

    single *p1 = single::getinstance();
    single *p2 = single::getinstance();

    if (p1 == p2)
        cout << "same" << endl;

    system("pause");
    return 0;
}

3. 阻塞队列

  1. 阻塞队列类中封装了生产者-消费者模型,其中push成员是生产者,pop成员是消费者。

  2. 阻塞队列中,使用了循环数组实现了队列,作为两者共享缓冲区,当然了,队列也可以使用STL中的queue。

#ifndef BLOCK_QUEUE_H
#define BLOCK_QUEUE_H

#include "locker.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;
        m_array = new T[max_size];
        m_size = 0;
        m_front = -1;
        m_back = -1;
    }

    // 为了保证线程安全,每个操作前都需要加锁,操作结束后再解锁

    // 析构函数,释放数组资源
    ~block_queue()
    {
        m_mutex.lock();
        if (m_array != NULL)
            delete[] m_array;
        m_mutex.unlock();
    }

    // 清空阻塞队列
    void clear()
    {
        m_mutex.lock();
        m_size = 0;
        m_front = -1;
        m_back = -1;
        m_mutex.unlock();
    }

    // 判断队列是否已满
    bool full()
    {
        m_mutex.lock();
        if (m_size >= m_max_size)
        {
            m_mutex.unlock();
            return false;
        }
        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]; // value是传出参数
        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_mutex.unlock();
            return false;
        }
        m_back = (m_back + 1) % m_max_size;
        m_array[m_back] = item;
        m_size++;
        m_cond.broadcast();
        m_mutex.lock();
        return true;
    }

    // 从队列中取元素,如果当前队列没有元素,则会等待条件变量
    bool pop(T &item)
    {
        m_mutex.lock();
        // 当有多个消费者时,用while而非if
        while (m_size <= 0)
        {
            if (!m_cond.wait(m_mutex.get()))
            {
                m_mutex.unlock();
                return false;
            }
        }
        m_front = (m_front + 1) % m_max_size;
        item = m_array[m_front]; // 传出参数
        m_size--;
        m_mutex.lock();
        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;
};

#endif

4. 日志类的定义

  1. 流程图
    在这里插入图片描述
  2. 日志类的定义

日志类中的方法都不会被其他程序直接调用,末尾的四个可变参数宏提供了其他程序的调用方法。

前述方法对日志等级进行分类,包括DEBUG,INFO,WARN和ERROR四种级别的日志。

#ifndef LOG_H
#define LPG_H

#include <stdio.h> // 提供fputs函数
#include <string>
#include <stdio.h>
#include "block_queue.h"
#include "locker.h"

using namespace std;

// 使用单例模式创建日志系统
class Log
{
public:
    // 使用共有的静态方法获得实例,c++11后使用局部变量懒汉不用加锁
    static Log *get_instance()
    {
        static Log instance;
        return &instance;
    }

    // 异步写日志公共方法,调用私有方法async_write_log
    static void *flush_log_thread(void *args)
    {
        Log::get_instance()->async_write_log();
    }

    // 日志参数包括日志文件、日志缓冲区大小、最大行数以及最长日志条队列(若队列大小为0,则为同步,否则为异步)
    bool init(const char *file_name, 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,写入文件
        while (m_log_queue->pop(single_log))
        {
            m_mutex.lock();
            fputs(single_log.c_str(), m_fp); // 将single_log写入m_fp所指向的文件
            m_mutex.unlock();
        }
    }

private:
    block_queue<string> *m_log_queue; // 阻塞队列
    locker m_mutex;                   // 互斥锁
    FILE *m_fp;                       // log文件指针

    char dir_name[128]; // 路径名
    char log_name[128]; // log文件名
    int m_log_buf_size; // 日志缓冲区的大小
    int m_split_lines;  // 日志最大行数
    long long m_count;  // 日志行数记录
    int m_today;        // 按天分文件,记录当前时间是哪一天

    bool m_is_async; // 是否异步标志位
    char *m_buf;     // 要输出的内容
};

// 日志分级,以下宏定义在其他文件中使用,主要用于不同类型的日志输出(Warn实际并未使用)
// Debug:调试代码的输出,在系统实际运行时一般不使用
#define LOG_DEBUG(format, ...) Log::get_instance()->write_log(0, format, ##__VA_ARGS__);
// Info:报告系统当前的状态,当前执行的流程或接收的信息
#define LOG_INFO(format, ...) Log::get_instance()->write_log(1, format, ##__VA_ARGS__);
// Warn:与调试时中断的warning类似,调试代码时使用
#define LOG_WARN(format, ...) Log::get_instance()->write_log(2, format, ##__VA_ARGS__);
// Error:输出系统的错误
#define LOG_ERROR(format, ...) Log::get_instance()->write_log(3, format, ##__VA_ARGS__);

#endif

4. 日志类的实现

  1. init函数实现日志的创建、写入方式的判断。
    确定同步与异步的方法为判断阻塞队列的长度max_queue_size,如果为0就是同步,不等于0为异步。
// 调用init方法,初始化生成日志文件,服务器启动按当前时刻创建日志,前缀为时间,
// 后缀为自定义log文件名,并记录创建日志的时间day和行数count。异步需要设置阻塞队列的长度,同步不需要
bool Log::init(const char *file_name, int log_buf_size, int split_lines, int max_queue_size)
{
    // 如果max_queue_size不为0则为异步
    if (max_queue_size >= 1)
    {
        m_is_async = true;

        // 创建并设置阻塞队列长度
        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_log_buf_size = log_buf_size;
    m_buf = new char[m_log_buf_size];
    memset(m_buf, '\0', sizeof(m_buf)); // 将m_buf数组元素全部初始化为0

    // 日志最大长度
    m_split_lines = split_lines;

    // 获取当前时间
    time_t t = time(NULL);
    struct tm *sys_tm = localtime(&t);
    struct tm my_tm = *sys_tm;

    // 从后往前找到一个'/'的位置,而strchr是从前往后
    const char *p = strrchr(file_name, '/');
    char log_full_name[255] = {0};
    // 自定义日志名
    if (p == NULL)
    {
        // 若输入的文件名没有'/',则直接将时间+文件名作为日志名
        // snprintf将可变个参数(...)按照%d_%02d_%02d_%s格式化成字符串(02的意思是如果输出的整型数不足两位,左侧用0补齐),然后将其复制到log_full_name中。
        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
    {
        // 将p向后移动一个位置,然后复制到log_name中
        strcpy(log_name, p + 1);
        // 将路径赋值到dir_name
        strncpy(dir_name, file_name, p - file_name + 1);
        snprintf(log_full_name, 255, "%s%d_02%d_02%d_%s", dir_name, my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, file_name);
    }

    m_today = my_tm.tm_mday;

    m_fp = fopen(log_full_name, "a"); // 以追加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留
    if (m_fp == NULL)
        return false;
    return true;
}
  1. 日志分级与分文件

日志分级的实现大同小异,一般的会提供五种级别,具体的,

Debug,调试代码时的输出,在系统实际运行时,一般不使用。

Warn,这种警告与调试时终端的warning类似,同样是调试代码时使用。

Info,报告系统当前的状态,当前执行的流程或接收的信息等。

Error和Fatal,输出系统的错误信息。

超行、按天分文件逻辑,具体的,

日志写入前会判断当前day是否为创建日志的时间,行数是否超过最大行限制

若为创建日志时间,写入日志,否则按当前时间创建新log,更新创建时间和行数

若行数超过最大行限制,在当前日志的末尾加count/max_lines为后缀创建新log

// 工作线程写日志
void Log::write_log(int level, const char *format, ...)
{

    // 获取时间
    struct timeval now = {0, 0}; // now有两个成员,秒数和毫秒数
    gettimeofday(&now, NULL);    // 返回自1970-01-01 00:00:00到现在经历的秒数
    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[258] = {0};
        /*在使用多个输出函数连续进行多次输出到控制台时,有可能下一个数据再上一个数据还没输出完毕,
        还在输出缓冲区中时,下一个printf就把另一个数据加入输出缓冲区,结果冲掉了原来的数据,出
        现输出错误。fflush会强迫将缓冲区内的数据写回参数m_fp指定的文件中*/
        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)
        { // 如果时间不是今天,则创建今天的日志,更新m_today和m_count
            snprintf(new_log, 255, "%s%s%s", dir_name, tail, log_name);
            m_today = my_tm.tm_mday;
            m_count = 0;
        }
        else
        { // 超过了最大行,在之前的日志名后加m_count / m_split_lines
            snprintf(new_log, 257, "%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;
    // 将本函数传入的参数format赋值给valst,便于格式化输出
    va_start(valst, format);

    string log_str;
    m_mutex.lock();
    // 写入内容格式:时间+内容
    // 时间格式化,snprintf返回写字符的总数(不包括结尾的NULL)
    int n = snprintf(m_buf, 48, "%d-%02d-%02d %02d:%02d:%02d.%06ld %s", my_tm.tm_year, my_tm.tm_mon, 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();

    // 若m_is_async为true表示异步写,需要将日志信息加入阻塞队列,否则为同步直接加锁向文件中写
    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); // 清空可变参数列表
}

参考链接:微信-Web服务器项目讲解-日志系统

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值