前言
项目简介:
Linux下C++轻量级Web服务器,助力初学者快速实践网络编程,搭建属于自己的服务器.
- 使用 线程池 + 非阻塞socket + epoll(ET和LT均实现) + 事件处理(Reactor和模拟Proactor均实现) 的并发模型
- 使用状态机解析HTTP请求报文,支持解析GET和POST请求
- 访问服务器数据库实现web端用户注册、登录功能,可以请求服务器图片和视频文件
- 实现同步/异步日志系统,记录服务器运行状态
- 经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(); // 解锁
}