tinyWebServer 学习笔记——四、日志系统

文章目录

  • 一、基础知识
    • 1. 概念
    • 2. API
  • 二、代码解析
    • 1. 单例模式
    • 2. 阻塞队列
    • 3. 日志类定义
    • 4. 生成日志文件并判断写入方式
    • 5. 日志分级与分文件
  • 参考文献

一、基础知识

1. 概念

tinyWebServer 学习笔记——四、日志系统_第1张图片

流程图 [2]
  • 日志:由服务器自动创建,用于记录运行状态和错误信息;
  • 同步日志:日志写入函数与工作线程串行执行,由于涉及到 I/O 操作,当单条日志较大时,同步模式会阻塞整个处理流程,服务器所能处理的并发能力将有所下降,易称为性能的瓶颈;
  • 异步日志:将所写的日志内容先存入缓存队列,写线程从阻塞队列中去除内容,写入日志;
  • 生产者/消费者模型:生产者与消费者共享一个缓冲区,生产者往缓冲区添加消息,消费者从缓冲区中处理消息;
  • 阻塞队列:将生产者/消费者模型进行封装,使用循环数组实现队列,作为两者共享的缓存区;
  • 单例模式:保证一个类只创建一个实例,同时提供全局访问的方法,单例模式有懒汉模式与饿汉模式,前者在第一次使用时进行初始化,后者在程序运行时初始化;
  • 日志分级:一般提供五种级别:
    • Debug :调试代码时的输出,在系统实际运行时,一般不使用;
    • Warn :调试代码时使用的终端警告;
    • Info :报告系统当前的状态,当前执行的流程或接收的信息等;
    • Error 和 Fatal :输出系统的错误信息;
  • 日志分文件:根据日期、行数判断是否需要份文件;

2. API

  • pthread_cond_init 函数:用于初始化条件变量;

  • pthread_cond_destory 函数:销毁条件变量;

  • pthread_cond_broadcast 函数:以广播的方式唤醒所有等待目标条件变量的线程;

  • pthread_cond_wait 函数:用于等待目标条件变量,调用时传入 mutex 参数(加锁的互斥锁),执行时先把调用线程放入条件变量的请求队列,然后将互斥锁解锁,成功时返回 0 ,表示重新抢到了互斥锁,将其再次锁上,因此函数内部会有一次解锁和加锁操作,使用方法如下:

    pthread _mutex_lock(&mutex)
    
    // 此处必须使用while,而不是if
    // 若使用if,假设此时有A、B两个线程竞争资源,当A的wait函数返回成功后,B已将资源使用,那么此时A将访问被消耗的资源或不访问资源
    // 为避免这个问题,应当使用while循环判断资源是否真的可用
    while(线程执行的条件是否成立){
        pthread_cond_wait(&cond, &mutex);
    }
    
    pthread_mutex_unlock(&mutex);
    
    • 由 API 介绍可知,在使用 pthread_cond_wait 函数前需要加锁,这是为了避免资源竞争,使得每个线程互斥访问公有资源。

    • 在函数内部将互斥锁解锁,是因为函数会阻塞自己,此时它仍持有互斥锁,若不解锁则其他线程无法访问公有资源。

    • 在对互斥锁解锁前需要将调用线程放入请求队列,这是因为如果在把调用线程放入等待队列之前就释放互斥锁,会导致其他线程获得互斥锁从而访问公有资源,此时调用线程锁等待的条件改变了,但是它没有被放在等待队列上,导致调用线程忽略了等待条件被满足的信号,发生错误。

    • 当调用线程放在条件变量之后,将互斥锁解锁,此时等待被唤醒,若成功竞争到互斥锁,再次加锁来访问公共资源。

  • int fputs(const char *str, FILE *stream);

    • str :一个包含了要写入的以空字符终止的字符序列;
    • stream :指向 FILE 对象的指针,标识了被写入字符串的流;
  • __VA_ARGS__ :可变参数的宏,可以在宏定义中使用省略号,若加 ## 则更健壮,可以在可变参数为 0 时将逗号去掉:

    //最简单的定义
    #define my_print1(...)  printf(__VA_ARGS__)
    
    //搭配va_list的format使用
    #define my_print2(format, ...) printf(format, __VA_ARGS__)  
    #define my_print3(format, ...) printf(format, ##__VA_ARGS__)
    
  • int fflush(FILE *stream); :强迫缓存区内的数据协会参数 stream 指定的文件中,如果参数为 NULL ,则会将所有打开的文件数据更新。该函数主要用于防止新到的数据冲掉未输出完数据的缓冲区(即覆盖了原有数据),通常将其放在 printf 函数之后。

二、代码解析

1. 单例模式

经典的懒汉模式:

class single {
private:
    //私有静态指针变量指向唯一实例
    static single *p;

    //静态锁,是由于静态函数只能访问静态成员
    static pthread_mutex_t lock;

    //私有化构造函数
    single(){
        pthread_mutex_init(&lock, NULL);
    }
    ~single(){}

public:
    //公有静态方法获取实例
    static single* getinstance();

};

pthread_mutex_t single::lock;

single* single::p = NULL;
single* single::getinstance() {
    // 懒汉模式,使用双锁检测
    // 若只检测一次,则每次调用获取实例的方法时,都需要加锁,导致性能下降
    if (NULL == p){
        pthread_mutex_lock(&lock);
        if (NULL == p){
            p = new single;
        }
        pthread_mutex_unlock(&lock);
    }
    return p;
}

优雅的懒汉模式:

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

public:
    static single* getinstance();

};

single* single::getinstance() {
    // C++11能够保证静态变量的线程安全性,因此不用加锁
    static single obj;
    return &obj;
}

饿汉模式:

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

2. 阻塞队列

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

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

    // 析构函数
    ~block_queue()
    {
        m_mutex.lock();
        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_mutex.unlock();
            return false;
        }
        // 插入队列
        m_back = (m_back + 1) % m_max_size;
        m_array[m_back] = item;

        m_size++;
        // 唤醒
        m_cond.broadcast();
        m_mutex.unlock();
        return true;
    }

    // pop时,如果当前队列没有元素,将会等待条件变量
    bool pop(T &item)
    {
        m_mutex.lock();
        // 当前队列没有元素,等待
        while (m_size <= 0)
        {
            // 等待mutex
            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.unlock();
        return true;
    }

    // 增加了超时处理
    bool pop(T &item, int ms_timeout)
    {
        // timespec提供秒和纳秒,timeval提供秒和微妙
        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;     // 尾元素
};

3. 日志类定义

class Log
{
public:
    // C++11以后,使用局部变量懒汉不用加锁
    static Log *get_instance()
    {
        static Log instance;
        return &instance;
    }

    static void *flush_log_thread(void *args)
    {
        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,写入文件
        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;                      // log缓冲区指针
    block_queue<string> *m_log_queue; // 阻塞队列
    bool m_is_async;                  // 是否异步标志位
    locker m_mutex;                   // 互斥锁
    int m_close_log;                  // 关闭日志
};

// 日志分级
// DEBUG,获取实例来写log,然后刷新,##__VA_ARGS__是可变参数宏
#define LOG_DEBUG(format, ...)                                    \
    if (0 == m_close_log)                                         \
    {                                                             \
        Log::get_instance()->write_log(0, format, ##__VA_ARGS__); \
        Log::get_instance()->flush();                             \
    }
// log信息
#define LOG_INFO(format, ...)                                     \
    if (0 == m_close_log)                                         \
    {                                                             \
        Log::get_instance()->write_log(1, format, ##__VA_ARGS__); \
        Log::get_instance()->flush();                             \
    }
// warning
#define LOG_WARN(format, ...)                                     \
    if (0 == m_close_log)                                         \
    {                                                             \
        Log::get_instance()->write_log(2, format, ##__VA_ARGS__); \
        Log::get_instance()->flush();                             \
    }
// error
#define LOG_ERROR(format, ...)                                    \
    if (0 == m_close_log)                                         \
    {                                                             \
        Log::get_instance()->write_log(3, format, ##__VA_ARGS__); \
        Log::get_instance()->flush();                             \
    }

4. 生成日志文件并判断写入方式

// 初始化,异步需要设置阻塞队列的长度,同步不需要设置
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)
    {
        // 设置写入方式flag
        m_is_async = true;
        // 创建并设置阻塞队列长度
        m_log_queue = new block_queue<string>(max_queue_size);
        // 线程tid
        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);
    // 分解为tm结构,并用本地时区表示
    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)
    {
        // log_full_name存储处理后的字符串,复制大小为255,格式为第三个参数
        // 组织内容:年 月 日 文件名
        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
    {
        // 设置log名
        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;
    // 添加的方式打开log文件,存在m_fp中
    m_fp = fopen(log_full_name, "a");
    if (m_fp == NULL)
    {
        return false;
    }

    return true;
}

5. 日志分级与分文件

// 写日志
void Log::write_log(int level, const char *format, ...)
{
    // 获取时间
    struct timeval now = {0, 0};
    gettimeofday(&now, NULL);
    // 将记录精确到秒
    time_t t = now.tv_sec;
    // 转为tm
    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;
    }
    // 写入一个log,对m_count++,m_split_lines最大行数
    m_mutex.lock();
    // 记录一行log
    m_count++;
    // 判断日期或行数是否合规
    if (m_today != my_tm.tm_mday || m_count % m_split_lines == 0) // everyday log
    {

        char new_log[256] = {0};
        // 将缓冲区内容写入m_fp
        fflush(m_fp);
        // 关闭m_fp
        fclose(m_fp);
        char tail[16] = {0};
        // 在tail中记录日期
        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)
        {
            // new_log中记录路径和时间
            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, 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();

    // 写入的具体时间内容格式,时间、log级别,返回写入的字数
    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);
    // 1存储位置,2大小,3格式,4内容,返回写入的字数,该函数用于va_list
    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);
    }
    // 否则把缓存区的字符串写入m_fp
    else
    {
        m_mutex.lock();
        fputs(log_str.c_str(), m_fp);
        m_mutex.unlock();
    }

    va_end(valst);
}

参考文献

[1] 最新版Web服务器项目详解 - 09 日志系统(上)
[2] 最新版Web服务器项目详解 - 10 日志系统(下)

你可能感兴趣的:(计算机网络,linux,网络,服务器)