TinyWebSever源码逐行注释(四)_log.cpp

前言

项目源码地址
项目详细介绍

项目简介:
Linux下C++轻量级Web服务器,助力初学者快速实践网络编程,搭建属于自己的服务器.

  1. 使用 线程池 + 非阻塞socket + epoll(ET和LT均实现) + 事件处理(Reactor和模拟Proactor均实现) 的并发模型
  2. 使用状态机解析HTTP请求报文,支持解析GET和POST请求
  3. 访问服务器数据库实现web端用户注册、登录功能,可以请求服务器图片和视频文件
  4. 实现同步/异步日志系统,记录服务器运行状态
  5. 经Webbench压力测试可以实现上万的并发连接数据交换

log.cpp用于配置web服务器的同步/异步日志系统。该系统主要涉及了两个模块,一个是日志模块,一个是阻塞队列模块,其中加入阻塞队列模块主要是解决异步写入日志做准备。主要内容如下:

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

原项目地址的注释较少不适合初学者,于是我将每行都加上了注释帮助大家更好的理解:

#include <string.h>  // 包含字符串处理函数,如 strcpy、strncpy 等
#include <time.h>    // 包含处理时间的库,如 time、localtime 等
#include <sys/time.h> // 包含 gettimeofday 用来获取当前时间
#include <stdarg.h>  // 处理不定参数函数
#include "log.h"     // 包含 Log 类的定义
#include <pthread.h> // 包含多线程处理函数
using namespace std; // 使用标准命名空间

// Log 类的构造函数,初始化日志类对象的成员变量
Log::Log()
{
    m_count = 0;  // 初始化日志条目计数器为 0
    m_is_async = false;  // 默认设置日志为同步模式
}

// Log 类的析构函数,负责清理资源
Log::~Log()
{
    if (m_fp != NULL)  // 如果日志文件指针不为空,则关闭文件
    {
        fclose(m_fp);  // 关闭日志文件
    }
}

// 初始化日志系统的函数
// file_name: 日志文件名,close_log: 是否关闭日志,log_buf_size: 日志缓冲区大小,split_lines: 最大行数,max_queue_size: 阻塞队列大小
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;  // 设置为异步模式
        // 创建一个阻塞队列,队列的最大长度为 max_queue_size
        m_log_queue = new block_queue<string>(max_queue_size);
        pthread_t tid;  // 定义一个线程 id
        // 创建一个新线程,用来异步写入日志,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);  // 将缓冲区初始化为全 0
    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;  // 记录当天日期
    
    // 打开日志文件,追加模式,如果打开失败则返回 false
    m_fp = fopen(log_full_name, "a");
    if (m_fp == NULL)
    {
        return false;  // 如果文件打开失败,返回 false
    }

    return true;  // 初始化成功
}

// 写入日志的函数
// level: 日志级别,format: 格式化字符串,...: 可变参数
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++;  // 日志计数器加 1

    // 如果当前日期不是今天或者日志条数超过设定的最大行数
    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 - 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(valst);  // 结束可变参数处理
}

// 刷新日志函数,强制将缓冲区内容写入日志文件
void Log::flush(void)
{
    m_mutex.lock();  // 加锁
    fflush(m_fp);  // 刷新文件流
    m_mutex.unlock();  // 解锁
}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值