C语言 服务器编程-日志系统

日志系统的实现

  • 引言
  • 最简单的日志类 demo
  • 按天日志分类和超行日志分类
  • 日志信息分级
  • 同步和异步两种写入方式

引言

日志系统是通过文件来记录项目的 调试信息,运行状态,访问记录,产生的警告和错误的一个系统,是项目中非常重要的一部分. 程序员可以通过日志文件观测项目的运行信息,方便及时对项目进行调整.

最简单的日志类 demo

日志类一般使用单例模式实现:

Log.h:

class Log
{
private:
	
	Log() {};
	~Log();
public:
	bool init(const char* file_name);

	void write_log(const char* str);

	static Log* getinstance();

private:
	FILE* file;
};

Log.cpp:

Log::~Log()
{
	if (file != NULL)
    {
	   fflush(file);
	   fclose(file);
    }
}

Log* Log::getinstance()
{
	static Log instance;
	return &instance;
}

bool Log::init(const char * file_name)
{
	file = fopen(file_name,"a");
	if (file == NULL)
	{
		return false;
	}
	return true;
}

void Log::write_log(const char* str)
{
	if (file == NULL)
		return;

	fputs(str, file);
}

main.cpp:

#include"Log.h"

int main()
{
	Log::getinstance()->init("log.txt");
	Log::getinstance()->write_log("Hello World");
}

这个日志类实现了最简单的写日志的功能,但是实际应用时,需要在日志系统上开发出许多额外的功能来满足工作需要,有些时候还要进行日志的分类操作,因为你不能将所有的日志信息都塞到一个日志文件中,这样会大大降低可读性,接下来讲一下在这个最简单的日志类的基础上,怎么添加一些新功能.

按天日志分类和超行日志分类

先说两个比较简单的
按天分类和超行分类

按天分类:每一个日志按照天来分类(日志前加上当前的日期作为日志的前缀) 并且写日志前检查日志的创建时间,如果日志创建时间不是今天,那么就额外新创建一个日志,更新创建时间和行数,然后向新日志中写日志信息

超行分类:写日志前检查本次程序写入日志的行数,如果当前本次程序写入日志的行数已经到达上限,那么额外创建新的日志,更新创建时间,然后向新日志中写日志信息

为了实现这两个小功能,我们需要先向日志类中添加以下成员:

  1. 程序本次启动,写入日志文件的最大行数
  2. 程序本次启动,已经写入日志的行数
  3. 日志的创建时间
  4. 日志的路径名+文件名(创建新日志的时候,命名要跟之前的命名标准一样,最好是标准日志名+后缀的形式,这样便于标识)

更新后的日志类:

Log.h:

#pragma once
#include<stdio.h>
#include<string.h>
#include<string>
#include<time.h>
using namespace std;

class Log
{
private:
	
	Log() ;
	~Log();
	
public:
	//初始化文件路径,文件最大行数
	bool init(const char* file_name,int split_lines= 5000000);

	void write_log(const char* str);

	static Log* getinstance();

private:
	FILE* file;
	char dir_name[128];//路径名
	char log_name[128];//日志名

	int m_split_lines; //日志文件最大行数(之前的日志行数不计,只记录本次程序启动写入的行数)
	long long m_count; //已经写入日志的行数
	int m_today;       //日志的创建时间
};

Log.cpp

#define _CRT_SECURE_NO_WARNINGS
#include"Log.h"

Log::Log()
{
	m_count = 0;
}

Log::~Log()
{
	if (file != NULL)
	{
		fflush(file);
		fclose(file);
	}
}

Log* Log::getinstance()
{
	static Log instance;
	return &instance;
}

bool Log::init(const char * file_name,int split_lines)
{
	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, '/');//这里需要注意下,windows和linux的路径上的 斜杠符浩方向是不同的,windows是\,linux是 / ,而且因为转义符号的原因,必须是 \\
	char log_full_name[256] = { 0 };

	if (p == NULL)  //判断是否输入了完整的路径+文件名,如果只输入了文件名
	{
	    strcpy(log_name, file_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            //如果输入了完整的路径名+文件名
	{
		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;          //更新时间

	file = fopen(log_full_name,"a");  //打开文件,打开方式:追加
	if (file == NULL)
	{
		return false;
	}
	return true;
}

void Log::write_log(const char* str)
{
    if (file == NULL)
	   return;
	   
	time_t t = time(NULL); 
	struct tm* sys_tm = localtime(&t); 
	struct tm  my_tm = *sys_tm;       //获取当前的时间,用来后续跟日志的创建时间作对比

	m_count++;                        //日志行数+1

	if (m_today != my_tm.tm_mday || m_count % m_split_lines == 0) //如果创建时间!=当前时间或者本次写入行数达到上限
	{ 
		char new_log[256] = { 0 };    //新日志的文件名
		fflush(file);
		fclose(file);

		char time_now[16] = { 0 };    //格式化当前的时间
		snprintf(time_now, 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在init函数被调用的时候一定会被设置成当天的时间,只有init和write函数的调用不在同一天中,才会出现这种情况
		
			snprintf(new_log, 255, "%s%s%s", dir_name, time_now, log_name);
			m_today = my_tm.tm_mday;  //更新创建时间
			m_count = 0;              //更新日志的行数
		}
		else                          //如果是行数达到本次我们规定的写入上限
		{
			snprintf(new_log,255,"%s%s%lld_%s", dir_name, time_now, m_count / m_split_lines,log_name);//加上版本后缀
		}

		file = fopen(new_log, "a");
	}

	fputs(str, file);
	fputs("\n", file);
}

运行的结果:
在这里插入图片描述
出现了一个以时间开头命名的日志,实现了按天分类

接下来我将一次性写入行数的上限调成5,看一下如果一次性写入超过了行数上限的运行结果是什么样:
在这里插入图片描述
出现了一个后缀_1的新文件

PS:这里有一个小BUG:因为m_count是每次运行程序都会重置的一个变量,所以上一次运行时可能因为输出的行数过多,创建了好多新日志,但是下一次运行程序时,还是从第一个日志开始打印的. 而且规定行数上限并不是日志中文件行数的上限,而是每次运行程序写入日志文件的行数上限,所以这个功能并不完美甚至说非常鸡肋暂时还没优化好,在这里仅做一个小小的演示吧.

日志信息分级

我们应该将每一条日志信息进行分类,可以分为四大类:
Debug: 调试中产生的信息
WARN: 调试中产生的警告信息
INFO: 项目运行时的状态信息
ERROR: 系统的错误信息

然后我们可以在日志文件中,每一条日志信息的前面,加上这条信息被写入的时间和其所属的分级,这样会大大增加日志的可读性.

代码还是在上面代码的基础上继续改动
Log.h:

#pragma once
#include<stdio.h>
#include<string.h>
#include<string>
#include<time.h>
using namespace std;

class Log
{
private:
	
	Log() ;
	~Log();
	
public:
	//初始化文件路径,日志缓冲区大小,文件最大行数
	bool init(const char* file_name, int log_buf_size = 8192, int split_lines= 5000000);

	//新增了一个日志分级
	void write_log(int level,const char* str);

	static Log* getinstance();

private:
	FILE* file;
	char dir_name[128];//路径名
	char log_name[128];//日志名

	int m_split_lines; //日志文件最大行数
	long long m_count; //日志当前的行数
	int m_today;       //日志创建的日期,记录是那一天

	int m_log_buf_size; //日志缓冲区的大小,用来存放日志信息字符串
	char* m_buf;        //日志信息字符串;因为后续要把时间和日志分级也加进来,所以开一个新的char *
};

Log.cpp:

#define _CRT_SECURE_NO_WARNINGS
#include"Log.h"

Log::Log()
{
	m_count = 0;
}

Log::~Log()
{
	if (file != NULL)
	{
		fflush(file);
		fclose(file);
	}

	
	if (m_buf != NULL)
	{
		delete[] m_buf;
		m_buf = nullptr;
	}
}

Log* Log::getinstance()
{
	static Log instance;
	return &instance;
}

bool Log::init(const char * file_name, int log_buf_size , int split_lines)
{
	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);
	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);
		strcpy(log_name, 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;          //更新日志的创建时间

	file = fopen(log_full_name,"a");  //打开文件,打开方式:追加
	if (file == NULL)
	{
		return false;
	}
	return true;
}

void Log::write_log(int level,const char* str)
{
	if (file == NULL)
		return;
	

	time_t t = time(NULL); 
	struct tm* sys_tm = localtime(&t); 
	struct tm  my_tm = *sys_tm;       //获取当前的时间,用来后续跟日志的创建时间作对比

	char level_s[16] = { 0 };         //日志分级
	switch (level)
	{
	case 0:
		strcpy(level_s, "[debug]:");
		break;
	case 1:
		strcpy(level_s, "[info]:"); 
		break;
	case 2:
		strcpy(level_s, "[warn]:"); 
		break;
	case 3:
		strcpy(level_s, "[erro]:"); 
		break;
	default:
		strcpy(level_s, "[info]:"); 
		break;
	}


	m_count++;                        //日志行数+1

	if (m_today != my_tm.tm_mday || m_count % m_split_lines == 0) //如果创建时间!=当前时间或者行数达到上限
	{
		char new_log[256] = { 0 };    //新日志的文件名
		fflush(file);
		fclose(file);

		char time_now[16] = { 0 };    //格式化当前的时间
		snprintf(time_now, 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, time_now, log_name);
			m_today = my_tm.tm_mday;  //更新创建时间
			m_count = 0;              //更新日志的行数
		}
		else                          //如果是行数达到文件上限
		{
			snprintf(new_log,255,"%s%s%lld_%s", dir_name, time_now, m_count / m_split_lines,log_name);//加上版本后缀
		}

		file = fopen(new_log, "a");
	}


	
	int n = snprintf(m_buf, 48, "%d-%02d-%02d %02d:%02d:%02d %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,level_s); 

	int m = snprintf(m_buf + n, m_log_buf_size-n-1,"%s",str);

	m_buf[n+m] = '\n';
	m_buf[n+m+1] = '\0';

	
	fputs(m_buf, file);

}

main.cpp:

#include<iostream>
#include"Log.h"

int main()
{
	Log::getinstance()->init("Log\\log.txt");

	Log::getinstance()->write_log(0,"Hello World");
	Log::getinstance()->write_log(1,"Hello World");
	Log::getinstance()->write_log(2,"Hello World");
	Log::getinstance()->write_log(3,"Hello World");
}

运行结果:
在这里插入图片描述
如图:日志信息前面已经加上了时间和类别分级,增加了可读性

同步和异步两种写入方式

同步写入和异步写入的逻辑:

图片来自公众号:两猿社
在这里插入图片描述
我先说明一下同步和异步的特点:
同步可以理解为顺序执行,而异步可以理解为并行执行
比如说吃饭和烧水两件事,如果先吃饭后烧水,这种是同步执行
如果说一边吃饭一边烧水,这种就是异步执行

那么同步执行和异步执行有什么优点,又使用在什么场景之下呢?

同步:

  1. 当对写入顺序和实时性要求很高时,例如需要确保按照特定顺序写入或写入即时生效的情况下,同步写入通常更合适。
  2. 在数据完整性和一致性很重要的情况下,同步写入能够提供更好的保证,避免数据丢失或不完整。
  3. 对于一些不频繁的、关键的写入操作,同步写入方式可能更容易确保操作的可靠性。

异步:

  1. 当写入频率很高或写入操作消耗较多时间时,使用异步写入可以显著提升系统性能和响应速度。
  2. 对于写入操作对主线程影响较大,容易阻塞主线程的情况下,通过异步写入可以将写入操作移到独立的线程中处理,减少主线程负担。
  3. 在需要降低I/O操作的影响、提高系统吞吐量和并发能力的场景下,异步写入方式更为适宜。

然后说一下如何实现同步和异步

同步只需要正常写入就行了
而异步我们可以借助生产者-消费者模型,由子线程执行写入操作.

如果你想了解生产者-消费者模型,请点击链接
生产者-消费者模型

接下来给出带有同步和异步两种写入方式的日志实现,还是在之前代码的基础上改动

封装了生产者-消费者模型的阻塞队列类:

#ifndef BLOCK_QUEUE_H
#define BLOCK_QUEUE_H

#include <iostream>
#include <stdlib.h>
#include <pthread.h>
#include <sys/time.h> //包含时间和定时器的头文件!
#include "../lock/locker.h"
using namespace std;

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)
        {
            
            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;
    }
private:
    locker m_mutex;
    cond m_cond;

    T *m_array;
    int m_size;
    int m_max_size;
    int m_front;
    int m_back;
};
#endif

注意: 这个生产者消费者模型并没有考虑生产者必须要在不满的情况下才能生产这一情况,不过这样也能凑活用,先凑活看吧

Log.h:

#pragma once
#include<stdio.h>
#include<string.h>
#include<string>
#include<time.h>
using namespace std;

class Log
{
private:
	
	Log() ;
	~Log();

	//异步写入方法:
	void* async_write_log()
	{
		string single_log;//要写入的日志
		while (m_log_queue->pop(single_log))
        {
            m_mutex.lock();//互斥锁上锁
            fputs(single_log.c_str(), file);
            m_mutex.unlock();//互斥锁解锁
        }
	}
	
public:
	//初始化文件路径,日志缓冲区大小,文件最大行数,阻塞队列长度(如果阻塞队列长度为正整数,表示使用异步写入,否则为同步写入)
	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* str);

	//公有的异步写入函数,作为消费者线程的入口函数
	static void* flush_log_thread(void *args)
	{
		Log::getinstance()->async_write_log();
	}

	static Log* getinstance();

private:
	FILE* file;
	char dir_name[128];//路径名
	char log_name[128];//日志名

	int m_split_lines; //日志文件最大行数
	long long m_count; //日志当前的行数
	int m_today;       //日志创建的日期,记录是那一天

	int m_log_buf_size; //日志缓冲区的大小,用来存放日志信息字符串
	char* m_buf;        //日志信息字符串;因为后续要把时间和日志分级也加进来,所以开一个新的char *

	block_queue<string>* m_log_queue; //阻塞队列,封装生产者消费者模型
	bool  m_is_async;                 //异步标记,如果为true,表示使用异步写入方式,否则是同步写入方式
	locker m_mutex;                   //互斥锁类,内部封装了互斥锁,用来解决多线程竞争资源问题
};

Log.cpp

#define _CRT_SECURE_NO_WARNINGS
#include"Log.h"
// pthread,mutex等需要在Linux下使用相关的头文件才能使用,因为我是windows环境就暂时不加了.

Log::Log()
{
	m_count = 0;
	m_is_async = false;
}

Log::~Log()
{
	if (file != NULL)
	{
		fflush(file);
		fclose(file);
	}

	
	if (m_buf != NULL)
	{
		delete[] m_buf;
		m_buf = nullptr;
	}
}

Log* Log::getinstance()
{
	static Log instance;
	return &instance;
}

bool Log::init(const char * file_name, int log_buf_size , int split_lines,int max_queue_size)
{
	if (max_queue_size >= 1)
	{
		//设置写入方式flag
		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', m_log_buf_size);// 开辟缓冲区,准备存放格式化的日志字符串


	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);
		strcpy(log_name, 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;          //更新日志的创建时间

	file = fopen(log_full_name,"a");  //打开文件,打开方式:追加
	if (file == NULL)
	{
		return false;
	}
	return true;
}

void Log::write_log(int level,const char* str)
{
	if (file == NULL)
		return;
	

	time_t t = time(NULL); 
	struct tm* sys_tm = localtime(&t); 
	struct tm  my_tm = *sys_tm;       //获取当前的时间,用来后续跟日志的创建时间作对比

	char level_s[16] = { 0 };         //日志分级
	switch (level)
	{
	case 0:
		strcpy(level_s, "[debug]:");
		break;
	case 1:
		strcpy(level_s, "[info]:"); 
		break;
	case 2:
		strcpy(level_s, "[warn]:"); 
		break;
	case 3:
		strcpy(level_s, "[erro]:"); 
		break;
	default:
		strcpy(level_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(file);
		fclose(file);

		char time_now[16] = { 0 };    //格式化当前的时间
		snprintf(time_now, 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, time_now, log_name);
			m_today = my_tm.tm_mday;  //更新创建时间
			m_count = 0;              //更新日志的行数
		}
		else                          //如果是行数达到文件上限
		{
			snprintf(new_log,255,"%s%s%lld_%s", dir_name, time_now, m_count / m_split_lines,log_name);//加上版本后缀
		}

		file = fopen(new_log, "a");
	}

	m_mutex.unlock();                 //互斥锁解锁

	string log_str; 
	m_mutex.lock();

   //格式化
	int n = snprintf(m_buf, 48, "%d-%02d-%02d %02d:%02d:%02d %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,level_s); 
  
	int m = snprintf(m_buf + n, m_log_buf_size-n-1,"%s",str);

	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(), file);
		m_mutex.unlock(); 
	}
}

日志系统先介绍到这里,我介绍的日志系统还是属于功能比较稀缺,实际使用上可能远远比这复杂,如果想使用日志系统,可以以文章介绍的为雏形继续添加新功能.

本文中代码非常可能有错误,如果发现有错误,烦请评论区指正,我会及时修改.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/383610.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Flutter 网络请求之Dio库

Flutter 网络请求之Dio库 前言正文一、配置项目二、网络请求三、封装① 单例模式② 网络拦截器③ 返回值封装④ 封装请求 四、结合GetX使用五、源码 前言 最近再写Flutter系列文章&#xff0c;在了解过状态管理之后&#xff0c;我们再来学习一下网络请求。 正文 网络请求对于一…

Linux基础-配置网络

Linux配置网络的方式 1.图形界面 右上角-wired-配置 点加号-新建网络配置文件2.NetworkManager工具 2.1用图形终端nmtui 1.新建网络配置文件add 1.指定网络设备的类型Ethernet 2.配置网络配置文件的名称&#xff0c;名称可以有空格 3.配置网络配置文件对应的物理网络设备的…

【大厂AI课学习笔记】【1.6 人工智能基础知识】(2)机器学习

目录 必须理解的知识点&#xff1a; 举一个草莓的例子&#xff1a; 机器学习的三个类别&#xff1a; 监督学习&#xff1a; 无监督学习&#xff1a; 强化学习&#xff1a; 更多知识背景&#xff1a; 机器学习的诞生需求 监督学习的关键技术与实现步骤 无监督学习的关…

【教学类-48-03】202402011“闰年”(每4年一次 2月有29日)世纪年必须整除400才是闰年)

2000-2099年之间的闰年有25次&#xff0c; 背景需求&#xff1a; 已经制作了对称年月的数字提取&#xff0c;和年月日相等的年份提取 【教学类-48-01】20240205对称的“年”和“月日”&#xff08;如2030 0302&#xff09;-CSDN博客文章浏览阅读84次。【教学类-48-01】202402…

可达鸭二月月赛——入门赛第四场T4题解

name 王胤皓 AC 记录 Problem Ideas 用一个字符串进行输入&#xff0c;第二个字符串赋值为第一个字符串&#xff0c;然后把第二个字符串进行翻转&#xff0c;第一个字符串称为 s s s&#xff0c;第二个字符串称为 s 2 s2 s2。 再用另外一个存储字典序最小的字符串&#xf…

中科大计网学习记录笔记(九):DNS

前言&#xff1a; 学习视频&#xff1a;中科大郑烇、杨坚全套《计算机网络&#xff08;自顶向下方法 第7版&#xff0c;James F.Kurose&#xff0c;Keith W.Ross&#xff09;》课程 该视频是B站非常著名的计网学习视频&#xff0c;但相信很多朋友和我一样在听完前面的部分发现信…

opencv图像像素的读写操作

void QuickDemo::pixel_visit_demo(Mat & image) {int w image.cols;//宽度int h image.rows;//高度int dims image.channels();//通道数 图像为灰度dims等于一 图像为彩色时dims等于三 for (int row 0; row < h; row) {for (int col 0; col < w; col) {if…

EMC学习笔记(二十四)降低EMI的PCB设计指南(四)

降低EMI的PCB设计指南&#xff08;四&#xff09; 1.电路板分区2.信号走线2.1 电容和电感串扰2.2 天线2.3 端接和传输线2.4输入端的阻抗匹配 tips&#xff1a;资料主要来自网络&#xff0c;仅供学习使用。 1.电路板分区 电路板分区与电路板平面规划具有相同的基本含义&#x…

【深度学习每日小知识】全景分割

全景分割 全景分割是一项计算机视觉任务&#xff0c;涉及将图像或视频分割成不同的对象及其各自的部分&#xff0c;并用相应的类别标记每个像素。与传统的语义分割相比&#xff0c;它是一种更全面的图像分割方法&#xff0c;传统的语义分割仅将图像划分为类别&#xff0c;而不…

集群及LVS简介、LVSNAT模式原理、LVSNAT模式配置、LVSDR模式原理、LVSDR模式配置、LVS错误排查

目录 集群 LVS 配置LVS NAT模式步骤 LVS DR模式 配置LVS DR模式 集群 将很多机器组织到一起&#xff0c;作为一个整体对外提供服务 集群在扩展性、性能方面都可以做到很灵活 集群分类&#xff1a; 负载均衡集群&#xff1a;Load Balance高可用集群&#xff1a;High Avai…

flask+python高校学生综合测评管理系统 phl8b

系统包括管理员、教师和学生三个角色&#xff1b; 。通过研究&#xff0c;以MySQL为后端数据库&#xff0c;以python为前端技术&#xff0c;以pycharm为开发平台&#xff0c;采用vue架构&#xff0c;建立一个提供个人中心、学生管理、教师管理、课程类型管理、课程信息管理、学…

CSS基础---新手入门级详解

CSS:层叠样式表 CSS&#xff08;Cascading Style Sheets,层叠样式表&#xff09;&#xff0c;是一种用来为结构化文档添加样式&#xff08;字体、间距和颜色&#xff09;的计算机语言&#xff0c;css扩展名为.css。 实例: <!DOCTYPE html><html> <head><…

ubuntu中尝试安装ros2

首先&#xff0c;ubuntu打开后有个机器人栏目&#xff0c;打开后&#xff0c;有好多可选的&#xff0c;看了半天 ,好像是博客&#xff0c;算了&#xff0c;没啥关系&#xff0c;再看看其他菜单 这些都不是下载链接。先不管&#xff0c;考虑了一下&#xff0c;问了ai&#xff…

板块一 Servlet编程:第二节 Servlet的实现与生命周期 来自【汤米尼克的JAVAEE全套教程专栏】

板块一 Servlet编程&#xff1a;第二节 Servlet的实现与生命周期 一、Servlet相关概念Serlvet的本质 二、中Web项目中实现Servlet规范&#xff08;1&#xff09;在普通的Java类中继承HttpServlet类&#xff08;2&#xff09;重写service方法编辑项目对外访问路径 二、Servlet工…

LeetCode.144. 二叉树的前序遍历

题目 144. 二叉树的前序遍历 分析 这道题目是比较基础的题目&#xff0c;我们首先要知道二叉树的前序遍历是什么&#xff1f; 就是【根 左 右】 的顺序&#xff0c;然后利用递归的思想&#xff0c;就可以得到这道题的答案&#xff0c;任何的递归都可以采用 栈 的结构来实现…

[C++] opencv + qt 创建带滚动条的图像显示窗口代替imshow

在OpenCV中&#xff0c;imshow函数默认情况下是不支持滚动条的。如果想要显示滚动条&#xff0c;可以考虑使用其他库或方法来进行实现。 一种方法是使用Qt库&#xff0c;使用该库可以创建一个带有滚动条的窗口&#xff0c;并在其中显示图像。具体步骤如下&#xff1a; 1&…

使用PyOD进行异常值检测

异常值检测各个领域的关键任务之一。PyOD是Python Outlier Detection的缩写&#xff0c;可以简化多变量数据集中识别异常值的过程。在本文中&#xff0c;我们将介绍PyOD包&#xff0c;并通过实际给出详细的代码示例 PyOD简介 PyOD为异常值检测提供了广泛的算法集合&#xff0c…

【Rust】使用Rust实现一个简单的shell

一、Rust Rust是一门系统编程语言&#xff0c;由Mozilla开发并开源&#xff0c;专注于安全、速度和并发性。它的主要目标是解决传统系统编程语言&#xff08;如C和C&#xff09;中常见的内存安全和并发问题&#xff0c;同时保持高性能和底层控制能力。 Rust的特点包括&#x…

C++构造和折构函数详解,超详细!

个人主页&#xff1a;PingdiGuo_guo 收录专栏&#xff1a;C干货专栏 大家龙年好呀&#xff0c;今天我们来学习一下C构造函数和折构函数。 文章目录 1.构造函数 1.1构造函数的概念 1.2构造函数的思想 1.3构造函数的特点 1.4构造函数的作用 1.5构造函数的操作 1.6构造函数…

洗地机哪个品牌最耐用质量好?耐用的洗地机型号

相较于传统的打扫方式&#xff0c;洗地机的出现可以称得上是懒人福音。一台洗地机就能包办吸、扫、拖所有清洁步骤&#xff0c;节省了大量的打扫时间。不过最近几年洗地机行业涌入的品牌属实有些鱼龙混杂了&#xff0c;至于型号就更是乱七八糟&#xff0c;稍不留神就会白白花了…