本项目中,使用单例模式创建日志系统,对服务器运行状态、错误信息和访问数据进行记录,该系统可以实现按天分类,超行分类功能,为了简单,将使用异步写入的方式。(后续再添加同步写入)
异步写入方式,就是将生产者-消费者模型封装为阻塞队列,创建一个写线程,工作线程只需要将要写的内容push进队列,写线程从队列中取出内容,写入日志文件即可。
系统流程
为了确保对同一日志文件的统一访问,我们采用get_instance()
方法来获取唯一的日志文件指针p
。get_instance()
作为Log
类的静态成员函数,它的作用是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
- 在程序的入口点
main
函数中,我们需要首先对Log
类进行初始化。 - 每当有新的信息需要记录到日志文件中时,系统会执行一个文件有效性检查。如果发现当前没有可用的日志文件、现有日志文件已达到存储上限,或者上一个日志文件不是在当前日期创建的,系统将自动生成一个新的日志文件。
- 随后,主线程将负责将信息进行格式化,并将信息添加到阻塞队列中。完成这一操作后,主线程便可以继续执行其他任务,而无需等待日志写入操作的完成。
- 与此同时,一旦阻塞队列中有待处理的日志记录任务,专门的写线程(一个子进程)将被唤醒。它会调用
async_write_log()
函数,将队列中的日志信息异步写入到磁盘中。
在这里,循环队列实际上是一个连续数组+2个移动指针组成的数据结构
日志类定义
日志类包括但不限于如下方法,
- 公有的实例获取方法
- 初始化日志文件方法
- 异步日志写入方法,内部调用私有异步方法
- 内容格式化方法
- 刷新缓冲区
class Log
{
public:
// 返回log对象的指针
static Log *get_instance()
{
/*
instance只能初始化一次,所以即便调用 get_instance() 函数多次,
代码static Log instance;只执行一次,即 instance 的地址不会被改变
*/
static Log instance;
return &instance;
}
static void *flush_log_thread(void* arg)
{
Log::get_instance()->async_write_log();
}
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();
private:
long long m_count;
int m_today;
int m_log_buf_size;
bool m_is_async;
char *m_buf;
int m_split_lines;
char log_name[128]; // log文件名
char dir_name[128]; // 路径名
locker m_mutex;
FILE *m_fp; //打开log的文件指针
block_queue<string> *m_log_queue; //阻塞队列
private:
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();
}
}
Log()
{
m_count = 0;
m_is_async = false;
}
~Log()
{
if(m_fp != NULL)
{
fclose(m_fp);
}
}
};
日志类中的方法都不会被其他程序直接调用,末尾的四个可变参数宏提供了其他程序的调用方法。
前述方法对日志等级进行分类,包括DEBUG,INFO,WARN和ERROR四种级别的日志。
#define LOG_DEBUG(format, ...) Log::get_instance()->write_log(0, format, ##__VA_ARGS__)
#define LOG_INFO(format, ...) Log::get_instance()->write_log(0, format, ##__VA_ARGS__)
#define LOG_WARN(format, ...) Log::get_instance()->write_log(0, format, ##__VA_ARGS__)
#define LOG_ERROR(format, ...) Log::get_instance()->write_log(0, format, ##__VA_ARGS__)
日志分文件判断
- 日志写入前会判断当前day是否为创建日志的时间,行数是否超过最大行限制
- 若为创建日志时间,写入日志,否则按当前时间创建新log,更新创建时间和行数
- 若行数超过最大行限制,在当前日志的末尾加count/max_lines为后缀创建新log
// 一天一个log,或者当文件行数满了,就新开一个log
if(m_today != my_tm.tm_mday || m_count % m_split_lines == 0)
{
char new_log[256] = {0};
// 刷新由 fopen 打开的输出流(如文件或控制台)的缓冲区。当使用 fflush 刷新流时,所有缓冲中的数据将被写入到流所关联的文件或设备中。
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");
}
异步写入日志
void Log::write_log(int level, const char* format, ...)
{
/*
struct timeval {
time_t tv_sec; // 秒
suseconds_t tv_usec; // 微秒
}
*/
struct timeval now = {0,0};
// 获取当前的日期和时间, 存储在now中
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;
}
// 写入一个log, 对 m_count++, m_split_lines最大行数
m_mutex.lock();
m_count++;
// 一天一个log,或者当文件行数满了,就新开一个log
if(m_today != my_tm.tm_mday || m_count % m_split_lines == 0)
{
char new_log[256] = {0};
// 刷新由 fopen 打开的输出流(如文件或控制台)的缓冲区。当使用 fflush 刷新流时,所有缓冲中的数据将被写入到流所关联的文件或设备中。
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-1, format, valst);
m_buf[n+m] = '\n';
// 加 \0 表示前面的字符组成一句话
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);
}
系列文章
GitHub - yzfzzz/MyWebServer: Linux高并发服务器项目,参考了TinyWebServer,将在此基础上进行性能改进与功能增加。为方便读者学习,附带详细注释和博客!
TinyWebserver的复现与改进(1):服务器环境的搭建与测试-CSDN博客
TinyWebserver的复现与改进(2):项目的整体框架-CSDN博客
TinyWebserver的复现与改进(3):线程同步机制类封装及线程池实现-CSDN博客
TinyWebserver的复现与改进(4):主线程的具体实现-CSDN博客
TinyWebserver的复现与改进(5):HTTP报文的解析与响应-CSDN博客
TinyWebserver的复现与改进(6):定时器处理非活动连接-CSDN博客