【项目】多设计模式下的同步异步日志系统

文章目录

  • 项目介绍
  • 开发环境
  • 核心技术
  • 日志系统介绍
    • 为什么需要日志系统
    • 日志系统技术实现
      • 同步写日志
      • 异步写日志
  • 相关技术知识补充
    • 不定参函数
      • 不定参宏函数的使用
      • C中不定参函数的使用
      • C++中不定参函数的使用
    • 设计模式
      • 单例模式
      • 工厂模式
      • 建造者模式
      • 代理模式
  • 日志系统框架设计
    • 模块划分
      • 日志等级模块
      • 日志消息模块
      • 日志消息格式化模块
      • 日志落地模块
      • 日志器模块
      • 异步线程模块
      • 单例的日志器管理模块
      • 模块关系图
  • 代码设计
    • 实用类设计
    • 日志等级模块设计
    • 日志消息类设计
    • 日志格式化类设计
    • 日志落地类设计 -- 简单工厂模式
    • 日志器模块设计 -- 涉及建造者模式
      • 同步日志器
      • 异步日志器
        • 单个缓冲区设计
        • 异步工作器的设计 -- 双缓冲区思想
        • 异步日志器设计
    • 日志器管理模块 -- 单例模式
      • 日志管理器设计
      • 设计一个全局的日志器建造者
      • 测试日志器管理器的接口和全局建造者类
    • 宏函数和全局接口设计 -- 涉及代理模式
      • 新建一个.h,文件,文件里面放我们写的.hpp(各个模块文件)
      • 对宏函数与全局接口进行功能测试
  • 项目目录结构
  • 功能测试
  • 性能测试
    • 项目性能测试工具实现
      • C中不定参函数的使用
      • C++中不定参函数的使用
    • 设计模式
      • 单例模式
      • 工厂模式
      • 建造者模式
      • 代理模式
  • 日志系统框架设计
    • 模块划分
      • 日志等级模块
      • 日志消息模块
      • 日志消息格式化模块
      • 日志落地模块
      • 日志器模块
      • 异步线程模块
      • 单例的日志器管理模块
      • 模块关系图
  • 代码设计
    • 实用类设计
    • 日志等级模块设计
    • 日志消息类设计
    • 日志格式化类设计
    • 日志落地类设计 -- 简单工厂模式
    • 日志器模块设计 -- 涉及建造者模式
      • 同步日志器
      • 异步日志器
        • 单个缓冲区设计
        • 异步工作器的设计 -- 双缓冲区思想
        • 异步日志器设计
    • 日志器管理模块 -- 单例模式
      • 日志管理器设计
      • 设计一个全局的日志器建造者
      • 测试日志器管理器的接口和全局建造者类
    • 宏函数和全局接口设计 -- 涉及代理模式
      • 新建一个.h,文件,文件里面放我们写的.hpp(各个模块文件)
      • 对宏函数与全局接口进行功能测试
  • 项目目录结构
  • 功能测试
  • 性能测试
    • 项目性能测试工具实现

日志系统:

日志:程序运行过程中所记录的程序运行状态信息

日志的作用:记录程序运行状态信息,以便于程序员能够随时根据状态信息,对系统的运行状态进行分析。

项目不是一个功能型业务型的项目,是一个组件型的项目,是一个库,是给别人用的;所以项目演示时,就展示一下日志系统的性能。

项目介绍

本项目主要实现的是一个日志系统,其支持以下功能:

  • 支持多级别日志消息
  • 支持同步日志和异步日志
  • 支持写入日志到控制台、文件以及滚动文件中
  • 支持多线程程序并发写日志
  • 支持扩展不同的日志落地到目标地

开发环境

  • 操作系统 :Centos 7
  • 编辑器: vscode / vim
  • 编译器/调试器:g++ / gdb
  • 项目自动化构建工具:Makefile

核心技术

  • 类层次设计(继承、多态的实际应用)
  • C++11语法(多线程库,auto,智能指针,右值引用等)
  • 双缓冲区
  • 生产者消费者模型
  • 多线程
  • 设计模式(单例、工厂、代理、建造者等)

日志系统介绍

为什么需要日志系统

  • 在生产环境中的产品,为了保证其稳定性以及安全性,是不允许开发人员附加调试器去排查问题的(调试器调试时出现程序崩溃需要根据程序运行数据进行分析,这是不允许的,产品是有隐私的),可以借助日志系统来打印一些日志帮助开发人员解决问题
  • 上线客户端的产品出现的Bug无法复现并解决,可以借助日志系统打印日志并且上传到服务端帮助开发人员进行问题分析
  • 对于一些高频操作(如定时器,心跳包等)在少量调试次数下可能无法触发我们想要的行为,通过断点暂停的方式,我们需要重复几十次甚至上百次,导致排查问题的效率非常低下,可以借助打印日志的方式排查问题
  • 在分布式、多线程/多进程的代码中,出现bug非常难定位,可以借助日志系统打印日志帮助定位bug
  • 帮助刚接触项目不久的开发人员理解代码的运行流程

日志系统技术实现

日志系统的技术实现主要包括三种类型:

  1. 利用printf、std::cout等输出函数将日志信息打印到控制台。-- 1
  2. 对于大型商业化项目,为了方便排查问题,我们一般会将日志输出到文件或者是数据库系统方便查询和分析日志,主要分为同步日志异步日志方式。
    • 同步写日志 – 2
    • 异步写日志 – 3

同步写日志

同步日志指的是当输出日志时,必须等待日志输出语句执行完毕后,才能执行后面的业务逻辑语句,日志输出语句与程序的业务逻辑语句将在同一个线程中运行。每次调用一次打印日志API就对应一次系统调用write写日志文件

image-20240827114052891

优点:流程简单,编写代码简单

缺点:在高并发场景下,随着日志数量不断增加,同步日志系统容易产生系统瓶颈:

  1. ⼀方⾯,⼤量的日志打印陷⼊等量的write系统调用,有⼀定系统开销.
  2. 另⼀方⾯,使得打印日志的进程附带了⼤量同步的磁盘IO,影响程序性能

异步写日志

异步日志是指在进行日志输出时,日志输出语句与业务逻辑语句并不是在同一个线程中运行,而是有专门的线程用于进行日志输出操作,业务线程只需要将日志放到一个内存缓冲区中,不需要等待即可继续执行后续业务逻辑(作为日志的生产者),而日志的落地操作交给单独的日志线程完成(作为日志的消费者),这是一个典型的生产者消费者模型

image-20240827120755639

这样的好处是即使日志没有真正的完成输出也不会影响程序的主业务,提高程序的性能

  1. 主线程调用日志打印接口成为非阻塞操作
  2. 同步的磁盘IO操作从主线程剥离出来交给单独的线程完成。

相关技术知识补充

不定参函数

在初学C语⾔的时候,我们都⽤过printf函数进⾏打印。其中printf函数就是⼀个不定参函数,在函数内部可以根据格式化字符串中格式化字符分别获取不同的参数进⾏数据的格式化。

不定参宏函数的使用

  1. 不定参的表示用:“…”
  2. 不定参的使用:“ __VA_REGS__”。(在 __VA_REGS__ 前加 ##,意思是如果 __VA_REGS__ 为空就取消前面的逗号。)
#include <stdio.h>
#define LOG(fmt, ...) printf("[%s:%d]" fmt, __FILE__, __LINE__, __VA_ARGS__);
int main()
{//编译器内置的宏:__FILE__当前文件名;__LINE__当前行号。LOG("%s-%d\n", "Anduin", 521);return 0;
}

image-20240827153855676如果只有LOG(“Anduin”),就会报错;这时候就要加##号。

#define LOG(fmt, ...) printf("[%s:%d]" fmt, __FILE__, __LINE__, ##__VA_ARGS__);

image-20240827154001378

C中不定参函数的使用

#define _GNU_SOURCE //使用vasprintf函数必须带这个宏
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
//参数类型一样
void printNum(int n, ...)
{va_list al; // 可变参数列表va_start(al, n); // 获取n参数之后的第一个不定参数的地址for (int i = 0; i < n; i++){int num = va_arg(al, int); // 从可变参数列表中获取⼀个整形参数printf("param[%d]:%d\n", i, num);}va_end(al); // 将al置空
}
//参数类型不一样 -- 模拟一下 printf 的实现
void myprintf(const char* fmt,...)
{va_list ap;va_start(ap,fmt);char *res;//对不定参数的基本使用;給一个一级指针的地址(&res)进去,根据fmt里面的字符串格式,一个一个的取出可变参数列表的每个数。int ret=vasprintf(&res,fmt,ap);if(ret!=-1){printf(res);free(res);}va_end(ap);//将ap指针置空
}
int main()
{printNum(3, 11, 22,33);printNum(5, 44, 55, 66, 77, 88);myprintf("%s %d\n","Anduin",521);return 0;
}

image-20240828075727198

C++中不定参函数的使用

#include <iostream>// 实现一个打印操作// 特化
void xprintf()
{std::cout<<std::endl;
}template <typename T,typename ...Args>
void xprintf(const T &v,Args &&...args)
{std::cout << v;// 通过 sizeof 计算参数包参数的个数if((sizeof ...(args))>0){ xprintf(std::forward<Args>(args)...);}else{// 参数包没参数了xprintf();}
}int main()
{xprintf("Anduin","haha",521);xprintf("Anduin","haha");xprintf("Anduin");return 0;
}

image-20240828083611601

设计模式

设计模式是前辈们对代码开发经验的总结,是解决特定问题的⼀系列套路。它不是语法规定,⽽是⼀套⽤来提⾼代码可复⽤性、可维护性、可读性、稳健性以及安全性的解决⽅案。

六⼤原则:

  • 单⼀职责原则(SingleResponsibilityPrinciple);

    • 类的职责应该单⼀,⼀个类只做⼀件事。职责划分清晰了,每次改动到最⼩单位的⽅法或类。
    • 使⽤建议:两个完全不⼀样的功能不应该放⼀个类中,⼀个类中应该是⼀组相关性很⾼的函数、数据的封装
    • ⽤例:⽹络聊天:⽹络通信&聊天,应该分割成为网络通信类&聊天类
  • 开闭原则(OpenClosedPrinciple )

    • 对扩展开放,对修改封闭。
    • 使⽤建议:对软件实体的改动,最好⽤扩展⽽⾮修改的⽅式。
    • ⽤例:超市卖货:商品价格—不是修改商品的原来价格,⽽是新增促销价格。
  • 里氏替换原则(LiskovSubstitutionPrinciple )

    • 通俗点讲,就是只要⽗类能出现的地⽅,⼦类就可以出现,⽽且替换为⼦类也不会产⽣任何错误或异常。
    • 在继承类时,务必重写⽗类中所有的⽅法,尤其需要注意⽗类的protected⽅法,⼦类尽量不要暴露⾃⼰的public⽅法供外界调⽤。
    • 使⽤建议:⼦类必须完全实现⽗类的⽅法,孩⼦类可以有⾃⼰的个性。覆盖或实现⽗类的⽅法时,输⼊参数可以被放⼤,输出可以缩⼩。
    • ⽤例:跑步运动员类-会跑步,⼦类⻓跑运动员-会跑步且擅⻓⻓跑,⼦类短跑运动员-会跑步且擅⻓短跑。
  • 依赖倒置原则(Dependence Inversion Principle)

    • ⾼层模块不应该依赖低层模块,两者都应该依赖其抽象.不可分割的原⼦逻辑就是低层模式,原⼦逻辑组装成的就是⾼层模块。
    • 模块间依赖通过抽象(接⼝)发⽣,具体类之间不直接依赖。
    • 使⽤建议:每个类都尽量有抽象类,任何类都不应该从具体类派⽣。尽量不要重写基类的⽅法。结合⾥⽒替换原则使⽤。
    • ⽤例:奔驰⻋司机类–只能开奔驰;司机类–给什么⻋,就开什么⻋;开⻋的⼈:司机–依赖于抽象。
  • 迪⽶特法则(LawofDemeter),⼜叫“最少知道法则”

    • 尽量减少对象之间的交互,从⽽减⼩类之间的耦合。⼀个对象应该对其他对象有最少的了解。 对类的低耦合提出了明确要求:1.只和直接的朋友交流,朋友之间也是有距离的。⾃⼰的就是⾃⼰的。(如果⼀个⽅法放在本类中,既不增加类间关系,也对本类不产⽣负⾯影响,那就放置在本类中)
    • ⽤例:⽼师让班⻓点名–⽼师给班⻓⼀个名单,班⻓完成点名勾选,返回结果,⽽不是班⻓点名,⽼师勾选。
  • 接⼝隔离原则(Interface Segregation Principle );

    • 客⼾端不应该依赖它不需要的接⼝,类间的依赖关系应该建⽴在最⼩的接⼝上。
    • 使⽤建议:接⼝设计尽量精简单⼀,但是不要对外暴露没有实际意义的接⼝。
    • ⽤例:修改密码,不应该提供修改⽤⼾信息接⼝,⽽就是单⼀的最⼩修改密码接⼝,更不要暴露数据库操作。

从整体上来理解六⼤设计原则,可以简要的概括为⼀句话,用抽象构建框架,用实现扩展细节,每⼀条设计原则对应⼀条注意事项:

  • 单⼀职责原则告诉我们实现类要职责单⼀;
  • 开闭原则是总纲,告诉我们要对扩展开放,对修改关闭;
  • ⾥⽒替换原则告诉我们不要破坏继承体系;
  • 依赖倒置原则告诉我们要⾯向接⼝编程;
  • 迪⽶特法则告诉我们要降低耦合;
  • 接⼝隔离原则告诉我们在设计接⼝的时候要精简单⼀。

单例模式

一个类只创建一个对象。 该设计模式可以保证系统中该类只有⼀个实例化对象,并提供⼀个访问它的全局访问点,该实例被所有程序模块共享。⽐如在某个服务器程序中,该服务器的配置信息存放在⼀个⽂件中,这些配置数据由⼀个单例对象统⼀读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种⽅式简化了在复杂环境下的配置管理。

饿汉模式:

程序启动时就会创建⼀个唯⼀的实例对象。因为单例对象已经确定,所以⽐较适⽤于多线程环境中,多线程获取单例对象不需要加锁,可以有效的避免资源竞争,提⾼性能。

#include <iostream>
class Singleton
{
private:static Singleton _eton;Singleton() : _data(99){std::cout << "单例对象构造" << std::endl;}Singleton(const Singleton&) = delete; Singleton& operator= (Singleton&) = delete;~Singleton(){};                    
private:int _data;
public:static Singleton &getInstance(){return _eton;}int getData(){return _data;}
};
Singleton Singleton::_eton;int main()
{std::cout << Singleton::getInstance().getData() << std::endl;return 0;
}

image-20240828100333824

懒汉模式:

懒加载,延迟加载的思想,一个对象在用的时候在进行实例化。 如果单例对象构造特别耗时或者耗费资源(加载插件、加载⽹络资源等), 可以选择懒汉模式, 在第⼀次使⽤的时候才创建对象。

  • 这⾥介绍的是《Effective C++》⼀书作者ScottMeyers 提出的⼀种更加优雅简便的单例模式 Meyers’Singleton in C++。
  • C++11 Static local variables特性以确保C++11起,静态变量将能够在满⾜ thread-safe 的前提下唯⼀地被构造和析构
#include <iostream>
class Singleton
{
private:Singleton() : _data(99){std::cout << "单例对象构造" << std::endl;}Singleton(const Singleton &) = delete; Singleton& operator= (Singleton&) = delete;~Singleton(){};                        
private:int _data; 
public:static Singleton &getInstance(){static Singleton _eton;return _eton;}int getData(){return _data;}
};
int main()
{std::cout << Singleton::getInstance().getData() << std::endl; return 0;
}

注释 27 行:

image-20240828102633143

没注释 27 行:

image-20240828102543257

工厂模式

⼯⼚模式是⼀种创建型设计模式,它提供了⼀种创建对象的最佳⽅式。在⼯⼚模式中,我们创建对象时不会对上层暴露创建逻辑,⽽是通过使⽤⼀个共同结构来指向新创建的对象,以此实现创建-使⽤的分离

工厂模式可以分为:

  1. 简单工厂模式

简单⼯⼚模式实现由⼀个⼯⼚对象通过类型决定创建出来指定产品类的实例。

假设有个⼯⼚能⽣产出⽔果,当客⼾需要产品的时候明确告知⼯⼚⽣产哪类⽔果,⼯⼚需要接收⽤⼾提供的类别信息,当新增产品的时候,⼯⼚内部去添加新产品的⽣产⽅式。

#include <iostream>
#include <memory>// 水果 -- 抽象类
class Fruit
{
public:virtual void name() = 0;
};// 水果产品 -- 苹果 香蕉
class Apple : public Fruit
{
public:void name() override{std::cout << "我是一个苹果" << std::endl;}
};class Banana : public Fruit
{
public:void name() override{std::cout << "我是一个香蕉" << std::endl;}
};// 水果工厂
class FruitFactory
{
public:static std::shared_ptr<Fruit> create(const std::string &name){if (name == "苹果")return std::make_shared<Apple>();elsereturn std::make_shared<Banana>();}
};int main()
{std::shared_ptr<Fruit> fruit = FruitFactory::create("苹果");fruit->name();fruit = FruitFactory::create("香蕉");fruit->name();return 0;
}

image-20240828115618131

  • 优点:简单粗暴,直观易懂。

  • 缺点:

    • 生产产品的代码都写在一起,产品太多会导致代码量庞⼤
    • 扩展性差,当新增产品时,需要修改工厂类新增新产品的产品创建逻辑,违背了开闭原则。
  1. 工厂方法模式

在简单⼯⼚模式下新增多个⼯⼚,多个产品,每个产品对应⼀个⼯⼚。假设现在有 A、B 两种产品,则开两个⼯⼚,⼯⼚ A 负责⽣产产品 A,⼯⼚ B 负责⽣产产品 B,用户只知道产品 的⼯⼚名,⽽不知道具体的产品信息,⼯⼚不需要再接收客户的产品类别,⽽只负责⽣产产品。

// 工厂方法模式遵循了开闭原则,扩展性好
#include <iostream>
#include <memory>// 水果类 -- 抽象类
class Fruit
{
public:virtual void name() = 0;
};class Apple : public Fruit
{
public:void name() override{std::cout << "我是一个苹果" << std::endl;}
};// 水果产品 -- 苹果 香蕉
class Banana : public Fruit
{
public:void name() override{std::cout << "我是一个香蕉" << std::endl;}
};// 水果工厂 -- 抽象类
class FruitFactory
{
public:virtual std::shared_ptr<Fruit> create() = 0;
};// 苹果工厂
class AppleFactory : public FruitFactory
{
public:virtual std::shared_ptr<Fruit> create() override // 用来重写基类的同名虚函数{return std::make_shared<Apple>();}
};
// 香蕉工厂
class BananFactory : public FruitFactory
{
public:virtual std::shared_ptr<Fruit> create() override // 用来重写基类的同名虚函数{return std::make_shared<Banana>();}
};int main()
{std::shared_ptr<FruitFactory> ff(new AppleFactory()); // 创建一个苹果工厂std::shared_ptr<Fruit> fruit = ff->create();          // 生产苹果fruit->name();ff.reset(new BananFactory()); // 重新设置工厂为香蕉工厂fruit = ff->create();         // 生产香蕉fruit->name();                return 0;return 0;
}

image-20240828143717281

但是⼯⼚⽅法模式每次增加⼀个产品时,都需要增加⼀个具体产品类和⼯⼚类,这会使得系统中类的个数成倍增加的,代码就会比较臃肿。

  1. 抽象工厂模式

⼯⼚⽅法模式通过引⼊⼯⼚等级结构,解决了简单⼯⼚模式中⼯⼚类职责太重的问 题,但由于⼯⼚⽅法模式中的每个⼯⼚只⽣产⼀类产品,可能会导致系统中存在⼤量的⼯⼚类,势必会增加系统的开销。

此时,我们可以考虑将⼀些相关的产品组成⼀个产品族,由同⼀个⼯⼚来统⼀⽣产,这就是抽象⼯⼚模式的基本思想。

#include<iostream>
#include<memory>class Fruit
{
public:Fruit() {}virtual void name() = 0;
};class Apple : public Fruit
{
public:Apple() {}virtual void name(){std::cout << "我是一个苹果" << std::endl;}
};class Banana : public Fruit
{
public:Banana() {}virtual void name(){std::cout << "我是一个香蕉" << std::endl;}
};class Animal
{
public:virtual void name() = 0;
};class Lamp : public Animal
{
public:virtual void name(){std::cout << "我是山羊" << std::endl;}
};class Dog : public Animal
{
public:virtual void name(){std::cout << "我是小狗" << std::endl;}
};class Factory
{
public:virtual std::shared_ptr<Fruit> GetFruit(const std::string &name) = 0;virtual std::shared_ptr<Animal> GetAnimal(const std::string &name) = 0;
};class FruitFactory : public Factory
{
public:// 纯虚函数必须重写virtual std::shared_ptr<Animal> GetAnimal(const std::string &name){return std::shared_ptr<Animal>(); // 返回一个空对象}virtual std::shared_ptr<Fruit> GetFruit(const std::string &name){if(name ==  "苹果"){return std::make_shared<Apple>();}else if(name == "香蕉"){return std::make_shared<Banana>();}}
};class AnimalFactory : public Factory
{
public:virtual std::shared_ptr<Fruit> GetFruit(const std::string &name){return std::shared_ptr<Fruit>();}virtual std::shared_ptr<Animal> GetAnimal(const std::string &name){if(name ==  "山羊"){return std::make_shared<Lamp>();}else if(name == "小狗"){return std::make_shared<Dog>();}}
};class FactoryProducer
{
public:static std::shared_ptr<Factory> getFactory(const std::string &name){if(name == "动物"){return std::make_shared<AnimalFactory>();}else {return std::make_shared<FruitFactory>();}}
};int main()
{std::shared_ptr<Factory> ff = FactoryProducer::getFactory("水果");std::shared_ptr<Fruit> f = ff->GetFruit("香蕉");f->name();f = ff->GetFruit("苹果");f->name();std::shared_ptr<Factory> af = FactoryProducer::getFactory("动物");std::shared_ptr<Animal> a = af->GetAnimal("山羊");a->name();a = af->GetAnimal("小狗");a->name();return 0;
}

image-20240828153345509

抽象⼯⼚模式适⽤于⽣产多个⼯⼚系列产品的设计模式,增加新的产品时,需要对原有系统进⾏较⼤的修改,甚⾄需要修改抽象层(例如 Factory)的代码,违背了开闭原则

建造者模式

建造者模式是⼀种创建型设计模式, 使⽤多个简单的对象⼀步⼀步构建成⼀个复杂的对象(可能会按照一定的顺序才能构建成产品),能够将⼀个复杂的对象的构建与它的表⽰分离,提供⼀种创建对象的最佳⽅式。主要⽤于解决对象的构建过于复杂的问题。

建造者模式主要基于五个类实现:

  • 抽象产品类
  • 具体产品类:⼀个具体的产品对象类 – 对抽象产品类做一个具象化的实现
  • 抽象 Builder 类:创建⼀个产品对象所需的各个部件的抽象接⼝
  • 具体产品 Builder 类:实现抽象接⼝,构建各个部件
  • 指挥者 Director 类:统⼀组建过程,提供给调⽤者使⽤,通过指挥者类来构造产品
#include <iostream>
#include <string>
#include <memory>// 抽象产品类
class Computer
{
public:Computer() {}void setBoard(const std::string &board){_board = board;}void setDisplay(const std::string &display){_display = display;}void showParamaters(){std::string computer = "Computer: \n  \n";computer += "\tboard = " + _board + ",\n";computer += "\tdisplay = " + _display + ",\n";computer += "\tOS = " + _os + ",\n  \n";std::cout << computer << std::endl;}virtual void setOs() = 0;// 构造电脑所需要的零部件
protected: // 用 private,public 继承看不见,所以写 protectedstd::string _board;std::string _display;std::string _os;
};// 具体产品类
class MacBook : public Computer
{
public:void setOs() override{_os = "MAC OS x12";}
};// 抽象 Builder 类
class Builder
{
public:virtual void buildBoard(const std::string &board) = 0;virtual void buildDisplay(const std::string &display) = 0;virtual void buildOs() = 0;virtual std::shared_ptr<Computer> build() = 0;
};// 具体产品 Builder 类
class MacBookBuilder : public Builder
{
public:MacBookBuilder() : _computer(new MacBook()) {}void buildBoard(const std::string &board){_computer->setBoard(board);}void buildDisplay(const std::string &display){_computer->setDisplay(display);}void buildOs(){_computer->setOs();}std::shared_ptr<Computer> build(){return _computer;}private:std::shared_ptr<Computer> _computer;
};// 指挥者 Director 类
class Director
{
public:Director(Builder* builder): _builder(builder) {}void construct(const std::string& board, const std::string& display){_builder->buildBoard(board);_builder->buildDisplay(display);_builder->buildOs();}
private:std::shared_ptr<Builder> _builder;	
};int main()
{Builder* builder = new MacBookBuilder(); // 构造一个具体产品类std::unique_ptr<Director> director(new Director(builder)); // 构造一个指挥者类director->construct("华硕主板", "三星显示器");std::shared_ptr<Computer> computer = builder->build();computer->showParamaters();return 0;
}

image-20240903083755162

代理模式

代理模式指代理控制对其他对象的访问,也就是代理对象控制对原对象的引⽤。在某些情况下,⼀个对象不适合或者不能直接被引⽤访问,⽽代理对象可以在客⼾端和⽬标对象之间起到中介的作⽤。

代理模式的结构包括⼀个是真正的你要访问的对象(⽬标类)、⼀个是代理对象。⽬标对象与代理对象实现同⼀个接⼝(代理对象的接口中会包含目标对象的接口,最终使用的是代理对象的接口,所以说实现同一个接口),先访问代理类再通过代理类访问⽬标对象。代理模式分为静态代理、动态代理:

  • 静态代理指的是,在编译时就已经确定好了代理类和被代理类的关系。也就是说,在编译时就已经确定了代理类要代理的是哪个被代理类。-- (
  • 动态代理指的是,在运⾏时才动态⽣成代理类,并将其与被代理类绑定。这意味着,在运⾏时才能
    确定代理类要代理的是哪个被代理类 – (涉及很多其他知识,不讲)。

以租房为例,房东将房⼦租出去,但是要租房⼦出去,需要发布招租启⽰,带⼈看房,负责维修,这些⼯作中有些操作并⾮房东能完成,因此房东为了图省事,将房⼦委托给中介进⾏租赁。

代理模式 – 静态代理:

#include <iostream>// 抽象租房类 
class RentHouse
{
public:virtual void rentHouse() = 0;
};// 房东
class Landlord : public RentHouse
{
public:void rentHouse() override{std::cout << "将房子租出去\n";}
};// 中介代理类 -- 对租房的功能加强,在房东租房的基础之上,额外完成其他功能 
class Intermediary : public RentHouse
{
public:void rentHouse() override{std::cout << "发布招租启示\n";std::cout << "带人看房\n";_landlord.rentHouse();std::cout << "负责租后维修\n";}private:Landlord _landlord;
};int main()
{Intermediary intermediary;intermediary.rentHouse(); // 通过中介来出租房子return 0;
}

image-20240829103733973

日志系统框架设计

日志系统:
作用:将一条消息,进行格式化为指定格式的字符串后,写入到指定位置

  1. 日志要写入指定位置(标准输出,指定文件, 滚动文件等 – 可扩展 – 可以写入到其他扩展位置)
    日志系统需要支持将日志消息落地到不同的位置—多落地方向
  2. 日志写入指定位置,支持不同的写入方式(同步,异步)
    同步:业务线程自己负责日志的写入(流程简单,但是有可能会因为阻塞导致效率降低) 异步:业务线程将日志放入内存缓冲区,让其他异步线程负责将日志写入指定位置
  3. 日志输出以日志器为单位,支持多日志器(不同的项目组有不同的日志输出策略)

本项⽬实现的是⼀个多⽇志器⽇志系统,主要实现的功能是让程序员能够轻松的将程序运⾏⽇志信息落地到指定的位置,且⽀持同步与异步两种⽅式的⽇志落地⽅式。项⽬的框架设计将项⽬分为以下⼏个模块来实现。

模块划分

日志等级模块

枚举出日志分为多少个等级—对不同的日志有不同等级标记–以便于控制输出

  • OFF:关闭
  • DEBUG:调试,调试时的关键信息输出。
  • INFO:提示,普通的提⽰型⽇志信息。
  • WARN:警告,不影响正常运⾏(运行不会有错误),但是需要注意⼀下的⽇志。
  • ERROR:错误,程序运⾏出现错误,但是还能继续运行。这些错误可能会影响部分功能,但是通常不会导致整个程序崩溃的⽇志,如文件读写失败,数据库连接失败。
  • FATAL:致命,⼀般是代码异常导致程序⽆法继续推进运⾏的⽇志,即将崩溃或者已经崩溃。

日志消息模块

封装一条日志所需的各种要素(时间,线程ID,文件名,行号,日志等级,消息主体…)

  • 时间:描述本条⽇志的输出时间。
  • 线程ID:描述本条⽇志是哪个线程输出的。
  • ⽇志等级:描述本条⽇志的等级。
  • ⽇志数据:本条⽇志的有效载荷数据。
  • ⽇志⽂件名:描述本条⽇志在哪个源码⽂件中输出的。
  • ⽇志⾏号:描述本条⽇志在源码⽂件的哪⼀⾏输出的。

日志消息格式化模块

按照指定的格式,对于日志消息中关键要素进行组织,最终得到一个指定格式的字符串

系统默认的输出格式: [%d{%H:%M:%S}]%T[%t]%T[%p]%T[%c]%T%f:%l%T%m%n

[12:38:45] [12345678] [FATAL] [root] main.c:178 套接字创建失败…\n

  • %d{%H:%M:%S}:表⽰⽇期时间,花括号中的内容表示日期时间的格式。
  • %T:表⽰制表符缩进。
  • %t:表⽰线程ID。
  • %p:表⽰⽇志级别。
  • %c:表⽰⽇志器名称,不同的项目组可以创建⾃⼰的⽇志器进⾏⽇志输出,⼩组之间互不影响。
  • %f:表⽰⽇志输出时的源代码⽂件名。
  • %l:表⽰⽇志输出时的源代码⾏号。
  • %m:表⽰⽇志有效载荷数据 。
  • %n:表⽰换行。
  • 设计思想:设计不同的⼦类,不同的⼦类从⽇志消息中取出不同的数据进⾏处理。

日志落地模块

负责对日志消息进行指定方向的写入输出

  • 标准输出:表⽰将⽇志进⾏标准输出的打印。
  • ⽇志⽂件输出:表⽰将⽇志写⼊指定的⽂件末尾。
  • 滚动⽂件输出:当前以⽂件⼤⼩进⾏控制,当⼀个⽇志⽂件⼤⼩达到指定⼤⼩,则切换下⼀个⽂件进⾏输出 。
  • 后期,也可以扩展远程⽇志输出,创建客⼾端,将⽇志消息发送给远程的⽇志分析服务器。
  • 设计思想:设计不同的⼦类,不同的⼦类控制不同的⽇志落地⽅向。

日志器模块

对上面几个模块的整合,⽤⼾通过⽇志器进⾏⽇志的输出,有效降低⽤⼾的使⽤难度;包含有:⽇志落地模块对象,⽇志消息格式化模块对象,⽇志限制输出等级

  • 同步日志器模块—完成日志的同步输出功能。
  • 异步日志器模块—完成日志的异步输出功能

异步线程模块

负责异步日志的实际落地输出功能

  • 实现对⽇志的异步输出功能,⽤⼾只需要将输出⽇志任务放⼊任务池,异步线程负责⽇志的落地输出功能,提供了更加⾼效的⾮阻塞的⽇志输出。

单例的日志器管理模块

对日志进行全局的管理,以便于能够在项目的任何位置获取指定的日志器进行日志输出

  • 为了降低项⽬开发的⽇志耦合,不同的项⽬组可以有⾃⼰的⽇志器来控制输出格式以及落地⽅向,因此本项⽬是⼀个多⽇志器的⽇志系统。
  • 管理模块就是对创建的所有⽇志器进⾏统⼀管理。并提供⼀个默认⽇志器,提供标准输出的⽇志输出。

模块关系图

在这里插入图片描述

代码设计

实用类设计

提前完成一些零碎的功能接口:

  • 获取系统时间
  • 判断文件是否存在
  • 获取文件的所在目录路径
  • 创建目录

util.hpp:

# pragma once#include <iostream>
#include <ctime>
#include <sys/stat.h>
#include <sys/types.h>
#include <string>namespace log
{namespace util{class Date{public://获取系统时间static size_t now(){return (size_t)time(nullptr);}};class File{public:// 判断文件是否存在static bool exists(const std::string& pathname){// stat 获取文件属性,获取成功,说明文件存在;获取失败,则文件不存在struct stat st;if (stat(pathname.c_str(), &st) < 0) return false;return true;}// 获取文件的所在目录路径static std::string path(const std::string& pathname){// ./abc/a.txt 其实也就是获取最后一个 / 的位置,然后从开头进行截取size_t pos = pathname.find_last_of("/\\"); // 这个 find 调用在 windows 和 linux 下都可以,linux 下路径分隔符为 /;而 \ 为转义字符// 要表示一个 \ 需要两个 \,也就是查找 / 和 \ 中的任意一个,linux 下路径不会出现 \ ,所以这里没问题if (pos == std::string::npos) return ".";  // 如果没有找到,那么证明这个文件就在当前的目录return pathname.substr(0, pos + 1); // 把最后一个 / 也截进去}// 创建目录static void createDirectory(const std::string& pathname){// ./abc/bcd/cde// pos 是 / 的位置,idx是查找的起始位置size_t pos = 0, idx = 0;while (idx < pathname.size()){// 查找idx 位置起第一个 /size_t pos = pathname.find_first_of("/\\", idx);if (pos == std::string::npos){ // 创建文件夹,pathname.c_str()表示路径名,0777表示权限mkdir(pathname.c_str(), 0755);return;}// 截取从开始到/的目录std::string parent_dir = pathname.substr(0, pos + 1);// 目录存在就跳过,找下一个if (exists(parent_dir) == true){idx = pos + 1;continue;}// 目录不存在,创建目录mkdir(parent_dir.c_str(), 0777);idx = pos + 1;}}};}
}

测试:test.cc

#include "util.hpp"int main()
{std::cout << log::util::Date::now() << std::endl;std::string pathname = "./abc/bcd/a.txt";log::util::File::createDirectory(log::util::File::path(pathname));return 0;
}

image-20240830072836613

日志等级模块设计

  • UNKNOW=0,未知等级的日志

  • DEBUG ,调试等级的日志

  • INFO ,提示等级的日志

  • WARN ,警告等级的日志

  • ERROR ,错误等级的日志

  • FATAL ,致命错误等级的日志

  • OFF,关闭所有⽇志输出

每一个项目中都会设置一个默认的日志输出等级,只有输出的日志等级大于等于默认限制等级的时候才可以进行输出。

提供一个接口,将对应等级的枚举,转换为一个对应的字符串

level.hpp:

/*
1. 定义枚举类,枚举出日志等级
2. 提供转换接口,将枚举转换为对应的字符串
*/#pragma oncenamespace log
{class LogLevel{public:enum class value{UNKNOW = 0,DEBUG,INFO,WARN,ERROR,FATAL,OFF};// 转换接口static const char* toString(LogLevel::value level){switch (level){case LogLevel::value::DEBUG: return "DEBUG";case LogLevel::value::INFO: return "INFO";case LogLevel::value::WARN: return "WARN";case LogLevel::value::ERROR: return "ERROR";case LogLevel::value::FATAL: return "FATAL";case LogLevel::value::OFF: return "OFF";}return "UNKOWN";}};
}

日志消息类设计

意义:存储一条日志消息所需的各项要素

  • 日志的输出时间 – 用于过滤日志输出时间
  • 日志等级 – 用于进行日志过滤分析
  • 源文件名称
  • 源代码行号 – 名称和行号:用于定位出错的代码位置
  • 线程ID – 用于过滤出错的线程
  • 日志主体消息
  • 日志器名称 – 支持多日志器的同时使用

message.hpp:

// 定义日志消息类,进行日志信息的存储#pragma once#include"util.hpp"
#include"level.hpp"
#include <iostream>
#include <string>
#include <thread>namespace log
{struct LogMsg{time_t _ctime;          // 日志产生的时间戳LogLevel::value _level; // 日志等级size_t _line;           // 行号std::thread::id _tid;   // 线程idstd::string _file;      // 文件名std::string _logger;    // 日志器名称std::string _payload;   // 有效载荷数据// 构造函数LogMsg(LogLevel::value level,size_t line,const std::string file,const std::string logger,const std::string msg) : _ctime(util::Date::now()),_level(level),_line(line),_tid(std::this_thread::get_id()),_file(file),_logger(logger),_payload(msg){}};
}

日志格式化类设计

意义:对日志消息进行格式化,并且组织成指定格式的字符串。

%d ⽇期
%T 缩进
%t 线程id
%p ⽇志级别
%c ⽇志器名称
%f ⽂件名
%l ⾏号
%m ⽇志消息
%n 换⾏

[2024-07-09 17:04][root][1234567][main.c:99][FATAL]:\t创建套接字失败…\n

格式化字符串控制了日志的输出格式;定义格式化字符,是为了让日志系统进行日志格式化更加的灵活方便。

⽇志格式化(Formatter)类包含:

  • 格式化字符串:用户定义的输出格式
  • 格式化子项数组:对格式化字符串进行解析,保存了日志信息要素的顺序 – 不同的格式化子项,会从日志消息中取出指定的元素,转化为字符串。

格式化子项:

  • 其他信息(非格式化字符)子项:[
  • 日期子项:%H%M%S
  • 其他信息子项:]
  • 其他信息子项:[
  • 文件名子项:main.c
  • 其他信息子项::
  • 行号信息子项:99
  • 其他信息子项:]
  • 消息主体子项:吃饭睡觉打豆豆
  • 换行子项:\n

[12:40:50][main.c:99]吃饭睡觉打豆豆\n

格式化子项的实现思想:从日志消息中取出指定的元素,追加到一块内存空间中。

设计思想:

  1. 抽象出一个格式化子项的基类

  2. 基于基类,派生出不同的格式化子项子类:

    主体消息、日志等级、时间子项、文件名、行号、日志器名称、线程ID、制表符、换行、非格式化的原始字符串。

    这样就可以在父类中定义父类指针(智能指针)的数组,指向不同的格式化子项子类的对象。

FormatItem类主要负责⽇志消息⼦项的获取及格式化;其包含以下⼦类:

  • MsgFormatItem:表⽰要从LogMsg中取出有效⽇志数据
  • LevelFormatItem:表⽰要从LogMsg中取出⽇志等级
  • NameFormatItem:表⽰要从LogMsg中取出⽇志器名称
  • ThreadFormatItem:表⽰要从LogMsg中取出线程ID
  • TimeFormatItem:表⽰要从LogMsg中取出时间戳并按照指定格式进⾏格式化
  • CFileFormatItem:表⽰要从LogMsg中取出源码所在⽂件名
  • CLineFormatItem:表⽰要从LogMsg中取出源码所在⾏号
  • TabFormatItem:表⽰⼀个制表符缩进
  • NLineFormatItem:表⽰⼀个换⾏
  • OtherFormatItem:表⽰⾮格式化的原始字符串

格式化子项类:

# pragma once#include "level.hpp"
#include "message.hpp"
#include <memory>namespace log
{class FormatItem{public:using ptr = std::shared_ptr<FormatItem>;virtual void format(std::ostream& cout, const LogMsg &msg) = 0;};// 派生格式化子项子类 -- 主体消息、日志等级、时间子项、文件名、行号、日志器名称、线程ID、制表符、换行、 其他// 主体消息class MsgFormatItem : public FormatItem{public:void format(std::ostream& out, const LogMsg& msg) override{out << msg._payload;}};// 日志等级class LevelFormatItem : public FormatItem{public:void format(std::ostream& out, const LogMsg& msg) override{out << LogLevel::toString(msg._level);}};//时间子项class TimeFormatItem:public FormatItem{public:// 设置时间的默认格式TimeFormatItem(const std::string &fmt = "%H:%M:%S") : _time_fmt(fmt) {}//虚函数进行重写void format(std::ostream& out, const LogMsg& msg) override{struct tm t; // 时间结构体localtime_r(&msg._ctime, &t); // 把时间写入 t 中char tmp[32] = { 0 };strftime(tmp, 31, _time_fmt.c_str(), &t); // 把格式化的时间写入 tmp 中out<<tmp;}private:std::string _time_fmt; // 时间格式};// 文件名class FileFormatItem : public FormatItem{public:// 虚函数进行重写void format(std::ostream &out, const LogMsg &msg) override{out << msg._file;}};// 行号class LineFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg) override{out << msg._line;}};// 日志器名称class LoggerFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg) override{out << msg._logger;}};// 线程IDclass ThreadFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg) override{out << msg._tid;}};// 制表符class TabFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg) override{out << "\t";}};// 换行class NLineFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg) override{out << "\n";}};// 其他 -- 输出原始字符串 -- abcdef[%d{...}] -- abcdef[ 就是原始字符串,输出class OtherFormatItem : public FormatItem{public:OtherFormatItem(const std::string &str) : _str(str) {}void format(std::ostream &out, const LogMsg &msg) override{out << _str;}private:std::string _str;};
}

日志格式化类:

parsePattern 函数思想:

image-20240906093915781

补充说明:

  • 原始字符串包含两种,就是 abcd 这种的直接原始字符串,还有 %% 的原始 % 字符串。原始字符串处理完毕后,遇到 % ,则 % 的下一个位置一定是格式化字符。
  • 处理一个格式化字符,会连带着它的子规则一起处理

image-20240906094255944

(nullptr 和 null 就是代表 string 为空 “”)

class Formatter{public:Formatter(const std::string& pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n") : _pattern(pattern) {assert(parsePattern()); // 解析必须成功,不能失败}// 数据放入 io 流中,io 流进行处理void format(std::ostream& out, const LogMsg& msg){// 遍历格式化子项数组for (auto &item : _items){item->format(out, msg); // 从 msg 取信息到 out 中}}// 对 LogMsg 进行格式化,返回一个格式化的字符串std::string format(const LogMsg& msg){std::stringstream ss;format(ss, msg); // 调用的是上面的 format 重载,void format(std::ostream& out, LogMsg& msg)return ss.str();}private://对格式化规则字符串进行解析bool parsePattern(){// 1. 格式化规则字符串解析//abcd[%d{%H:%M:%S}][%p]%T%m%nstd::vector<std::pair<std::string, std::string>> fmt_order;size_t pos = 0;std::string key, val;while (pos < _pattern.size()){// 处理原始字符串 -- 判断是否是 %,不是一定是原始字符if (_pattern[pos] != '%'){val.push_back(_pattern[pos++]);continue;}// 到这里就代表 pos 位置是 % 字符,看后一个是不是 %,%% 处理为一个原始 % 字符if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%'){val.push_back('%');pos += 2;continue;}// 到这里,说明 % 后是个格式化字符;这时候原始字符串已经处理完毕了,把 val push 进去if (val.empty() == false) // 可能出现开头就是格式化字符的情况,所以加判断,如果 val 为空,则不添加 -- 是格式化字符的话,上面不会处理,val 是空的fmt_order.push_back(std::make_pair("", val));val.clear();// 这时候是格式化字符的处理pos += 1; // pos 原本指向 % 位置,+1指向格式化字符的位置if (pos == _pattern.size()){std::cout << "%之后,没有对应的格式化字符!\n";return false;}key = _pattern[pos]; // %d,写入的就是 d,因为上面 pos +1 了pos += 1; // 再往后走一个,看是否有 {,这时候 pos 指向格式化字符后的位置bool error_flag = false;if (pos < _pattern.size() && _pattern[pos] == '{'){// 处理子串pos += 1; // 这时候 pos 指向子规则的起始位置,即 { 的下一个位置while (pos < _pattern.size() && _pattern[pos] != '}'){val.push_back(_pattern[pos++]); // 把子项逐字符放入,带 %}if (pos == _pattern.size()) // 走到末尾跳出循环,则代表没有找到 },代表格式错误{std::cout << "子规则 {} 匹配出错!\n";return false;}pos += 1; // 这时候 pos 指向 } 位置,往后走一步,就走到下次处理的新位置}fmt_order.push_back(std::make_pair(key, val));key.clear();val.clear();}   // 2. 根据解析得到的数据初始化格式化子项数组for (auto& it : fmt_order){_items.push_back(createItem(it.first, it.second));}return true;}// key -- 格式化字符(关键字),val -- 关键字对应值// 例如 %d 对应的子串就是 %H:%M:%S,'[' 则没有 key,就只有 value('[' 本身),这时返回的对象,就用自己填充// 根据不同的格式化字符创建不同的格式化子项对象FormatItem::ptr createItem(const std::string& key,  const std::string& value){if (key == "d") return std::make_shared<TimeFormatItem>(value);if (key == "t")return std::make_shared<ThreadFormatItem>();if (key == "c")return std::make_shared<LoggerFormatItem>();if (key == "f")return std::make_shared<FileFormatItem>();if (key == "l")return std::make_shared<LineFormatItem>();if (key == "p")return std::make_shared<LevelFormatItem>();if (key == "T")return std::make_shared<TabFormatItem>();if (key == "m")return std::make_shared<MsgFormatItem>();if (key == "n")return std::make_shared<NLineFormatItem>();if (key.empty())return std::make_shared<OtherFormatItem>(value); std::cout << "没有对应的格式化字符串:%" << key << std::endl;abort();return FormatItem::ptr();}private:std::string _pattern; // 格式化规则字符串std::vector<FormatItem::ptr> _items; // 格式化子项数组,元素是智能指针};

测试代码:

#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"int main()
{log::LogMsg msg(log::LogLevel::value::INFO, 53, "main.c", "root", "格式化功能测试...");log::Formatter fmt;// 边缘情况测试// log::Formatter fmt("abc%%ab%g%g%gc[%d{%H:%M:%S}]  %m%n"); // 没有 %g 格式字符// log::Formatter fmt("abc%%abc[%d{%H:%M:%S}]%m%n");// 测试 %%// log::Formatter fmt("abc%%abc[%d{%H:%M:%S}]%m%n{"); // 测试花括号// log::Formatter fmt("abc%%abc[%d{%H:%M:%S}]%m%"); // 末尾只带 %,没有格式化字符std::string str = fmt.format(msg);std::cout << str << std::endl;return 0;
}

日志落地类设计 – 简单工厂模式

功能:将格式化后的日志消息字符串,输出到指定的位置

扩展:支持同时将日志落地到不同的位置

位置分类:

  1. 标准输出 – 不常用,测试的时候用
  2. 指定文件(事后进行日志分析)
  3. 滚动文件(文件按时间/大小进行滚动切换)

扩展:支持落地方向的扩展

用户可以自己编写一个新的落地模块,将日志进行其他方向的落地

实现思想:

  1. 抽象出落地模块类
  2. 不同落地方向从基类进行派生(使用基类指针,指向子类对象,就可以调用子类对象的接口进行扩展)
  3. 使用工厂模式进行创建与表示的分离

框架:

amespace log
{class LogSink{public:using ptr = std::shared_ptr<LogSink>;LogSink() {}virtual ~LogSink() {}virtual void log(const char* data, size_t len) = 0; };// 落地方向:标准输出class StdoutSink : public LogSink{public:// 将日志消息写入到标准输出void log(const char* data, size_t len){}};// 落地方向:指定文件class FileSink : public LogSink{public:// 构造时传入文件名,并打开文件,将操作句柄 -- _ofs 管理起来FileSink(const std::string& pathname);void log(const char* data, size_t len){}private:std::string _filename;std::ofstream _ofs; // 输出文件的操作句柄};// 落地方向:滚动文件(以大小滚动)class RollBySizeSink{public:RollBySizeSink(const std::string &basename, size_t max_size);//将日志消息写入到文件,写入前判断文件大小,超过了最大大小,就要切换文件void log(const char* data, size_t len){}private:// 进行大小判断,超过指定大小则创建新文件void createNewFile();private:// 基础文件名 + 扩展文件名(以时间生成 -- 直观,好分析) --》 组成一个实际的当前输出文件名std::string _basename;  // ./log/base-  --> ./log/base-20020809132356.logstd::ofstream _ofs;size_t _max_fsize; // 记录最大大小,当前文件超过了这个大小就要切换文件size_t _cur_size; // 记录当前文件已经写入的大小};// 日志落地的工厂class SinkFackory{};
}

code:

/*日志落地模块的实现
1.抽象落地基类
2.派生子类(根据不同的落地方向进行派生)
3.使用工厂模式进行创建与表示分离
*/#pragma once#include "util.hpp"
#include <iostream>
#include <memory>
#include <fstream>
#include <sstream>
#include <cassert>namespace log
{class LogSink{public:using ptr = std::shared_ptr<LogSink>;LogSink() {}virtual ~LogSink() {}virtual void log(const char* data, size_t len) = 0; };// 落地方向:标准输出class StdoutSink : public LogSink{public:// 将日志消息写入到标准输出void log(const char* data, size_t len){// 因为日志输出不一定是字符串,cout 不能指定大小,字符串有 \0 结尾,别的没有// 因此需要调用write接口,从data位置开始写,写入len长度的数据std::cout.write(data, len);}};// 落地方向:指定文件class FileSink : public LogSink{public:// 构造时传入文件名,并打开文件,将操作句柄 -- _ofs 管理起来FileSink(const std::string& pathname) : _pathname(pathname){// 1. 创建日志文件所在的目录 -- 文件路径可能不存在,以防万一先创建目录util::File::createDirectory(util::File::path(pathname));// 2. 创建并打开日志文件_ofs.open(_pathname, std::ios::binary | std::ios::app); // 二进制可写可追加权限assert(_ofs.is_open());}void log(const char* data, size_t len){_ofs.write(data, len);assert(_ofs.good());}private:std::string _pathname;std::ofstream _ofs; // 输出文件的操作句柄};// 落地方向:滚动文件(以大小滚动)class RollBySizeSink : public LogSink{public:RollBySizeSink(const std::string &basename, size_t max_size):_basename(basename), _max_fsize(max_size), _cur_fsize(0), _name_count(0){std::string pathname = createNewFile();util::File::createDirectory(util::File::path(pathname));_ofs.open(pathname, std::ios::binary | std::ios::app); assert(_ofs.is_open());}//将日志消息写入到文件,写入前判断文件大小,超过了最大大小,就要切换文件void log(const char* data, size_t len){if (_cur_fsize >= _max_fsize){_ofs.close(); // 切换文件前,关闭文件,防止内存泄漏std::string pathname = createNewFile();_ofs.open(pathname, std::ios::binary | std::ios::app);assert(_ofs.is_open());_cur_fsize = 0; }_ofs.write(data, len);assert(_ofs.good());_cur_fsize += len;}private:std::string createNewFile(){// 获取系统时间,以时间来构造文件扩展名time_t t = util::Date::now();struct tm lt;localtime_r(&t, &lt);std::stringstream filename;// 从 1900 年 1.1 开始的,所以对应的加上filename << _basename;filename << lt.tm_year + 1900;filename << lt.tm_mon + 1;filename << lt.tm_mday;filename << lt.tm_hour;filename << lt.tm_min;filename << lt.tm_sec;filename << "-";filename << _name_count++;filename << ".log";return filename.str();}private:// 基础文件名 + 扩展文件名(以时间生成 -- 直观,好分析) --> 组成一个实际的当前输出文件名size_t _name_count; // 用于区别文件名的 -- 因为创建文件可能很快,名字可能会一样std::string _basename;  // ./log/base-  --> ./log/base-20020809132356.logstd::ofstream _ofs;size_t _max_fsize; // 记录最大大小,当前文件超过了这个大小就要切换文件size_t _cur_fsize; // 记录当前文件已经写入的大小};// 日志落地的工厂//SinkType通过模板参数,可以生产我们需要的落地方式,因为不同落地方式构造的参数不一样,所以需要用到不定参class SinkFactory{public:   template<typename SinkType,typename ...Args>  static LogSink::ptr create(Args && ...args){return std::make_shared<SinkType>(std::forward<Args>(args)...);}};
}

测试代码:

#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"int main()
{// 日志落地模块的测试log::LogMsg msg(log::LogLevel::value::INFO, 53, "main.c", "root", "格式化功能测试...");log::Formatter fmt;std::string str = fmt.format(msg);// 设置落地方向log::LogSink::ptr stdout_lsp = log::SinkFactory::create<log::StdoutSink>();                                    // 标准输出落地log::LogSink::ptr file_lsp = log::SinkFactory::create<log::FileSink>("./logfile/test.log");                    // 文件落地方式log::LogSink::ptr roll_lsp = log::SinkFactory::create<log::RollBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式,大小为 1 兆// 通过指针去控制打印的日志stdout_lsp->log(str.c_str(), str.size()); file_lsp->log(str.c_str(), str.size());size_t cursize = 0;size_t count = 0;// 用滚动文件的方法希望生产10个文件,一个文件 1 兆while (cursize < 1024 * 1024 * 10){std::string tmp = std::to_string(count++) + str; // 给日志标号roll_lsp->log(tmp.c_str(), tmp.size());cursize += tmp.size();}return 0;
}

结果:

image-20240909105639755

image-20240909105659711

image-20240909105632408

扩展一个以时间作为日志文件滚动切换类型的日志落地模块:

在 test.cc 中写的代码:

/*扩展一个以时间作为日志文件滚动切换类型的日志落地模块1.以时间进行文件滚动,实际上是以时间段进行滚动实现思想:以当前系统时间,取模获得时间段大小,可以得到当前时间段是第几个时间段time(nullptr)%gap;每次以当前系统时间取模,判断与当前文件的时间段是否一致,不一致代表不是同一个时间段*/
// 使用枚举来确定时间段的大小
enum class TimeGap
{GAP_SECOND,GAP_MINUTE,GAP_HOUR,GAP_DAY,
};class RollByTimeSink : public log::LogSink
{
public:RollByTimeSink(const std::string &basename, TimeGap gap_type) : _basename(basename){switch (gap_type){case TimeGap::GAP_SECOND:_gap_size = 1;break; // 以秒为时间段case TimeGap::GAP_MINUTE:_gap_size = 60;break; // 以分钟为时间段case TimeGap::GAP_HOUR:_gap_size = 3600;break; // 以小时为时间段case TimeGap::GAP_DAY:_gap_size = 3600 * 24;break; // 以天为时间段}_cur_gap = _gap_size==1?log::util::Date::now():log::util::Date::now() / _gap_size; // 获取当前是第几个时间段;前面是 _gap_size == 1 的情况// 创建文件std::string filename = createNewFile();log::util::File::createDirectory(log::util::File::path(filename));_ofs.open(filename, std::ios::binary | std::ios::app); assert(_ofs.is_open());}// 将日志消息写入到标准输出,判断当前时间是否是当前文件的时间段,不是就要切换文件。void log(const char *Date, size_t len){time_t cur = log::util::Date::now(); // 获取当前系统时间,时间戳if ((cur / _gap_size) != _cur_gap)//(每次写日志时判断当前的时间段与上次的时间段是否是一致得,一致的话就写入,不一致就创建新文件){_ofs.close();                                          // 打开文件,就必须关闭文件(这里关闭以前的文件)std::string pathname = createNewFile();                // 创建新文件_cur_gap = _gap_size==1?log::util::Date::now():log::util::Date::now() / _gap_size; // 获取当前是第几个时间段_ofs.open(pathname, std::ios::binary | std::ios::app); // 打开文件 二进制可写可追加权限assert(_ofs.is_open());                                // 打开失败就报错}_ofs.write(Date, len);assert(_ofs.good()); // 检测文件流状态和文件读写过程是否正常}protected:// 创建一个新文件,不需要用户去创建,所有我们把权限设置为私有std::string createNewFile(){// 获取系统时间,以时间来构造文件名的扩展名time_t t = log::util::Date::now();struct tm lt;localtime_r(&t, &lt);std::stringstream filename;filename << _basename;filename << lt.tm_year + 1900;filename << lt.tm_mon + 1;filename << lt.tm_mday;filename << lt.tm_hour;filename << lt.tm_min;filename << lt.tm_sec;filename << ".log";return filename.str();}
private:std::string _basename; // 基本文件名std::ofstream _ofs;    // 会默认以写的方式打开文件size_t _cur_gap;       // 当前是第几个时间段size_t _gap_size;      // 时间段的大小
};

三秒,写三个文件:

image-20240911115731393

日志器模块设计 – 涉及建造者模式

功能:对前面所有功能进行整合,向外提供接口完成不同等级日志的输出。

管理的成员:

  1. 格式化模块对象
  2. 落地模块对象
  3. 默认的日志输出限制等级(大于等于限制输出等级的日志才能输出)
  4. 互斥锁(保证日志输出过程的线程安全,不会出现交叉日志)
  5. 日志名称(日志器的唯一标识,方便查找)

提供的操作:

  • debug等级日志的输出操作(分别封装日志消息LogMsg——各个接口日志等级不同)
  • info等级日志的输出操作
  • warn等级日志的输出操作
  • error等级日志的输出操作
  • fatal等级日志的输出操作

实现:

  1. 实现Logger基类(派生出同步日志器和异步日志器)
  2. 因为两种日志器的落地方式不同,需要将落地操作给抽象出来,不同的日志器调用不同的落地操作进行日志落地
  3. 模块关联过程中使用基类指针对子类日志器对象进行日志管理和操作

当前日志系统支持同步日志&异步日志,它们的不同点在于日志的落地方式上不同:

  • 同步日志器:直接对日志消息进行输出
  • 异步日志器:先将日志消息放到缓冲区,然后异步线程进行输出

因此 :日志器类在设计的时候,先要设计一个Logger的基类,在Logger基类的基础上,继承出同步日志器(SyncLogger)和异步日志器(AsyncLoggrr)。

同步日志器

在这里插入图片描述

框架:

/*完成日志器模块1. 抽象日志器基类2. 派生出不同的子类(同步日志器嘞 & 异步日志器类)
*/#pragma once
#include "util.hpp"
#include "level.hpp"
#include "format.hpp"
#include "sink.hpp"
#include <thread>
#include <atomic>
#include <mutex>namespace log
{class Logger{public:using ptr = std::shared_ptr<Logger>;void debug(const std::string& file, size_t line, const std::string& fmt, ...);void info(const std::string& file, size_t line, const std::string& fmt, ...);void warn(const std::string& file, size_t line, const std::string& fmt, ...);void error(const std::string& file, size_t line, const std::string& fmt, ...);void fatal(const std::string& file, size_t line, const std::string& fmt, ...);protected:// 抽象接口完成实际的落地输出 -- 不同日志器会有不同的实际落地方式 virtual void log(const char* data, size_t len) = 0;private:std::mutex _mutex; // 互斥锁std::string _logger_name; // 日志器名称std::atomic<LogLevel::value> _limit_level; // 限制日志等级, atomic原子操作Formatter::ptr _formatter; // 格式化模块对象std::vector<LogSink::ptr> _sinks; // 用一个数组来存放日志落地方向 -- 一个日志器中可能有多个日志落地方向};class SynLogger : public Logger{protected:void log(const char* data, size_t len);};
}

Logger.hpp:

/*完成日志器模块1. 抽象日志器基类2. 派生出不同的子类(同步日志器嘞 & 异步日志器类)
*/#pragma once#include "util.hpp"
#include "level.hpp"
#include "sink.hpp"
#include "format.hpp"
#include <memory>
#include <mutex>
#include <atomic>
#include <cstdarg>namespace log
{//  设计日志器基类class Logger{// 公有public:// 基类指针,用来控制继承子类的对象using ptr = std::shared_ptr<Logger>;// 构造函数Logger(const std::string &logger_name, LogLevel::value level, Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks) : _logger_name(logger_name), _limit_level(level), _formatter(formatter), _sliks(sinks.begin(), sinks.end()) {}// 操作方法//获取日志器名称const std::string& name(){return _logger_name;}// 构造日志消息对象并进行格式化,得到格式化后的日志消息字符串--然后进行落地输出,5个等级void debug(const std::string &file, size_t line, const std::string &fmt, ...){ // 日志的输出操作// 1.判断当前的日志是否达到输出等级,没达到等级就 return if (LogLevel::value::DEBUG < _limit_level){return;} // 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串va_list ap;va_start(ap, fmt);char *res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1){std::cout << "vasprintf failed!!\n";return;}va_end(ap); // 代码一样,可以封装成一个函数serialize(LogLevel::value::DEBUG, file, line, res);free(res); // 将指针释放掉,防止内存泄漏}void info(const std::string &file, size_t line, const std::string &fmt, ...){ // 日志的输出操作// 1.判断当前的日志是否达到输出等级if (LogLevel::value::INFO < _limit_level){return;} // 没有达到输出等级// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串va_list ap;va_start(ap, fmt);char *res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1){std::cout << "vasprintf failed!!\n";return;}va_end(ap); // 将ap指针置空// 代码一样,可以封装成一个函数serialize(LogLevel::value::INFO, file, line, res);free(res); // 将指针释放掉}void warn(const std::string &file, size_t line, const std::string &fmt, ...){ // 日志的输出操作// 1.判断当前的日志是否达到输出等级if (LogLevel::value::WARN < _limit_level){return;} // 没有达到输出等级// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串va_list ap;va_start(ap, fmt);char *res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1){std::cout << "vasprintf failed!!\n";return;}va_end(ap); // 将ap指针置空// 代码一样,可以封装成一个函数serialize(LogLevel::value::WARN, file, line, res);free(res); // 将指针释放掉}void error(const std::string &file, size_t line, const std::string &fmt, ...){ // 日志的输出操作// 1.判断当前的日志是否达到输出等级if (LogLevel::value::ERROR < _limit_level){return;} // 没有达到输出等级// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串va_list ap;va_start(ap, fmt);char *res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1){std::cout << "vasprintf failed!!\n";return;}va_end(ap); // 将ap指针置空// 代码一样,可以封装成一个函数serialize(LogLevel::value::ERROR, file, line, res);free(res); // 将指针释放掉}void fatal(const std::string &file, size_t line, const std::string &fmt, ...){ // 日志的输出操作// 1.判断当前的日志是否达到输出等级if (LogLevel::value::FATAL < _limit_level){return;} // 没有达到输出等级// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串va_list ap;va_start(ap, fmt);char *res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1){std::cout << "vasprintf failed!!\n";return;}va_end(ap); // 将ap指针置空// 代码一样,可以封装成一个函数serialize(LogLevel::value::FATAL, file, line, res);free(res); // 将指针释放掉}void serialize(LogLevel::value level, const std::string &file, size_t line, char *str){// 3.构造LogMsg对象LogMsg msg(level, line, file, _logger_name, str); // 传入等级、行号、文件、日志器、有效信息// 4.通过格式化工具对LogMsg进行格式化,得到格式化后的日志字符串std::stringstream ss;_formatter->format(ss, msg);// 5.进行日志落地log(ss.str().c_str(), ss.str().size()); // 日志字符串和长度,调用派生类的 log}// 日志落地,抽象接口完成实际的落地输出——不同的日志器会有不同的实际落地方式virtual void log(const char *data, size_t len) = 0;protected:std::mutex _mutex;                         // 互斥锁std::string _logger_name;                  // 日志器的名字std::atomic<LogLevel::value> _limit_level; // 限制日志等级,atomic原子操作的意思是该操作执行过程中不能被中断,该操作要么不执行,要么全部执行,不存在执行一部分的情况。Formatter::ptr _formatter;                 // 控制格式化模块的对象std::vector<LogSink::ptr> _sliks;          // 这是一个数组,数组里存放日志落地方式的对象};// 派生出同步日志器class SyncLogger : public Logger{public:// 构造函数SyncLogger(const std::string &logger_name, LogLevel::value level, Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks) : Logger(logger_name, level, formatter, sinks) {}protected:// 重写虚函数,同步日志器是将日志通过落地模块句柄进行日志落地void log(const char *data, size_t len){std::unique_lock<std::mutex> lock(_mutex);// 是空if (_sliks.empty()){return;}// 不是空for (auto &sink : _sliks){sink->log(data, len); // 调用 sink 中的 log}}};
}

测试代码:

// 测试代码
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "logger.hpp"
#include <unistd.h>int main()
{// 日志器模块:同步日志器std::string logger_name = "sync_logger";log::LogLevel::value limit = log::LogLevel::value::WARN;log::Formatter::ptr fmt(new log::Formatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"));log::LogSink::ptr stdout_lsp = log::SinkFactory::create<log::StdoutSink>();                                 // 标准输出落地log::LogSink::ptr file_lsp = log::SinkFactory::create<log::FileSink>("./logfile/test.log");                 // 文件落地方式log::LogSink::ptr roll_lsp = log::SinkFactory::create<log::RollBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式std::vector<log::LogSink::ptr> sinks = {stdout_lsp, file_lsp, roll_lsp};log::Logger::ptr logger(new log::SyncLogger(logger_name, limit, fmt, sinks));logger->debug(__FILE__, __LINE__, "%s", "测试日志");logger->info(__FILE__, __LINE__, "%s", "测试日志");logger->warn(__FILE__, __LINE__, "%s", "测试日志");logger->error(__FILE__, __LINE__, "%s", "测试日志");logger->fatal(__FILE__, __LINE__, "%s", "测试日志");size_t cursize = 0;size_t count = 0;while (cursize < 1024 * 1024 * 10){logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);cursize += 20;}return 0;
}

image-20240913093154108

image-20240913093518190

日志器建造者类:

框架:

// 使用建造者模式来建造日志器,而不要让用户直接去构造日志器,简化用户的使用成本 // 1. 抽象一个日志器建造者类//     1 - 设置日志器类型//     2 - 将不同类型日志器的创建都放到同一个日志器建造者类中完成 enum class LoggerType{LOGGER_SYNC, // 同步日志器LOGGER_ASYNC // 异步日志器};class LoggerBuilder{public:void buildLoggerType(LoggerType type);void buildLoggerName(std::string &name);void buildLoggerLevel(LogLevel::value level);// 构造一个格式化器void buildLoggerFormatter(const std::string &pattern);// 一个日志器可以有多个不同的落地方式template <typename SinkType, typename... Args>void buildSink(Args &&...arg);// 完成我们的日志器构建virtual Logger::ptr build() = 0;protected:LoggerType _logger_type;std::string _logger_name;LogLevel::value _limit_level;Formatter::ptr _formatter;std::vector<LogSink::ptr> _sinks;};// 2.派生出具体的建造者类——局部日志器的建造者 & 全局日志器建造者(后边添加了全局单例管理之后,将日志器添加全局管理) // 局部日志器的建造者class LocalLoggerBuilder : public LoggerBuilder{public:Logger::ptr build() override;};

code:

enum class LoggerType{LOGGER_SYNC, // 同步日志器LOGGER_ASYNC // 异步日志器};class LoggerBuilder{public:LoggerBuilder() :_logger_type(LoggerType::LOGGER_SYNC),_limit_level(LogLevel::value::DEBUG){}void buildLoggerType(LoggerType type) { _logger_type = type;}void buildLoggerName(std::string &name) {_logger_name = name;}void buildLoggerLevel(LogLevel::value level){_limit_level = level;}// 构造一个格式化器void buildLoggerFormatter(const std::string &pattern){_formatter = std::make_shared<Formatter>(pattern);}// 一个日志器可以有多个不同的落地方式template <typename SinkType, typename... Args>void buildSink(Args &&...arg){LogSink::ptr psink = SinkFactory::create<SinkType>(std::forward<Args>(args)...);_sinks.push_back(psink);}// 完成我们的日志器构建virtual Logger::ptr build() = 0;protected:LoggerType _logger_type;std::string _logger_name;LogLevel::value _limit_level;Formatter::ptr _formatter;std::vector<LogSink::ptr> _sinks;};// 2.派生出具体的建造者类——局部日志器的建造者 & 全局日志器建造者(后边添加了全局单例管理之后,将日志器添加全局管理) // 局部日志器的建造者class LocalLoggerBuilder : public LoggerBuilder{public:Logger::ptr build() override{// 必须要有日志器名称assert(_logger_name.empty());// 必须要有格式化器,没有就要创建if (_formatter.get() == nullptr){_formatter = std::make_shared<Formatter>();}// 如果没有落地方式,默认添加一个标准输出的落地方式if (_sinks.empty()){buildSink<StdoutSink>();}// 类型为异步日志器if (_logger_type == LoggerType::LOGGER_ASYNC){// 之后写}return std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);}};
}

我们这个建造者模式没有指挥者。因为我们构造对象的零部件没有顺序的要求,只管构造就可以了,所有只要建造者。

测试代码:

#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "logger.hpp"
#include <unistd.h>
int main()
{//同步日志器建造者模式的测试//先要构造一个建造者出来std::unique_ptr<log::LoggerBuilder> builder(new log::LocalLoggerBuilder());//建造者构建零部件builder->buildLoggerType(log::LoggerType::LOGGER_SYNC);builder->buildLoggerName("sync_logger");builder->buildLoggerLevel(log::LogLevel::value::WARN);builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");builder->buildSink<log::StdoutSink>();                                 // 标准输出落地builder->buildSink<log::FileSink>("./logfile/test.log"); // 文件落地方式builder->buildSink<log::RollBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式//零部件构建好后,用建造者建筑对象log::Logger::ptr logger=builder->build();//测试日志打印logger->debug(__FILE__, __LINE__, "%s", "测试日志");logger->info(__FILE__, __LINE__, "%s", "测试日志");logger->warn(__FILE__, __LINE__, "%s", "测试日志");logger->error(__FILE__, __LINE__, "%s", "测试日志");logger->fatal(__FILE__, __LINE__, "%s", "测试日志");size_t cursize = 0;size_t count = 0;while (cursize < 1024 * 1024 * 10){logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);cursize += 20;}return 0;
}

异步日志器

思想:为了避免写日志的过程中阻塞,导致影响业务线程的执行效率。异步的思想就是不让业务线程进行日志的实际落地,而是将日志消息放到缓冲区(一块指定的内存)中接下来有一个专门的异步线程,去针对缓冲区中的数据进行处理(实际的落地操作)

实现:

  1. 实现一个线程安全的缓冲区
  2. 创建一个异步工作线程,专门用来负责缓冲区中日志信息的落地操作。

在这里插入图片描述

缓冲区详情设计:

1.使用队列缓存日志消息,逐条处理

  • 要求:不能涉及空间的频繁申请与释放,否则会降低效率。
  • 结果:设计一个环形队列(提前将空间申请好,然后对空间循环利用)
  • 存在问题:这个缓冲区的操作会涉及到多线程,因此缓冲区的操作必须保证线程安全。
  • 线程安全实现:对于缓冲区的读写加锁
  • 因此写日志操作,在实际开发中,不好分配太多资源,工作线程只需要一个日志器就行
  • 涉及到的锁冲突:生产者与生产者之间的互斥&生产者与消费者的互斥。
  • 问题:锁冲突较为严重,所有线程之间都存在互斥关系
  • 解决方案:双缓冲区

两个缓冲区,写入和处理:

在这里插入图片描述

固定时间,对两个缓冲区做交换。

单个缓冲区设计

设计一个缓冲区:直接存放格式化后的日志消息字符串,而不是放的 LogMsg 都西昂

好处:

  1. 减少了LogMsg对象频繁的构造的消耗
  2. 可以针对缓冲区中的日志消息,一次性进行IO操作,减少IO次数,提高效率

缓冲区类的设计:

1.管理一个存放字符串数据的缓冲区(使用vecotor进行空间管理)
2.当前的写入数据位置的指针(指向可写区域的起始位置,避免数据的写入覆盖)
3.当前的读取数据位置的指针(指向可读数据区域的起始位置,当读取指针与写入指针指向相同位置

提供的操作:

1.向缓冲区写入数据
2.获取可读数据起始地址的接口
3.获取可读数据长度的接口
4.移动读写位置的接口
5.初始化缓冲区的操作(将读写位置初始化——将一个缓冲区所有数据处理完毕之后)
6.提供交换缓冲区的接口(交换空间地址,并不交换空间数据)

框架:

/*实现异步日志缓冲区*/
#include "util.hpp"
#include <vector>
namespace logslearn
{
// 定义宏,表示缓冲区的大小
#define DEFAULT_BUFFER_SIZE (1 * 1024 * 1024)
#define THRESHOLD_BUFFER_SIZE (8 * 1024 * 1024)
#define INCREMENT_BUFFER_SIZE (1 * 1024 * 1024)// 异步缓冲区class Buffer{public:// 构造函数Buffer() {}// 1.向缓冲区写入数据void push(const char *data, size_t len);// 2.返回可读数据起始地址的接口const char *begin();// 3.返回可读数据的长度的接口;返回可写数据的长度的接口size_t readAbleSize();size_t writeAbleSize();// 4.移动读写指针进行向后偏移的接口void moveWriter(size_t len);void moveReader(size_t len);// 5.重置读写位置,初始化缓冲区的操作void reset();// 6.交换缓冲区的接口void swap( Buffer &buffer);// 判断缓冲区是否为空bool empty();private:// 1.存放字符串数据的缓冲区std::vector<char> _buffer;// 2.当前可写数据的指针--本质是下标size_t _reader_idx;// 3.当前可读数据的指针size_t _writer_idx;};
}

buffer.hpp:

// 实现异步日志缓冲区
#pragma once#include "util.hpp"
#include <vector>
#include <cassert>namespace log
{#define DEFAULT_BUFFER_SIZE (10 * 1024 * 1024)#define THRESHOLD_BUFFER_SIZE (80 * 1024 * 1024) // 阈值大小,小于阈值翻倍增长,达到阈值线性增长#define INCREMENT_BUFFER_SIZE (1 * 1024 * 1024)class Buffer{public:Buffer() : _buffer(DEFAULT_BUFFER_SIZE), _reader_idx(0), _writer_idx(0) {}// 向缓冲区写入数据,容量不够就扩容(两种方式,极限测试的时候使用扩容,实际使用过程中固定空间大小,空间不够阻塞)void push(const char* data, size_t len){// 缓冲区剩余空间不够的情况下:扩容。// // 1.固定大小,直接返回// if (len > writeAbleSize())//     return;// 2.动态空间,用于极限测试--扩容ensureEnoughSize(len);// 将数据拷贝到缓冲区std::copy(data, data + len, &_buffer[_writer_idx]);// 将写入位置向后偏移moveWriter(len);}// 返回可读数据起始地址的接口const char *begin(){return &_buffer[_reader_idx];}// 返回可读取数据的长度;返回可写数据的长度size_t readAbleSize(){return (_writer_idx - _reader_idx);}size_t writeAbleSize(){// 对于扩容的思路来说,不存在可写空间大小,因为总是可写的。// 因此这个接口只提供给固定大小缓冲区。return (_buffer.size() - _writer_idx);}// 移动读写指针进行向后偏移的接口void moveWriter(size_t len){assert((len + _writer_idx) <= _buffer.size());_writer_idx += len;}void moveReader(size_t len){assert(len <= readAbleSize());_reader_idx += len;}// 重置读写位置,初始化缓冲区void reset(){// 读写为 0_writer_idx = 0; // 缓冲区所有空间都是空闲的_reader_idx = 0; //_reader_idx与_writer_idx相等就表示没有数据可以读}// 对 Buffer 实现交换操作void swap(Buffer& buffer){_buffer.swap(buffer._buffer);std::swap(_reader_idx, buffer._reader_idx);std::swap(_writer_idx, buffer._writer_idx);}// 判断缓冲区是否为空bool empty(){return (_reader_idx == _writer_idx);}private:void ensureEnoughSize(size_t len){// 不需要扩容if (len < writeAbleSize()) return ;size_t new_size = 0;if (_buffer.size() < THRESHOLD_BUFFER_SIZE){// 小于阈值翻倍增长new_size = _buffer.size() * 2;} else{// 否则线性增长new_size = _buffer.size() + INCREMENT_BUFFER_SIZE;}// 重新调整空间大小_buffer.resize(new_size);}private:// 存放字符串数据的缓冲区std::vector<char> _buffer;// 当前可写数据的指针--本质是下标size_t _reader_idx;// 当前可读数据的指针size_t _writer_idx;};
}

测试代码:

测试思想:读取文件数据,一点一点写入缓冲区,最终将缓冲区数据写入文件,判断生成的新文件是否和读取的文件一致

code:

int main()
{// 异步日志器缓冲区测试// 读取文件数据,一点一点写入缓冲区,最终将缓冲区数据写入文件,判断生成的新文件与源文件是否一致std::ifstream ifs("./logfile/test.log", std::ios::binary); // 打开一个文件if (ifs.is_open() == false){return -1;} // 文件打开失败返回-1// 让读写位置跳转到末尾ifs.seekg(0, std::ios::end);// 获取当前读写位置相对于起始位置的偏移量size_t fsize = ifs.tellg();// 重新让指针跳转到起始位置ifs.seekg(0, std::ios::beg);std::string body;body.resize(fsize);ifs.read(&body[0], fsize);if (ifs.good() == false){std::cout << "read error!\n";return -1;}// 打开文件,也要关闭ifs.close();std::cout << fsize << std::endl; // 读取文件的数据大小log::Buffer buffer;              // 定义一个缓冲区for (int i = 0; i < body.size(); i++){buffer.push(&body[i], 1);}std::cout << buffer.readAbleSize() << std::endl; // buffer里面可读的数据大小std::ofstream ofs("./logfile/tem.log", std::ios::binary);size_t rsize = buffer.readAbleSize();for (int i = 0; i < rsize; i++){ ofs.write(buffer.begin(), 1);if (ofs.good() == false){std::cout << "writer error!\n";return -1;}buffer.moveReader(1);}ofs.close(); // 关闭文件return 0;
}

image-20240918185546248

md5sum 可以验证文件的完整性,tem.log和test.log文件的md5值一样,说明文件内容一模一样。

异步工作器的设计 – 双缓冲区思想

异步工作器:

异步工作使用双缓冲区的思想

  • 外界将任务数据,添加到输入缓冲区中
  • 异步线程对处理缓冲区中的数据进行处理,若处理缓冲区中没有数据了则交换缓冲区

管理的成员:

1.双缓冲区(生产,消费)
2.互斥锁 – 保证线程安全
3.条件变量-生产&消费(生产缓冲区没有数据,处理完消费缓冲区数据后就休眠)
4.回调函数(针对缓冲区中数据的处理接口-外界传入一个函数,告诉异步工作器数据该如何处理)

  • 提供的操作:

    a.停止异步工作器

    b.添加数据到缓冲区

  • 私有操作:

    创建线程,线程入口函数中,交换缓冲区,对消费缓冲区数据使用回调函数进行处理,处理完后再次交换

框架:

/*实现异步工作器
*/
#pragma once
#include "buffer.hpp"         //缓冲区
#include <thread>             //线程库
#include <mutex>              //互斥锁
#include <condition_variable> //条件变量
#include <functional>         //包装器
#include <atomic>             //原子类型
namespace logslearn
{// 异步工作器类using Functor = std::function<void(Buffer &)>;class AsyncLooper{public:using ptr = std::shared_ptr<AsyncLooper>;AsyncLooper();void stop();void push(const char *data, size_t len);private:void threadEntry(); // 线程入口函数private:Functor _callBack; // 具体对缓冲区数据进行处理的回调函数,由异步工作器使用者传入。private:std::atomic<bool> _stop;           // 让工作器停止标准变成原子性操作,提高了线程安全Buffer _pro_buf;                   // 生产缓冲区Buffer _con_buf;                   // 消费缓冲区std::mutex _mutex;                 // 互斥锁std::condition_variable _cond_pro; // 两个pcb的等待队列,这是生产者,等待队列的条件变量std::condition_variable _cond_con; // 这是消费者,等待队列的条件变量std::thread _thread;               // 异步工作器对应的工作线程};
}

looper.hpp:

/*实现异步工作器
*/
#pragma once#include "buffer.hpp"         //缓冲区
#include <thread>             //线程库
#include <mutex>              //互斥锁
#include <condition_variable> //条件变量
#include <functional>         //包装器
#include <atomic>             //原子类型namespace log
{// 异步工作器类using Functor = std::function<void(Buffer &)>;enum class AsyncType{ASYNC_SAFE,  // 安全状态,表示缓冲区满了就阻塞,避免了资源耗尽的风险ASUNC_UNSAFE // 非安全状态,不考虑资源耗尽的情况,可以无限扩容,常用与测试};class AsyncLooper{public:using ptr = std::shared_ptr<AsyncLooper>;AsyncLooper(const Functor &cb, AsyncType looper_type = AsyncType::ASUNC_UNSAFE) : _looper_type(looper_type), _stop(false), _thread(std::thread(&AsyncLooper::threadEntry, this)),_callBack(cb) {}~AsyncLooper() { stop(); }void stop() {_stop = true;_cond_con.notify_all(); // 唤醒所有的工作线程_thread.join(); // 等待工作线程退出}void push(const char *data, size_t len){// 1.无限扩容-非安全(极限压力测试的情况下使用)2.固定大小std::unique_lock<std::mutex> lock(_mutex);// 条件变量空值,若缓冲区剩余空间大小大于数据长度,就可以添加数据// 如果是安全状态就把这个代码加上,非安全状态就把这个代码屏蔽if (_looper_type == AsyncType::ASYNC_SAFE)_cond_pro.wait(lock, [&](){ return _pro_buf.writeAbleSize() >= len; }); // 被唤醒之后,如果缓冲区有剩余空间,则添加数据;否则继续休眠(lambda表达式结果为假)// 能够走下来代表满足了条件,可以向缓冲区添加数据_pro_buf.push(data, len);// 唤醒一个消费者对缓冲区中的数据进行处理_cond_con.notify_one();}private:void threadEntry() // 线程入口函数{while (!_stop){// 要为互斥锁设置一个生命周期,将缓冲区交换完毕后就解锁(不对数据的处理过程加锁保护){// 1.判断生产缓冲区里有没有数据,有则交换,无则阻塞std::unique_lock<std::mutex> lock(_mutex);//退出标志被设置,且生产缓冲区无数据,这时候在退出,否则有可能会造成生产缓冲区有数据,但是没有被完全处理                   if (_stop && _pro_buf.empty()) break;// 若退出前被唤醒,或者有数据被唤醒,则返回真,继续向下运行,否则重新陷入休眠_cond_con.wait(lock, [&](){ return !_pro_buf.empty() || _stop; }); //_stop是真表示程序退出,把剩余的数据进行交换// 等待完毕,消费者与生产者进行地址交换_con_buf.swap(_pro_buf);// 2.唤醒生产者if (_looper_type == AsyncType::ASYNC_SAFE)_cond_pro.notify_all();}// 3.被唤醒后,对消费缓冲区进行数据处理_callBack(_con_buf);// 4.初始化消费缓冲区_con_buf.reset();}}private:Functor _callBack; // 具体对缓冲区数据进行处理的回调函数,由异步工作器使用者传入。private:AsyncType _looper_type;            // 默认是安全模式std::atomic<bool> _stop;           // 让工作器停止标志变成原子性操作,提高了线程安全Buffer _pro_buf;                   // 生产缓冲区Buffer _con_buf;                   // 消费缓冲区std::mutex _mutex;                 // 互斥锁std::condition_variable _cond_pro; // 生产者,等待队列的条件变量std::condition_variable _cond_con; // 消费者,等待队列的条件变量std::thread _thread;               // 异步工作器对应的工作线程};
}
异步日志器设计

1.继承于Logger日志器类 对于写日志操作进行函数重写(不再将数据写入文件,而是通过异步消息处理器,放到缓冲区中)
2.通过异步消息处理器,进行日志数据的实际落地

管理成员:

异步工作器(异步消息处理器)
完成后,完善日志器建造者,进行异步日志器安全模式的选择,提供异步日志器的创建

框架:

//派生出异步日志器class AsyncLogger:public Logger{public:AsyncLogger( const std::string &logger_name, LogLevel::value level, Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks,AsyncType looper_type) :Logger(logger_name, level, formatter, sinks) {}//将数据写入缓冲区void log(const char*data,size_t len);//设计一个实际落地函数(将缓冲区里的数据进行落地)void realLog(Buffer &buf);private:AsyncLooper::ptr _looper;};

完善日志器建造者功能:

// 使用建造者模式来建造日志器,而不要让用户直接去构造日志器,简化用户的使用成本// 1. 抽象一个日志器建造者类//     1 - 设置日志器类型//     2 - 将不同类型日志器的创建都放到同一个日志器建造者类中完成enum class LoggerType{LOGGER_SYNC, // 同步日志器LOGGER_ASYNC // 异步日志器};class LoggerBuilder{public:LoggerBuilder() : _logger_type(LoggerType::LOGGER_SYNC),_limit_level(LogLevel::value::DEBUG),_looper_type(AsyncType::ASYNC_SAFE) {}void buildLoggerType(LoggerType type){_logger_type = type;}void buildLoggerName(const std::string &name){_logger_name = name;}void buildLoggerLevel(LogLevel::value level){_limit_level = level;}void buildEnableUnSafeAsync(){_looper_type = AsyncType::ASUNC_UNSAFE;}// 构造一个格式化器void buildLoggerFormatter(const std::string &pattern){_formatter = std::make_shared<Formatter>(pattern);}// 一个日志器可以有多个不同的落地方式template <typename SinkType, typename... Args>void buildSink(Args &&...args){LogSink::ptr psink = SinkFactory::create<SinkType>(std::forward<Args>(args)...);_sinks.push_back(psink);}// 完成我们的日志器构建virtual Logger::ptr build() = 0;protected:AsyncType _looper_type;LoggerType _logger_type;std::string _logger_name;LogLevel::value _limit_level;Formatter::ptr _formatter;std::vector<LogSink::ptr> _sinks;};// 2.派生出具体的建造者类——局部日志器的建造者 & 全局日志器建造者(后边添加了全局单例管理之后,将日志器添加全局管理)// 局部日志器的建造者class LocalLoggerBuilder : public LoggerBuilder{public:Logger::ptr build() override{// 必须要有日志器名称assert(_logger_name.empty() == false);// 必须要有格式化器,没有就要创建if (_formatter.get() == nullptr){_formatter = std::make_shared<Formatter>();}// 如果没有落地方式,默认添加一个标准输出的落地方式if (_sinks.empty()){buildSink<StdoutSink>();}// 类型为异步日志器if (_logger_type == LoggerType::LOGGER_ASYNC){// 返回异步日志器对象return std::make_shared<AsyncLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);}return std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);}};
}

测试代码:

int main()
{//异步日志器的测试//异步日志器和异步工作器进行联调// //先要构造一个建造者出来std::unique_ptr<log::LoggerBuilder> builder(new log::LocalLoggerBuilder());//建造者构建零部件builder->buildLoggerType(log::LoggerType::LOGGER_ASYNC);builder->buildLoggerName("async_logger");builder->buildLoggerLevel(log::LogLevel::value::WARN);builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");builder->buildSink<log::StdoutSink>();                                 // 标准输出落地builder->buildSink<log::FileSink>("./logfile/async.log"); // 文件落地方式// builder->buildSink<log::RoolBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式//零部件构建好后,用建造者建筑对象log::Logger::ptr logger=builder->build();//测试日志打印logger->debug(__FILE__, __LINE__, "%s", "测试日志");logger->info(__FILE__, __LINE__, "%s", "测试日志");logger->warn(__FILE__, __LINE__, "%s", "测试日志");logger->error(__FILE__, __LINE__, "%s", "测试日志");logger->fatal(__FILE__, __LINE__, "%s", "测试日志");size_t count = 0;while (count < 100000){logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);}return 0;
}

image-20240921082156581

日志器管理模块 – 单例模式

⽇志的输出,我们希望能够在任意位置都可以进⾏,但是当我们创建了⼀个⽇志器之后,就会受到⽇志器所在作⽤域的访问区域限制。

因此,为了突破访问区域的限制,我们创建⼀个⽇志器管理类,且这个类是⼀个单例类,这样的话, 我们就可以在任意位置来通过管理器单例获取到指定的⽇志器来进⾏⽇志输出了。

日志器管理器:

作用1:对所有创建的日志器进行管理
特性:将管理器设计为单例

作用2:可以在程序的任意位置,获取相同的单例对象,获取其中的日志器进行日志输出

拓展:单例管理器创建的时候,默认先创建一个日志器(用于进行标准输出的打印)

拓展目的:让用户在不创建任何日志器的情况下,也能进行标准输出的打印,方便用户使用

设计:
管理的成员:

1.默认日志器
2.所管理的日志器数组
3.互斥锁

提供的接口:

1添加日志器管理
2.判断是否管理了指定名称的日志器
3.获取指定名称的日志器
4.获取默认日志器

日志管理器设计

在 looger.hpp 中:

框架:

    // 日志器管理模块class LoggerManager{public:// 1添加日志器管理void addLogger(Logger::ptr &logger);// 2.判断是否管理了指定名称的日志器bool hasLogger(const std::string &name);// 3.获取指定名称的日志器Logger::ptr getLogger(const std::string &name);// 4.获取默认日志器Logger::ptr rootLogger();// 5. 获取单例句柄static LoggerManager &getInstance();private:// 构造函数私有化LoggerManager() {}private:// 1.默认日志器Logger::ptr _root_logger;// 2.所管理的日志器 -- 日志器名称和日志器对象(智能指针)的映射std::unordered_map<std::string, Logger::ptr> _loggers;// 3.互斥锁std::mutex _mutex;};

code:

// 日志器管理模块class LoggerManager{public:// 1添加日志器管理void addLogger(Logger::ptr &logger){if (hasLogger(logger->name())) return ;std::unique_lock<std::mutex> lock(_mutex);_loggers.insert(std::make_pair(logger->name(), logger));}// 2.判断是否管理了指定名称的日志器bool hasLogger(const std::string &name){std::unique_lock<std::mutex> lock(_mutex); // 判断之前加锁auto it = _loggers.find(name);             // 查找日志器if (it == _loggers.end()){// 代表没找到return false;}return true;}// 3.获取指定名称的日志器Logger::ptr getLogger(const std::string &name){std::unique_lock<std::mutex> lock(_mutex); // 判断之前加锁auto it = _loggers.find(name);             // 查找日志器if (it == _loggers.end()){// 代表没找到,返回一个空的智能指针return Logger::ptr();}return it->second;}// 4.获取默认日志器Logger::ptr rootLogger(){return _root_logger;}// 5. 获取单例句柄static LoggerManager &getInstance(){// 在c++11之后,针对静态局部变量,编译器在编译的层面上实现了线程安全// 当静态局部变量在没有构造完成之前,其他的线程进入就会阻塞static LoggerManager eton;return eton;}private:// 构造函数私有化LoggerManager(){// 构造一个日志器建造者std::unique_ptr<log::LoggerBuilder> builder(new log::LocalLoggerBuilder());builder->buildLoggerName("root");_root_logger = builder->build();// 把默认构造的日志器管理起来_loggers.insert(std::make_pair("root", _root_logger));}private:// 1.默认日志器Logger::ptr _root_logger;// 2.所管理的日志器std::unordered_map<std::string, Logger::ptr> _loggers;// 3.互斥锁std::mutex _mutex;};

设计一个全局的日志器建造者

在局部的日志器建造者上增加一个功能:将日志器添加到单例对象中;就是全局的日志器建造者

class GlobalLoggerBuilder : public LoggerBuilder{public:Logger::ptr build() override{// 必须要有日志器名称assert(_logger_name.empty() == false);// 必须要有formatter//必须要有格式化器,没有就要创建if (_formatter.get() == nullptr){_formatter = std::make_shared<Formatter>();}// 如果没有落地方式就给它添加一个标准输出的默认落地方式if (_sinks.empty()){buildSink<StdoutSink>();}//默认日志器Logger::ptr logger;// 如果类型为LOGGER_ASYNC,那么日志器为异步日志器if (_logger_type == LoggerType::LOGGER_ASYNC){// 返回异步日志器对象logger=std::make_shared<AsyncLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);}else{// 返回同步日志器的对象logger=std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks); // 日志器名字,等级,格式化,落地方式}//把日志器添加到日志器管理器中LoggerManager::getInstance().addLogger(logger);// 返回同步日志器的对象return logger;}};

测试日志器管理器的接口和全局建造者类

void test_log(){//日志器管理器log::Logger::ptr logger=log::LoggerManager::getInstance().getLogger("async_logger");//测试日志打印logger->debug(__FILE__, __LINE__, "%s", "测试日志");logger->info(__FILE__, __LINE__, "%s", "测试日志");logger->warn(__FILE__, __LINE__, "%s", "测试日志");logger->error(__FILE__, __LINE__, "%s", "测试日志");logger->fatal(__FILE__, __LINE__, "%s", "测试日志");size_t count = 0;while (count < 100000){logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);}}
int main()
{  //测试日志器管理模块//先要构造一个建造者出来//全局建造者构造日志器std::unique_ptr<log::LoggerBuilder> builder(new log::GlobalLoggerBuilder());//建造者构建零部件builder->buildLoggerName("async_logger");builder->buildLoggerLevel(log::LogLevel::value::WARN);builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");builder->buildLoggerType(log::LoggerType::LOGGER_ASYNC);builder->buildEnableUnSafeAsync();//切换模式builder->buildSink<log::StdoutSink>();                                 // 标准输出落地builder->buildSink<log::FileSink>("./logfile/async.log"); // 文件落地方式builder->build();// builder->buildSink<log::RoolBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式test_log();return 0;
}

image-20240921160519566

宏函数和全局接口设计 – 涉及代理模式

提供全局接口&宏函数, 对日志系统接口,进行使用便捷性优化

思想:

1.提供获取指定日志器的全局接口(避免用户自己操作单例对象)
2.使用宏函数对日志器的接口进行代理(代理模式)
3.提供宏函数,直接通过默认日志器进行日志的标准输出打印(不要获取日志器了)

新建一个.h,文件,文件里面放我们写的.hpp(各个模块文件)

log.h:

方便外界使用者进行调用,用户使用时直接包含该头文件就行,不需要包含各个模块的头文件了。

#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "logger.hpp"
#include <unistd.h>
#include "buffer.hpp"
#include "looper.hpp"
#include <fstream>namespace log
{//  1.提供获取指定日志器的全局接口(避免用户自己操作单例对象)Logger::ptr getLogger(const std::string &name) // 指定日志器{return log::LoggerManager::getInstance().getLogger(name);}Logger::ptr rootLogger() // 默认日志器{return log::LoggerManager::getInstance().rootLogger();}
//  2.使用宏函数对日志器的接口进行代理(代理模式)
#define debug(fmt, ...) debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define info(fmt, ...) info(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define warn(fmt, ...) warn(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define error(fmt, ...) error(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define fatal(fmt, ...) fatal(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
//  3.提供宏函数,直接通过默认日志器进行日志的标准输出打印(不要获取日志器了)
#define DEBUG(fmt, ...) log::rootLogger()->debug(fmt, ##__VA_ARGS__)
#define INFO(fmt, ...) log::rootLogger()->info(fmt, ##__VA_ARGS__)
#define WARN(fmt, ...) log::rootLogger()->warn(fmt, ##__VA_ARGS__)
#define ERROR(fmt, ...) log::rootLogger()->error(fmt, ##__VA_ARGS__)
#define FATAL(fmt, ...) log::rootLogger()->fatal(fmt, ##__VA_ARGS__)}

对宏函数与全局接口进行功能测试

测试第一组宏是否正确:

#include "log.h"void test_log(){//日志器管理器log::Logger::ptr logger=log::LoggerManager::getInstance().getLogger("async_logger");//测试日志打印logger->debug("%s", "测试日志");logger->info( "%s", "测试日志");logger->warn("%s", "测试日志");logger->error( "%s", "测试日志");logger->fatal( "%s", "测试日志");size_t count = 0;while (count < 100000){logger->fatal( "测试日志-%d", count++);}}
int main()
{  //测试日志器管理模块//先要构造一个建造者出来//全局建造者构造日志器std::unique_ptr<log::LoggerBuilder> builder(new log::GlobalLoggerBuilder());//建造者构建零部件builder->buildLoggerName("async_logger");builder->buildLoggerLevel(log::LogLevel::value::WARN);builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");builder->buildLoggerType(log::LoggerType::LOGGER_ASYNC);builder->buildEnableUnSafeAsync();//切换模式builder->buildSink<log::StdoutSink>();                                 // 标准输出落地builder->buildSink<log::FileSink>("./logfile/async.log"); // 文件落地方式builder->build();// builder->buildSink<log::RoolBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式test_log();return 0;
}

测试第二组宏:


#include "log.h"
void test_log(){// 使用默认日志器打印DEBUG("%s", "测试日志");INFO("%s", "测试日志");WARN("%s", "测试日志");ERROR("%s", "测试日志");FATAL("%s", "测试日志");size_t count = 0;while (count < 100000){FATAL("测试日志-%d", count++);}
}
int main()
{test_log();return 0;
}

image-20240921164601288

项目目录结构

example:如何使用项目的具体样例
logs:项目的各个模块,项目源码
pratice:练习代码,项目前置学习的代码
bench:用来进行性能测试

整理完成后,目录结构如下:

image-20240923103830510

功能测试

#include "../logs/log.h"//进行功能测试
void test_log(const std::string &name){INFO( "%s", "测试开始");log::Logger::ptr logger=log::LoggerManager::getInstance().getLogger(name);//测试日志打印logger->debug( "%s", "测试日志");logger->info( "%s", "测试日志");logger->warn( "%s", "测试日志");logger->error( "%s", "测试日志");logger->fatal("%s", "测试日志");INFO( "%s", "测试结束");
}
int main()
{std::unique_ptr<log::LoggerBuilder> builder(new log::GlobalLoggerBuilder());//建造者构建零部件builder->buildLoggerName("async_logger");builder->buildLoggerLevel(log::LogLevel::value::DEBUG);builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%p]%m%n");builder->buildLoggerType(log::LoggerType::LOGGER_SYNC);builder->buildSink<log::StdoutSink>();                                 // 标准输出落地builder->buildSink<log::FileSink>("./logfile/async.log"); // 文件落地方式builder->buildSink<log::RollBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式builder->build();test_log("async_logger");return 0;
}

image-20240921183817074

image-20240921182751586

性能测试

测试环境:

CPU:11th Gen Intel® Core™ i7-11800H @ 2.30GHz

RAM:16G DDR4 3200

ROM:512GB-SSD

OS:CentOS7.6(2核2G)

测试三要素:

1.测试环境
2.测试方法
3.测试结果

测试工具的编写:

1.可以控制写日志线程数量
2.可以控制写日志的总数量

分别对于同步日志器 & 异步日志器进行各自的性能测试,

​ 需要测试单写日志线程的性能

​ 需要测试多写日志线程的性能

实现:

封装一个接口,传入日志器名称,线程数量,日志数量,单条日志大小 在接口内,创建指定数量的线程,各自负责一部分日志的输出,在输出之前计时开始,在输出完毕后计时结束。
所耗时间=结束时间-起始时间
每秒输出量 =日志数量/总耗时
每秒输出大小 =日志数量*单条日志大小/总耗时
注意:异步日志输出这里,我们启动非安全模式,纯内存写入(不去考虑实际落地的时间)

项目性能测试工具实现

在对项目测试之前,需要编写测试工具,测试工具的具体实现放到bench目录下。

bench.cc:

#include "../logs/log.h"
// c++11提供的时间特性
#include <chrono>
// 设计性能测试功能
// logger_name日志器的名字,thr_count线程数的个数,msg_counr日志消息的总条数,len日志消息的长度
void bench(const std::string &logger_name, size_t thr_count, size_t msg_count, size_t msg_len)
{// 1.获取日志器log::Logger::ptr logger = log::getLogger(logger_name);// 如果没找到日志器就返回空if (logger.get() == nullptr){return;}// 2.组织指定长度的日志消息// 留一个字符,放换行符std::string msg(msg_len - 1, 'A');// 3.创建指定数量的线程// 创建一个存放线程的数组std::vector<std::thread> threads;// 存放每个线程打印日志需要消耗的时间std::vector<double> cost_arry(thr_count);> 日志系统:
>
> 日志:程序运行过程中所记录的程序运行状态信息
>
> 日志的作用:记录程序运行状态信息,以便于程序员能够随时根据状态信息,对系统的运行状态进行分析。项目不是一个功能型业务型的项目,是一个组件型的项目,是一个库,是给别人用的;所以项目演示时,就展示一下日志系统的性能。# 项目介绍本项目主要实现的是一个日志系统,其支持以下功能:- 支持多级别日志消息
- 支持同步日志和异步日志
- 支持写入日志到控制台、文件以及滚动文件中
- 支持多线程程序并发写日志
- 支持扩展不同的日志落地到目标地# 开发环境- 操作系统 :Centos 7
- 编辑器: vscode / vim
- 编译器/调试器:g++ / gdb
- 项目自动化构建工具:Makefile# 核心技术- 类层次设计(继承、多态的实际应用)
- C++11语法(多线程库,auto,智能指针,右值引用等)
- 双缓冲区
- 生产者消费者模型
- 多线程
- 设计模式(单例、工厂、代理、建造者等)# 日志系统介绍## 为什么需要日志系统* 在生产环境中的产品,为了保证其稳定性以及安全性,是不允许开发人员附加调试器去排查问题的(调试器调试时出现程序崩溃需要根据程序运行数据进行分析,这是不允许的,产品是有隐私的),可以借助日志系统来打印一些日志帮助开发人员解决问题
* 上线客户端的产品出现的Bug无法复现并解决,可以借助日志系统打印日志并且上传到服务端帮助开发人员进行问题分析
* 对于一些高频操作(如定时器,心跳包等)在少量调试次数下可能无法触发我们想要的行为,通过断点暂停的方式,我们需要重复几十次甚至上百次,导致排查问题的效率非常低下,可以借助打印日志的方式排查问题
* 在分布式、多线程/多进程的代码中,出现bug非常难定位,可以借助日志系统打印日志帮助定位bug
* 帮助刚接触项目不久的开发人员理解代码的运行流程## 日志系统技术实现> **日志系统的技术实现主要包括三种类型:**
>
> 1. 利用printf、std::cout等输出函数将日志信息打印到控制台。-- 1
> 2. 对于大型商业化项目,为了方便排查问题,我们一般会将日志输出到文件或者是数据库系统方便查询和分析日志,主要分为**同步日志****异步日志**方式。
>    * 同步写日志 -- 2
>    * 异步写日志 -- 3### 同步写日志**同步日志指的是当输出日志时,必须等待日志输出语句执行完毕后,才能执行后面的业务逻辑语句,日志输出语句与程序的业务逻辑语句将在同一个线程中运行。每次调用一次打印日志API就对应一次系统调用write写日志文件**![image-20240827114052891](https://anduin.oss-cn-nanjing.aliyuncs.com/image-20240827114052891.png)优点:流程简单,编写代码简单缺点:在高并发场景下,随着日志数量不断增加,同步日志系统容易产生系统瓶颈:1. ⼀方⾯,⼤量的日志打印陷⼊等量的write系统调用,有⼀定系统开销.
2. 另⼀方⾯,使得打印日志的进程附带了⼤量同步的磁盘IO,影响程序性能### 异步写日志**异步日志是指在进行日志输出时,日志输出语句与业务逻辑语句并不是在同一个线程中运行,而是有专门的线程用于进行日志输出操作,业务线程只需要将日志放到一个内存缓冲区中,不需要等待即可继续执行后续业务逻辑(作为日志的生产者),而日志的落地操作交给单独的日志线程完成(作为日志的消费者),这是一个典型的生产者消费者模型**![image-20240827120755639](https://anduin.oss-cn-nanjing.aliyuncs.com/image-20240827120755639.png)这样的好处是即使日志没有真正的完成输出也不会影响程序的主业务,提高程序的性能1. 主线程调用日志打印接口成为非阻塞操作
2. 同步的磁盘IO操作从主线程剥离出来交给单独的线程完成。# 相关技术知识补充## 不定参函数> 在初学C语⾔的时候,我们都⽤过printf函数进⾏打印。其中printf函数就是⼀个不定参函数,在函数内部可以根据格式化字符串中格式化字符分别获取不同的参数进⾏数据的格式化。### 不定参宏函数的使用1. 不定参的表示用:“…”
2. 不定参的使用:“ `__VA_REGS__`”。(在 `__VA_REGS__` 前加 ##,意思是如果  `__VA_REGS__` 为空就取消前面的逗号。)```c
#include <stdio.h>
#define LOG(fmt, ...) printf("[%s:%d]" fmt, __FILE__, __LINE__, __VA_ARGS__);
int main()
{//编译器内置的宏:__FILE__当前文件名;__LINE__当前行号。LOG("%s-%d\n", "Anduin", 521);return 0;
}

image-20240827153855676如果只有LOG(“Anduin”),就会报错;这时候就要加##号。

#define LOG(fmt, ...) printf("[%s:%d]" fmt, __FILE__, __LINE__, ##__VA_ARGS__);

image-20240827154001378

C中不定参函数的使用

#define _GNU_SOURCE //使用vasprintf函数必须带这个宏
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
//参数类型一样
void printNum(int n, ...)
{va_list al; // 可变参数列表va_start(al, n); // 获取n参数之后的第一个不定参数的地址for (int i = 0; i < n; i++){int num = va_arg(al, int); // 从可变参数列表中获取⼀个整形参数printf("param[%d]:%d\n", i, num);}va_end(al); // 将al置空
}
//参数类型不一样 -- 模拟一下 printf 的实现
void myprintf(const char* fmt,...)
{va_list ap;va_start(ap,fmt);char *res;//对不定参数的基本使用;給一个一级指针的地址(&res)进去,根据fmt里面的字符串格式,一个一个的取出可变参数列表的每个数。int ret=vasprintf(&res,fmt,ap);if(ret!=-1){printf(res);free(res);}va_end(ap);//将ap指针置空
}
int main()
{printNum(3, 11, 22,33);printNum(5, 44, 55, 66, 77, 88);myprintf("%s %d\n","Anduin",521);return 0;
}

image-20240828075727198

C++中不定参函数的使用

#include <iostream>// 实现一个打印操作// 特化
void xprintf()
{std::cout<<std::endl;
}template <typename T,typename ...Args>
void xprintf(const T &v,Args &&...args)
{std::cout << v;// 通过 sizeof 计算参数包参数的个数if((sizeof ...(args))>0){ xprintf(std::forward<Args>(args)...);}else{// 参数包没参数了xprintf();}
}int main()
{xprintf("Anduin","haha",521);xprintf("Anduin","haha");xprintf("Anduin");return 0;
}

image-20240828083611601

设计模式

设计模式是前辈们对代码开发经验的总结,是解决特定问题的⼀系列套路。它不是语法规定,⽽是⼀套⽤来提⾼代码可复⽤性、可维护性、可读性、稳健性以及安全性的解决⽅案。

六⼤原则:

  • 单⼀职责原则(SingleResponsibilityPrinciple);

    • 类的职责应该单⼀,⼀个类只做⼀件事。职责划分清晰了,每次改动到最⼩单位的⽅法或类。
    • 使⽤建议:两个完全不⼀样的功能不应该放⼀个类中,⼀个类中应该是⼀组相关性很⾼的函数、数据的封装
    • ⽤例:⽹络聊天:⽹络通信&聊天,应该分割成为网络通信类&聊天类
  • 开闭原则(OpenClosedPrinciple )

    • 对扩展开放,对修改封闭。
    • 使⽤建议:对软件实体的改动,最好⽤扩展⽽⾮修改的⽅式。
    • ⽤例:超市卖货:商品价格—不是修改商品的原来价格,⽽是新增促销价格。
  • 里氏替换原则(LiskovSubstitutionPrinciple )

    • 通俗点讲,就是只要⽗类能出现的地⽅,⼦类就可以出现,⽽且替换为⼦类也不会产⽣任何错误或异常。
    • 在继承类时,务必重写⽗类中所有的⽅法,尤其需要注意⽗类的protected⽅法,⼦类尽量不要暴露⾃⼰的public⽅法供外界调⽤。
    • 使⽤建议:⼦类必须完全实现⽗类的⽅法,孩⼦类可以有⾃⼰的个性。覆盖或实现⽗类的⽅法时,输⼊参数可以被放⼤,输出可以缩⼩。
    • ⽤例:跑步运动员类-会跑步,⼦类⻓跑运动员-会跑步且擅⻓⻓跑,⼦类短跑运动员-会跑步且擅⻓短跑。
  • 依赖倒置原则(Dependence Inversion Principle)

    • ⾼层模块不应该依赖低层模块,两者都应该依赖其抽象.不可分割的原⼦逻辑就是低层模式,原⼦逻辑组装成的就是⾼层模块。
    • 模块间依赖通过抽象(接⼝)发⽣,具体类之间不直接依赖。
    • 使⽤建议:每个类都尽量有抽象类,任何类都不应该从具体类派⽣。尽量不要重写基类的⽅法。结合⾥⽒替换原则使⽤。
    • ⽤例:奔驰⻋司机类–只能开奔驰;司机类–给什么⻋,就开什么⻋;开⻋的⼈:司机–依赖于抽象。
  • 迪⽶特法则(LawofDemeter),⼜叫“最少知道法则”

    • 尽量减少对象之间的交互,从⽽减⼩类之间的耦合。⼀个对象应该对其他对象有最少的了解。 对类的低耦合提出了明确要求:1.只和直接的朋友交流,朋友之间也是有距离的。⾃⼰的就是⾃⼰的。(如果⼀个⽅法放在本类中,既不增加类间关系,也对本类不产⽣负⾯影响,那就放置在本类中)
    • ⽤例:⽼师让班⻓点名–⽼师给班⻓⼀个名单,班⻓完成点名勾选,返回结果,⽽不是班⻓点名,⽼师勾选。
  • 接⼝隔离原则(Interface Segregation Principle );

    • 客⼾端不应该依赖它不需要的接⼝,类间的依赖关系应该建⽴在最⼩的接⼝上。
    • 使⽤建议:接⼝设计尽量精简单⼀,但是不要对外暴露没有实际意义的接⼝。
    • ⽤例:修改密码,不应该提供修改⽤⼾信息接⼝,⽽就是单⼀的最⼩修改密码接⼝,更不要暴露数据库操作。

从整体上来理解六⼤设计原则,可以简要的概括为⼀句话,用抽象构建框架,用实现扩展细节,每⼀条设计原则对应⼀条注意事项:

  • 单⼀职责原则告诉我们实现类要职责单⼀;
  • 开闭原则是总纲,告诉我们要对扩展开放,对修改关闭;
  • ⾥⽒替换原则告诉我们不要破坏继承体系;
  • 依赖倒置原则告诉我们要⾯向接⼝编程;
  • 迪⽶特法则告诉我们要降低耦合;
  • 接⼝隔离原则告诉我们在设计接⼝的时候要精简单⼀。

单例模式

一个类只创建一个对象。 该设计模式可以保证系统中该类只有⼀个实例化对象,并提供⼀个访问它的全局访问点,该实例被所有程序模块共享。⽐如在某个服务器程序中,该服务器的配置信息存放在⼀个⽂件中,这些配置数据由⼀个单例对象统⼀读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种⽅式简化了在复杂环境下的配置管理。

饿汉模式:

程序启动时就会创建⼀个唯⼀的实例对象。因为单例对象已经确定,所以⽐较适⽤于多线程环境中,多线程获取单例对象不需要加锁,可以有效的避免资源竞争,提⾼性能。

#include <iostream>
class Singleton
{
private:static Singleton _eton;Singleton() : _data(99){std::cout << "单例对象构造" << std::endl;}Singleton(const Singleton&) = delete; Singleton& operator= (Singleton&) = delete;~Singleton(){};                    
private:int _data;
public:static Singleton &getInstance(){return _eton;}int getData(){return _data;}
};
Singleton Singleton::_eton;int main()
{std::cout << Singleton::getInstance().getData() << std::endl;return 0;
}

image-20240828100333824

懒汉模式:

懒加载,延迟加载的思想,一个对象在用的时候在进行实例化。 如果单例对象构造特别耗时或者耗费资源(加载插件、加载⽹络资源等), 可以选择懒汉模式, 在第⼀次使⽤的时候才创建对象。

  • 这⾥介绍的是《Effective C++》⼀书作者ScottMeyers 提出的⼀种更加优雅简便的单例模式 Meyers’Singleton in C++。
  • C++11 Static local variables特性以确保C++11起,静态变量将能够在满⾜ thread-safe 的前提下唯⼀地被构造和析构
#include <iostream>
class Singleton
{
private:Singleton() : _data(99){std::cout << "单例对象构造" << std::endl;}Singleton(const Singleton &) = delete; Singleton& operator= (Singleton&) = delete;~Singleton(){};                        
private:int _data; 
public:static Singleton &getInstance(){static Singleton _eton;return _eton;}int getData(){return _data;}
};
int main()
{std::cout << Singleton::getInstance().getData() << std::endl; return 0;
}

注释 27 行:

image-20240828102633143

没注释 27 行:

image-20240828102543257

工厂模式

⼯⼚模式是⼀种创建型设计模式,它提供了⼀种创建对象的最佳⽅式。在⼯⼚模式中,我们创建对象时不会对上层暴露创建逻辑,⽽是通过使⽤⼀个共同结构来指向新创建的对象,以此实现创建-使⽤的分离

工厂模式可以分为:

  1. 简单工厂模式

简单⼯⼚模式实现由⼀个⼯⼚对象通过类型决定创建出来指定产品类的实例。

假设有个⼯⼚能⽣产出⽔果,当客⼾需要产品的时候明确告知⼯⼚⽣产哪类⽔果,⼯⼚需要接收⽤⼾提供的类别信息,当新增产品的时候,⼯⼚内部去添加新产品的⽣产⽅式。

#include <iostream>
#include <memory>// 水果 -- 抽象类
class Fruit
{
public:virtual void name() = 0;
};// 水果产品 -- 苹果 香蕉
class Apple : public Fruit
{
public:void name() override{std::cout << "我是一个苹果" << std::endl;}
};class Banana : public Fruit
{
public:void name() override{std::cout << "我是一个香蕉" << std::endl;}
};// 水果工厂
class FruitFactory
{
public:static std::shared_ptr<Fruit> create(const std::string &name){if (name == "苹果")return std::make_shared<Apple>();elsereturn std::make_shared<Banana>();}
};int main()
{std::shared_ptr<Fruit> fruit = FruitFactory::create("苹果");fruit->name();fruit = FruitFactory::create("香蕉");fruit->name();return 0;
}

image-20240828115618131

  • 优点:简单粗暴,直观易懂。

  • 缺点:

    • 生产产品的代码都写在一起,产品太多会导致代码量庞⼤
    • 扩展性差,当新增产品时,需要修改工厂类新增新产品的产品创建逻辑,违背了开闭原则。
  1. 工厂方法模式

在简单⼯⼚模式下新增多个⼯⼚,多个产品,每个产品对应⼀个⼯⼚。假设现在有 A、B 两种产品,则开两个⼯⼚,⼯⼚ A 负责⽣产产品 A,⼯⼚ B 负责⽣产产品 B,用户只知道产品 的⼯⼚名,⽽不知道具体的产品信息,⼯⼚不需要再接收客户的产品类别,⽽只负责⽣产产品。

// 工厂方法模式遵循了开闭原则,扩展性好
#include <iostream>
#include <memory>// 水果类 -- 抽象类
class Fruit
{
public:virtual void name() = 0;
};class Apple : public Fruit
{
public:void name() override{std::cout << "我是一个苹果" << std::endl;}
};// 水果产品 -- 苹果 香蕉
class Banana : public Fruit
{
public:void name() override{std::cout << "我是一个香蕉" << std::endl;}
};// 水果工厂 -- 抽象类
class FruitFactory
{
public:virtual std::shared_ptr<Fruit> create() = 0;
};// 苹果工厂
class AppleFactory : public FruitFactory
{
public:virtual std::shared_ptr<Fruit> create() override // 用来重写基类的同名虚函数{return std::make_shared<Apple>();}
};
// 香蕉工厂
class BananFactory : public FruitFactory
{
public:virtual std::shared_ptr<Fruit> create() override // 用来重写基类的同名虚函数{return std::make_shared<Banana>();}
};int main()
{std::shared_ptr<FruitFactory> ff(new AppleFactory()); // 创建一个苹果工厂std::shared_ptr<Fruit> fruit = ff->create();          // 生产苹果fruit->name();ff.reset(new BananFactory()); // 重新设置工厂为香蕉工厂fruit = ff->create();         // 生产香蕉fruit->name();                return 0;return 0;
}

image-20240828143717281

但是⼯⼚⽅法模式每次增加⼀个产品时,都需要增加⼀个具体产品类和⼯⼚类,这会使得系统中类的个数成倍增加的,代码就会比较臃肿。

  1. 抽象工厂模式

⼯⼚⽅法模式通过引⼊⼯⼚等级结构,解决了简单⼯⼚模式中⼯⼚类职责太重的问 题,但由于⼯⼚⽅法模式中的每个⼯⼚只⽣产⼀类产品,可能会导致系统中存在⼤量的⼯⼚类,势必会增加系统的开销。

此时,我们可以考虑将⼀些相关的产品组成⼀个产品族,由同⼀个⼯⼚来统⼀⽣产,这就是抽象⼯⼚模式的基本思想。

#include<iostream>
#include<memory>class Fruit
{
public:Fruit() {}virtual void name() = 0;
};class Apple : public Fruit
{
public:Apple() {}virtual void name(){std::cout << "我是一个苹果" << std::endl;}
};class Banana : public Fruit
{
public:Banana() {}virtual void name(){std::cout << "我是一个香蕉" << std::endl;}
};class Animal
{
public:virtual void name() = 0;
};class Lamp : public Animal
{
public:virtual void name(){std::cout << "我是山羊" << std::endl;}
};class Dog : public Animal
{
public:virtual void name(){std::cout << "我是小狗" << std::endl;}
};class Factory
{
public:virtual std::shared_ptr<Fruit> GetFruit(const std::string &name) = 0;virtual std::shared_ptr<Animal> GetAnimal(const std::string &name) = 0;
};class FruitFactory : public Factory
{
public:// 纯虚函数必须重写virtual std::shared_ptr<Animal> GetAnimal(const std::string &name){return std::shared_ptr<Animal>(); // 返回一个空对象}virtual std::shared_ptr<Fruit> GetFruit(const std::string &name){if(name ==  "苹果"){return std::make_shared<Apple>();}else if(name == "香蕉"){return std::make_shared<Banana>();}}
};class AnimalFactory : public Factory
{
public:virtual std::shared_ptr<Fruit> GetFruit(const std::string &name){return std::shared_ptr<Fruit>();}virtual std::shared_ptr<Animal> GetAnimal(const std::string &name){if(name ==  "山羊"){return std::make_shared<Lamp>();}else if(name == "小狗"){return std::make_shared<Dog>();}}
};class FactoryProducer
{
public:static std::shared_ptr<Factory> getFactory(const std::string &name){if(name == "动物"){return std::make_shared<AnimalFactory>();}else {return std::make_shared<FruitFactory>();}}
};int main()
{std::shared_ptr<Factory> ff = FactoryProducer::getFactory("水果");std::shared_ptr<Fruit> f = ff->GetFruit("香蕉");f->name();f = ff->GetFruit("苹果");f->name();std::shared_ptr<Factory> af = FactoryProducer::getFactory("动物");std::shared_ptr<Animal> a = af->GetAnimal("山羊");a->name();a = af->GetAnimal("小狗");a->name();return 0;
}

image-20240828153345509

抽象⼯⼚模式适⽤于⽣产多个⼯⼚系列产品的设计模式,增加新的产品时,需要对原有系统进⾏较⼤的修改,甚⾄需要修改抽象层(例如 Factory)的代码,违背了开闭原则

建造者模式

建造者模式是⼀种创建型设计模式, 使⽤多个简单的对象⼀步⼀步构建成⼀个复杂的对象(可能会按照一定的顺序才能构建成产品),能够将⼀个复杂的对象的构建与它的表⽰分离,提供⼀种创建对象的最佳⽅式。主要⽤于解决对象的构建过于复杂的问题。

建造者模式主要基于五个类实现:

  • 抽象产品类
  • 具体产品类:⼀个具体的产品对象类 – 对抽象产品类做一个具象化的实现
  • 抽象 Builder 类:创建⼀个产品对象所需的各个部件的抽象接⼝
  • 具体产品 Builder 类:实现抽象接⼝,构建各个部件
  • 指挥者 Director 类:统⼀组建过程,提供给调⽤者使⽤,通过指挥者类来构造产品
#include <iostream>
#include <string>
#include <memory>// 抽象产品类
class Computer
{
public:Computer() {}void setBoard(const std::string &board){_board = board;}void setDisplay(const std::string &display){_display = display;}void showParamaters(){std::string computer = "Computer: \n  \n";computer += "\tboard = " + _board + ",\n";computer += "\tdisplay = " + _display + ",\n";computer += "\tOS = " + _os + ",\n  \n";std::cout << computer << std::endl;}virtual void setOs() = 0;// 构造电脑所需要的零部件
protected: // 用 private,public 继承看不见,所以写 protectedstd::string _board;std::string _display;std::string _os;
};// 具体产品类
class MacBook : public Computer
{
public:void setOs() override{_os = "MAC OS x12";}
};// 抽象 Builder 类
class Builder
{
public:virtual void buildBoard(const std::string &board) = 0;virtual void buildDisplay(const std::string &display) = 0;virtual void buildOs() = 0;virtual std::shared_ptr<Computer> build() = 0;
};// 具体产品 Builder 类
class MacBookBuilder : public Builder
{
public:MacBookBuilder() : _computer(new MacBook()) {}void buildBoard(const std::string &board){_computer->setBoard(board);}void buildDisplay(const std::string &display){_computer->setDisplay(display);}void buildOs(){_computer->setOs();}std::shared_ptr<Computer> build(){return _computer;}private:std::shared_ptr<Computer> _computer;
};// 指挥者 Director 类
class Director
{
public:Director(Builder* builder): _builder(builder) {}void construct(const std::string& board, const std::string& display){_builder->buildBoard(board);_builder->buildDisplay(display);_builder->buildOs();}
private:std::shared_ptr<Builder> _builder;	
};int main()
{Builder* builder = new MacBookBuilder(); // 构造一个具体产品类std::unique_ptr<Director> director(new Director(builder)); // 构造一个指挥者类director->construct("华硕主板", "三星显示器");std::shared_ptr<Computer> computer = builder->build();computer->showParamaters();return 0;
}

image-20240903083755162

代理模式

代理模式指代理控制对其他对象的访问,也就是代理对象控制对原对象的引⽤。在某些情况下,⼀个对象不适合或者不能直接被引⽤访问,⽽代理对象可以在客⼾端和⽬标对象之间起到中介的作⽤。

代理模式的结构包括⼀个是真正的你要访问的对象(⽬标类)、⼀个是代理对象。⽬标对象与代理对象实现同⼀个接⼝(代理对象的接口中会包含目标对象的接口,最终使用的是代理对象的接口,所以说实现同一个接口),先访问代理类再通过代理类访问⽬标对象。代理模式分为静态代理、动态代理:

  • 静态代理指的是,在编译时就已经确定好了代理类和被代理类的关系。也就是说,在编译时就已经确定了代理类要代理的是哪个被代理类。-- (
  • 动态代理指的是,在运⾏时才动态⽣成代理类,并将其与被代理类绑定。这意味着,在运⾏时才能
    确定代理类要代理的是哪个被代理类 – (涉及很多其他知识,不讲)。

以租房为例,房东将房⼦租出去,但是要租房⼦出去,需要发布招租启⽰,带⼈看房,负责维修,这些⼯作中有些操作并⾮房东能完成,因此房东为了图省事,将房⼦委托给中介进⾏租赁。

代理模式 – 静态代理:

#include <iostream>// 抽象租房类 
class RentHouse
{
public:virtual void rentHouse() = 0;
};// 房东
class Landlord : public RentHouse
{
public:void rentHouse() override{std::cout << "将房子租出去\n";}
};// 中介代理类 -- 对租房的功能加强,在房东租房的基础之上,额外完成其他功能 
class Intermediary : public RentHouse
{
public:void rentHouse() override{std::cout << "发布招租启示\n";std::cout << "带人看房\n";_landlord.rentHouse();std::cout << "负责租后维修\n";}private:Landlord _landlord;
};int main()
{Intermediary intermediary;intermediary.rentHouse(); // 通过中介来出租房子return 0;
}

image-20240829103733973

日志系统框架设计

日志系统:
作用:将一条消息,进行格式化为指定格式的字符串后,写入到指定位置

  1. 日志要写入指定位置(标准输出,指定文件, 滚动文件等 – 可扩展 – 可以写入到其他扩展位置)
    日志系统需要支持将日志消息落地到不同的位置—多落地方向
  2. 日志写入指定位置,支持不同的写入方式(同步,异步)
    同步:业务线程自己负责日志的写入(流程简单,但是有可能会因为阻塞导致效率降低) 异步:业务线程将日志放入内存缓冲区,让其他异步线程负责将日志写入指定位置
  3. 日志输出以日志器为单位,支持多日志器(不同的项目组有不同的日志输出策略)

本项⽬实现的是⼀个多⽇志器⽇志系统,主要实现的功能是让程序员能够轻松的将程序运⾏⽇志信息落地到指定的位置,且⽀持同步与异步两种⽅式的⽇志落地⽅式。项⽬的框架设计将项⽬分为以下⼏个模块来实现。

模块划分

日志等级模块

枚举出日志分为多少个等级—对不同的日志有不同等级标记–以便于控制输出

  • OFF:关闭
  • DEBUG:调试,调试时的关键信息输出。
  • INFO:提示,普通的提⽰型⽇志信息。
  • WARN:警告,不影响正常运⾏(运行不会有错误),但是需要注意⼀下的⽇志。
  • ERROR:错误,程序运⾏出现错误,但是还能继续运行。这些错误可能会影响部分功能,但是通常不会导致整个程序崩溃的⽇志,如文件读写失败,数据库连接失败。
  • FATAL:致命,⼀般是代码异常导致程序⽆法继续推进运⾏的⽇志,即将崩溃或者已经崩溃。

日志消息模块

封装一条日志所需的各种要素(时间,线程ID,文件名,行号,日志等级,消息主体…)

  • 时间:描述本条⽇志的输出时间。
  • 线程ID:描述本条⽇志是哪个线程输出的。
  • ⽇志等级:描述本条⽇志的等级。
  • ⽇志数据:本条⽇志的有效载荷数据。
  • ⽇志⽂件名:描述本条⽇志在哪个源码⽂件中输出的。
  • ⽇志⾏号:描述本条⽇志在源码⽂件的哪⼀⾏输出的。

日志消息格式化模块

按照指定的格式,对于日志消息中关键要素进行组织,最终得到一个指定格式的字符串

系统默认的输出格式: [%d{%H:%M:%S}]%T[%t]%T[%p]%T[%c]%T%f:%l%T%m%n

[12:38:45] [12345678] [FATAL] [root] main.c:178 套接字创建失败…\n

  • %d{%H:%M:%S}:表⽰⽇期时间,花括号中的内容表示日期时间的格式。
  • %T:表⽰制表符缩进。
  • %t:表⽰线程ID。
  • %p:表⽰⽇志级别。
  • %c:表⽰⽇志器名称,不同的项目组可以创建⾃⼰的⽇志器进⾏⽇志输出,⼩组之间互不影响。
  • %f:表⽰⽇志输出时的源代码⽂件名。
  • %l:表⽰⽇志输出时的源代码⾏号。
  • %m:表⽰⽇志有效载荷数据 。
  • %n:表⽰换行。
  • 设计思想:设计不同的⼦类,不同的⼦类从⽇志消息中取出不同的数据进⾏处理。

日志落地模块

负责对日志消息进行指定方向的写入输出

  • 标准输出:表⽰将⽇志进⾏标准输出的打印。
  • ⽇志⽂件输出:表⽰将⽇志写⼊指定的⽂件末尾。
  • 滚动⽂件输出:当前以⽂件⼤⼩进⾏控制,当⼀个⽇志⽂件⼤⼩达到指定⼤⼩,则切换下⼀个⽂件进⾏输出 。
  • 后期,也可以扩展远程⽇志输出,创建客⼾端,将⽇志消息发送给远程的⽇志分析服务器。
  • 设计思想:设计不同的⼦类,不同的⼦类控制不同的⽇志落地⽅向。

日志器模块

对上面几个模块的整合,⽤⼾通过⽇志器进⾏⽇志的输出,有效降低⽤⼾的使⽤难度;包含有:⽇志落地模块对象,⽇志消息格式化模块对象,⽇志限制输出等级

  • 同步日志器模块—完成日志的同步输出功能。
  • 异步日志器模块—完成日志的异步输出功能

异步线程模块

负责异步日志的实际落地输出功能

  • 实现对⽇志的异步输出功能,⽤⼾只需要将输出⽇志任务放⼊任务池,异步线程负责⽇志的落地输出功能,提供了更加⾼效的⾮阻塞的⽇志输出。

单例的日志器管理模块

对日志进行全局的管理,以便于能够在项目的任何位置获取指定的日志器进行日志输出

  • 为了降低项⽬开发的⽇志耦合,不同的项⽬组可以有⾃⼰的⽇志器来控制输出格式以及落地⽅向,因此本项⽬是⼀个多⽇志器的⽇志系统。
  • 管理模块就是对创建的所有⽇志器进⾏统⼀管理。并提供⼀个默认⽇志器,提供标准输出的⽇志输出。

模块关系图

在这里插入图片描述

代码设计

实用类设计

提前完成一些零碎的功能接口:

  • 获取系统时间
  • 判断文件是否存在
  • 获取文件的所在目录路径
  • 创建目录

util.hpp:

# pragma once#include <iostream>
#include <ctime>
#include <sys/stat.h>
#include <sys/types.h>
#include <string>namespace log
{namespace util{class Date{public://获取系统时间static size_t now(){return (size_t)time(nullptr);}};class File{public:// 判断文件是否存在static bool exists(const std::string& pathname){// stat 获取文件属性,获取成功,说明文件存在;获取失败,则文件不存在struct stat st;if (stat(pathname.c_str(), &st) < 0) return false;return true;}// 获取文件的所在目录路径static std::string path(const std::string& pathname){// ./abc/a.txt 其实也就是获取最后一个 / 的位置,然后从开头进行截取size_t pos = pathname.find_last_of("/\\"); // 这个 find 调用在 windows 和 linux 下都可以,linux 下路径分隔符为 /;而 \ 为转义字符// 要表示一个 \ 需要两个 \,也就是查找 / 和 \ 中的任意一个,linux 下路径不会出现 \ ,所以这里没问题if (pos == std::string::npos) return ".";  // 如果没有找到,那么证明这个文件就在当前的目录return pathname.substr(0, pos + 1); // 把最后一个 / 也截进去}// 创建目录static void createDirectory(const std::string& pathname){// ./abc/bcd/cde// pos 是 / 的位置,idx是查找的起始位置size_t pos = 0, idx = 0;while (idx < pathname.size()){// 查找idx 位置起第一个 /size_t pos = pathname.find_first_of("/\\", idx);if (pos == std::string::npos){ // 创建文件夹,pathname.c_str()表示路径名,0777表示权限mkdir(pathname.c_str(), 0755);return;}// 截取从开始到/的目录std::string parent_dir = pathname.substr(0, pos + 1);// 目录存在就跳过,找下一个if (exists(parent_dir) == true){idx = pos + 1;continue;}// 目录不存在,创建目录mkdir(parent_dir.c_str(), 0777);idx = pos + 1;}}};}
}

测试:test.cc

#include "util.hpp"int main()
{std::cout << log::util::Date::now() << std::endl;std::string pathname = "./abc/bcd/a.txt";log::util::File::createDirectory(log::util::File::path(pathname));return 0;
}

image-20240830072836613

日志等级模块设计

  • UNKNOW=0,未知等级的日志

  • DEBUG ,调试等级的日志

  • INFO ,提示等级的日志

  • WARN ,警告等级的日志

  • ERROR ,错误等级的日志

  • FATAL ,致命错误等级的日志

  • OFF,关闭所有⽇志输出

每一个项目中都会设置一个默认的日志输出等级,只有输出的日志等级大于等于默认限制等级的时候才可以进行输出。

提供一个接口,将对应等级的枚举,转换为一个对应的字符串

level.hpp:

/*
1. 定义枚举类,枚举出日志等级
2. 提供转换接口,将枚举转换为对应的字符串
*/#pragma oncenamespace log
{class LogLevel{public:enum class value{UNKNOW = 0,DEBUG,INFO,WARN,ERROR,FATAL,OFF};// 转换接口static const char* toString(LogLevel::value level){switch (level){case LogLevel::value::DEBUG: return "DEBUG";case LogLevel::value::INFO: return "INFO";case LogLevel::value::WARN: return "WARN";case LogLevel::value::ERROR: return "ERROR";case LogLevel::value::FATAL: return "FATAL";case LogLevel::value::OFF: return "OFF";}return "UNKOWN";}};
}

日志消息类设计

意义:存储一条日志消息所需的各项要素

  • 日志的输出时间 – 用于过滤日志输出时间
  • 日志等级 – 用于进行日志过滤分析
  • 源文件名称
  • 源代码行号 – 名称和行号:用于定位出错的代码位置
  • 线程ID – 用于过滤出错的线程
  • 日志主体消息
  • 日志器名称 – 支持多日志器的同时使用

message.hpp:

// 定义日志消息类,进行日志信息的存储#pragma once#include"util.hpp"
#include"level.hpp"
#include <iostream>
#include <string>
#include <thread>namespace log
{struct LogMsg{time_t _ctime;          // 日志产生的时间戳LogLevel::value _level; // 日志等级size_t _line;           // 行号std::thread::id _tid;   // 线程idstd::string _file;      // 文件名std::string _logger;    // 日志器名称std::string _payload;   // 有效载荷数据// 构造函数LogMsg(LogLevel::value level,size_t line,const std::string file,const std::string logger,const std::string msg) : _ctime(util::Date::now()),_level(level),_line(line),_tid(std::this_thread::get_id()),_file(file),_logger(logger),_payload(msg){}};
}

日志格式化类设计

意义:对日志消息进行格式化,并且组织成指定格式的字符串。

%d ⽇期
%T 缩进
%t 线程id
%p ⽇志级别
%c ⽇志器名称
%f ⽂件名
%l ⾏号
%m ⽇志消息
%n 换⾏

[2024-07-09 17:04][root][1234567][main.c:99][FATAL]:\t创建套接字失败…\n

格式化字符串控制了日志的输出格式;定义格式化字符,是为了让日志系统进行日志格式化更加的灵活方便。

⽇志格式化(Formatter)类包含:

  • 格式化字符串:用户定义的输出格式
  • 格式化子项数组:对格式化字符串进行解析,保存了日志信息要素的顺序 – 不同的格式化子项,会从日志消息中取出指定的元素,转化为字符串。

格式化子项:

  • 其他信息(非格式化字符)子项:[
  • 日期子项:%H%M%S
  • 其他信息子项:]
  • 其他信息子项:[
  • 文件名子项:main.c
  • 其他信息子项::
  • 行号信息子项:99
  • 其他信息子项:]
  • 消息主体子项:吃饭睡觉打豆豆
  • 换行子项:\n

[12:40:50][main.c:99]吃饭睡觉打豆豆\n

格式化子项的实现思想:从日志消息中取出指定的元素,追加到一块内存空间中。

设计思想:

  1. 抽象出一个格式化子项的基类

  2. 基于基类,派生出不同的格式化子项子类:

    主体消息、日志等级、时间子项、文件名、行号、日志器名称、线程ID、制表符、换行、非格式化的原始字符串。

    这样就可以在父类中定义父类指针(智能指针)的数组,指向不同的格式化子项子类的对象。

FormatItem类主要负责⽇志消息⼦项的获取及格式化;其包含以下⼦类:

  • MsgFormatItem:表⽰要从LogMsg中取出有效⽇志数据
  • LevelFormatItem:表⽰要从LogMsg中取出⽇志等级
  • NameFormatItem:表⽰要从LogMsg中取出⽇志器名称
  • ThreadFormatItem:表⽰要从LogMsg中取出线程ID
  • TimeFormatItem:表⽰要从LogMsg中取出时间戳并按照指定格式进⾏格式化
  • CFileFormatItem:表⽰要从LogMsg中取出源码所在⽂件名
  • CLineFormatItem:表⽰要从LogMsg中取出源码所在⾏号
  • TabFormatItem:表⽰⼀个制表符缩进
  • NLineFormatItem:表⽰⼀个换⾏
  • OtherFormatItem:表⽰⾮格式化的原始字符串

格式化子项类:

# pragma once#include "level.hpp"
#include "message.hpp"
#include <memory>namespace log
{class FormatItem{public:using ptr = std::shared_ptr<FormatItem>;virtual void format(std::ostream& cout, const LogMsg &msg) = 0;};// 派生格式化子项子类 -- 主体消息、日志等级、时间子项、文件名、行号、日志器名称、线程ID、制表符、换行、 其他// 主体消息class MsgFormatItem : public FormatItem{public:void format(std::ostream& out, const LogMsg& msg) override{out << msg._payload;}};// 日志等级class LevelFormatItem : public FormatItem{public:void format(std::ostream& out, const LogMsg& msg) override{out << LogLevel::toString(msg._level);}};//时间子项class TimeFormatItem:public FormatItem{public:// 设置时间的默认格式TimeFormatItem(const std::string &fmt = "%H:%M:%S") : _time_fmt(fmt) {}//虚函数进行重写void format(std::ostream& out, const LogMsg& msg) override{struct tm t; // 时间结构体localtime_r(&msg._ctime, &t); // 把时间写入 t 中char tmp[32] = { 0 };strftime(tmp, 31, _time_fmt.c_str(), &t); // 把格式化的时间写入 tmp 中out<<tmp;}private:std::string _time_fmt; // 时间格式};// 文件名class FileFormatItem : public FormatItem{public:// 虚函数进行重写void format(std::ostream &out, const LogMsg &msg) override{out << msg._file;}};// 行号class LineFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg) override{out << msg._line;}};// 日志器名称class LoggerFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg) override{out << msg._logger;}};// 线程IDclass ThreadFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg) override{out << msg._tid;}};// 制表符class TabFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg) override{out << "\t";}};// 换行class NLineFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg) override{out << "\n";}};// 其他 -- 输出原始字符串 -- abcdef[%d{...}] -- abcdef[ 就是原始字符串,输出class OtherFormatItem : public FormatItem{public:OtherFormatItem(const std::string &str) : _str(str) {}void format(std::ostream &out, const LogMsg &msg) override{out << _str;}private:std::string _str;};
}

日志格式化类:

parsePattern 函数思想:

image-20240906093915781

补充说明:

  • 原始字符串包含两种,就是 abcd 这种的直接原始字符串,还有 %% 的原始 % 字符串。原始字符串处理完毕后,遇到 % ,则 % 的下一个位置一定是格式化字符。
  • 处理一个格式化字符,会连带着它的子规则一起处理

image-20240906094255944

(nullptr 和 null 就是代表 string 为空 “”)

class Formatter{public:Formatter(const std::string& pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n") : _pattern(pattern) {assert(parsePattern()); // 解析必须成功,不能失败}// 数据放入 io 流中,io 流进行处理void format(std::ostream& out, const LogMsg& msg){// 遍历格式化子项数组for (auto &item : _items){item->format(out, msg); // 从 msg 取信息到 out 中}}// 对 LogMsg 进行格式化,返回一个格式化的字符串std::string format(const LogMsg& msg){std::stringstream ss;format(ss, msg); // 调用的是上面的 format 重载,void format(std::ostream& out, LogMsg& msg)return ss.str();}private://对格式化规则字符串进行解析bool parsePattern(){// 1. 格式化规则字符串解析//abcd[%d{%H:%M:%S}][%p]%T%m%nstd::vector<std::pair<std::string, std::string>> fmt_order;size_t pos = 0;std::string key, val;while (pos < _pattern.size()){// 处理原始字符串 -- 判断是否是 %,不是一定是原始字符if (_pattern[pos] != '%'){val.push_back(_pattern[pos++]);continue;}// 到这里就代表 pos 位置是 % 字符,看后一个是不是 %,%% 处理为一个原始 % 字符if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%'){val.push_back('%');pos += 2;continue;}// 到这里,说明 % 后是个格式化字符;这时候原始字符串已经处理完毕了,把 val push 进去if (val.empty() == false) // 可能出现开头就是格式化字符的情况,所以加判断,如果 val 为空,则不添加 -- 是格式化字符的话,上面不会处理,val 是空的fmt_order.push_back(std::make_pair("", val));val.clear();// 这时候是格式化字符的处理pos += 1; // pos 原本指向 % 位置,+1指向格式化字符的位置if (pos == _pattern.size()){std::cout << "%之后,没有对应的格式化字符!\n";return false;}key = _pattern[pos]; // %d,写入的就是 d,因为上面 pos +1 了pos += 1; // 再往后走一个,看是否有 {,这时候 pos 指向格式化字符后的位置bool error_flag = false;if (pos < _pattern.size() && _pattern[pos] == '{'){// 处理子串pos += 1; // 这时候 pos 指向子规则的起始位置,即 { 的下一个位置while (pos < _pattern.size() && _pattern[pos] != '}'){val.push_back(_pattern[pos++]); // 把子项逐字符放入,带 %}if (pos == _pattern.size()) // 走到末尾跳出循环,则代表没有找到 },代表格式错误{std::cout << "子规则 {} 匹配出错!\n";return false;}pos += 1; // 这时候 pos 指向 } 位置,往后走一步,就走到下次处理的新位置}fmt_order.push_back(std::make_pair(key, val));key.clear();val.clear();}   // 2. 根据解析得到的数据初始化格式化子项数组for (auto& it : fmt_order){_items.push_back(createItem(it.first, it.second));}return true;}// key -- 格式化字符(关键字),val -- 关键字对应值// 例如 %d 对应的子串就是 %H:%M:%S,'[' 则没有 key,就只有 value('[' 本身),这时返回的对象,就用自己填充// 根据不同的格式化字符创建不同的格式化子项对象FormatItem::ptr createItem(const std::string& key,  const std::string& value){if (key == "d") return std::make_shared<TimeFormatItem>(value);if (key == "t")return std::make_shared<ThreadFormatItem>();if (key == "c")return std::make_shared<LoggerFormatItem>();if (key == "f")return std::make_shared<FileFormatItem>();if (key == "l")return std::make_shared<LineFormatItem>();if (key == "p")return std::make_shared<LevelFormatItem>();if (key == "T")return std::make_shared<TabFormatItem>();if (key == "m")return std::make_shared<MsgFormatItem>();if (key == "n")return std::make_shared<NLineFormatItem>();if (key.empty())return std::make_shared<OtherFormatItem>(value); std::cout << "没有对应的格式化字符串:%" << key << std::endl;abort();return FormatItem::ptr();}private:std::string _pattern; // 格式化规则字符串std::vector<FormatItem::ptr> _items; // 格式化子项数组,元素是智能指针};

测试代码:

#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"int main()
{log::LogMsg msg(log::LogLevel::value::INFO, 53, "main.c", "root", "格式化功能测试...");log::Formatter fmt;// 边缘情况测试// log::Formatter fmt("abc%%ab%g%g%gc[%d{%H:%M:%S}]  %m%n"); // 没有 %g 格式字符// log::Formatter fmt("abc%%abc[%d{%H:%M:%S}]%m%n");// 测试 %%// log::Formatter fmt("abc%%abc[%d{%H:%M:%S}]%m%n{"); // 测试花括号// log::Formatter fmt("abc%%abc[%d{%H:%M:%S}]%m%"); // 末尾只带 %,没有格式化字符std::string str = fmt.format(msg);std::cout << str << std::endl;return 0;
}

日志落地类设计 – 简单工厂模式

功能:将格式化后的日志消息字符串,输出到指定的位置

扩展:支持同时将日志落地到不同的位置

位置分类:

  1. 标准输出 – 不常用,测试的时候用
  2. 指定文件(事后进行日志分析)
  3. 滚动文件(文件按时间/大小进行滚动切换)

扩展:支持落地方向的扩展

用户可以自己编写一个新的落地模块,将日志进行其他方向的落地

实现思想:

  1. 抽象出落地模块类
  2. 不同落地方向从基类进行派生(使用基类指针,指向子类对象,就可以调用子类对象的接口进行扩展)
  3. 使用工厂模式进行创建与表示的分离

框架:

amespace log
{class LogSink{public:using ptr = std::shared_ptr<LogSink>;LogSink() {}virtual ~LogSink() {}virtual void log(const char* data, size_t len) = 0; };// 落地方向:标准输出class StdoutSink : public LogSink{public:// 将日志消息写入到标准输出void log(const char* data, size_t len){}};// 落地方向:指定文件class FileSink : public LogSink{public:// 构造时传入文件名,并打开文件,将操作句柄 -- _ofs 管理起来FileSink(const std::string& pathname);void log(const char* data, size_t len){}private:std::string _filename;std::ofstream _ofs; // 输出文件的操作句柄};// 落地方向:滚动文件(以大小滚动)class RollBySizeSink{public:RollBySizeSink(const std::string &basename, size_t max_size);//将日志消息写入到文件,写入前判断文件大小,超过了最大大小,就要切换文件void log(const char* data, size_t len){}private:// 进行大小判断,超过指定大小则创建新文件void createNewFile();private:// 基础文件名 + 扩展文件名(以时间生成 -- 直观,好分析) --》 组成一个实际的当前输出文件名std::string _basename;  // ./log/base-  --> ./log/base-20020809132356.logstd::ofstream _ofs;size_t _max_fsize; // 记录最大大小,当前文件超过了这个大小就要切换文件size_t _cur_size; // 记录当前文件已经写入的大小};// 日志落地的工厂class SinkFackory{};
}

code:

/*日志落地模块的实现
1.抽象落地基类
2.派生子类(根据不同的落地方向进行派生)
3.使用工厂模式进行创建与表示分离
*/#pragma once#include "util.hpp"
#include <iostream>
#include <memory>
#include <fstream>
#include <sstream>
#include <cassert>namespace log
{class LogSink{public:using ptr = std::shared_ptr<LogSink>;LogSink() {}virtual ~LogSink() {}virtual void log(const char* data, size_t len) = 0; };// 落地方向:标准输出class StdoutSink : public LogSink{public:// 将日志消息写入到标准输出void log(const char* data, size_t len){// 因为日志输出不一定是字符串,cout 不能指定大小,字符串有 \0 结尾,别的没有// 因此需要调用write接口,从data位置开始写,写入len长度的数据std::cout.write(data, len);}};// 落地方向:指定文件class FileSink : public LogSink{public:// 构造时传入文件名,并打开文件,将操作句柄 -- _ofs 管理起来FileSink(const std::string& pathname) : _pathname(pathname){// 1. 创建日志文件所在的目录 -- 文件路径可能不存在,以防万一先创建目录util::File::createDirectory(util::File::path(pathname));// 2. 创建并打开日志文件_ofs.open(_pathname, std::ios::binary | std::ios::app); // 二进制可写可追加权限assert(_ofs.is_open());}void log(const char* data, size_t len){_ofs.write(data, len);assert(_ofs.good());}private:std::string _pathname;std::ofstream _ofs; // 输出文件的操作句柄};// 落地方向:滚动文件(以大小滚动)class RollBySizeSink : public LogSink{public:RollBySizeSink(const std::string &basename, size_t max_size):_basename(basename), _max_fsize(max_size), _cur_fsize(0), _name_count(0){std::string pathname = createNewFile();util::File::createDirectory(util::File::path(pathname));_ofs.open(pathname, std::ios::binary | std::ios::app); assert(_ofs.is_open());}//将日志消息写入到文件,写入前判断文件大小,超过了最大大小,就要切换文件void log(const char* data, size_t len){if (_cur_fsize >= _max_fsize){_ofs.close(); // 切换文件前,关闭文件,防止内存泄漏std::string pathname = createNewFile();_ofs.open(pathname, std::ios::binary | std::ios::app);assert(_ofs.is_open());_cur_fsize = 0; }_ofs.write(data, len);assert(_ofs.good());_cur_fsize += len;}private:std::string createNewFile(){// 获取系统时间,以时间来构造文件扩展名time_t t = util::Date::now();struct tm lt;localtime_r(&t, &lt);std::stringstream filename;// 从 1900 年 1.1 开始的,所以对应的加上filename << _basename;filename << lt.tm_year + 1900;filename << lt.tm_mon + 1;filename << lt.tm_mday;filename << lt.tm_hour;filename << lt.tm_min;filename << lt.tm_sec;filename << "-";filename << _name_count++;filename << ".log";return filename.str();}private:// 基础文件名 + 扩展文件名(以时间生成 -- 直观,好分析) --> 组成一个实际的当前输出文件名size_t _name_count; // 用于区别文件名的 -- 因为创建文件可能很快,名字可能会一样std::string _basename;  // ./log/base-  --> ./log/base-20020809132356.logstd::ofstream _ofs;size_t _max_fsize; // 记录最大大小,当前文件超过了这个大小就要切换文件size_t _cur_fsize; // 记录当前文件已经写入的大小};// 日志落地的工厂//SinkType通过模板参数,可以生产我们需要的落地方式,因为不同落地方式构造的参数不一样,所以需要用到不定参class SinkFactory{public:   template<typename SinkType,typename ...Args>  static LogSink::ptr create(Args && ...args){return std::make_shared<SinkType>(std::forward<Args>(args)...);}};
}

测试代码:

#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"int main()
{// 日志落地模块的测试log::LogMsg msg(log::LogLevel::value::INFO, 53, "main.c", "root", "格式化功能测试...");log::Formatter fmt;std::string str = fmt.format(msg);// 设置落地方向log::LogSink::ptr stdout_lsp = log::SinkFactory::create<log::StdoutSink>();                                    // 标准输出落地log::LogSink::ptr file_lsp = log::SinkFactory::create<log::FileSink>("./logfile/test.log");                    // 文件落地方式log::LogSink::ptr roll_lsp = log::SinkFactory::create<log::RollBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式,大小为 1 兆// 通过指针去控制打印的日志stdout_lsp->log(str.c_str(), str.size()); file_lsp->log(str.c_str(), str.size());size_t cursize = 0;size_t count = 0;// 用滚动文件的方法希望生产10个文件,一个文件 1 兆while (cursize < 1024 * 1024 * 10){std::string tmp = std::to_string(count++) + str; // 给日志标号roll_lsp->log(tmp.c_str(), tmp.size());cursize += tmp.size();}return 0;
}

结果:

image-20240909105639755

image-20240909105659711

image-20240909105632408

扩展一个以时间作为日志文件滚动切换类型的日志落地模块:

在 test.cc 中写的代码:

/*扩展一个以时间作为日志文件滚动切换类型的日志落地模块1.以时间进行文件滚动,实际上是以时间段进行滚动实现思想:以当前系统时间,取模获得时间段大小,可以得到当前时间段是第几个时间段time(nullptr)%gap;每次以当前系统时间取模,判断与当前文件的时间段是否一致,不一致代表不是同一个时间段*/
// 使用枚举来确定时间段的大小
enum class TimeGap
{GAP_SECOND,GAP_MINUTE,GAP_HOUR,GAP_DAY,
};class RollByTimeSink : public log::LogSink
{
public:RollByTimeSink(const std::string &basename, TimeGap gap_type) : _basename(basename){switch (gap_type){case TimeGap::GAP_SECOND:_gap_size = 1;break; // 以秒为时间段case TimeGap::GAP_MINUTE:_gap_size = 60;break; // 以分钟为时间段case TimeGap::GAP_HOUR:_gap_size = 3600;break; // 以小时为时间段case TimeGap::GAP_DAY:_gap_size = 3600 * 24;break; // 以天为时间段}_cur_gap = _gap_size==1?log::util::Date::now():log::util::Date::now() / _gap_size; // 获取当前是第几个时间段;前面是 _gap_size == 1 的情况// 创建文件std::string filename = createNewFile();log::util::File::createDirectory(log::util::File::path(filename));_ofs.open(filename, std::ios::binary | std::ios::app); assert(_ofs.is_open());}// 将日志消息写入到标准输出,判断当前时间是否是当前文件的时间段,不是就要切换文件。void log(const char *Date, size_t len){time_t cur = log::util::Date::now(); // 获取当前系统时间,时间戳if ((cur / _gap_size) != _cur_gap)//(每次写日志时判断当前的时间段与上次的时间段是否是一致得,一致的话就写入,不一致就创建新文件){_ofs.close();                                          // 打开文件,就必须关闭文件(这里关闭以前的文件)std::string pathname = createNewFile();                // 创建新文件_cur_gap = _gap_size==1?log::util::Date::now():log::util::Date::now() / _gap_size; // 获取当前是第几个时间段_ofs.open(pathname, std::ios::binary | std::ios::app); // 打开文件 二进制可写可追加权限assert(_ofs.is_open());                                // 打开失败就报错}_ofs.write(Date, len);assert(_ofs.good()); // 检测文件流状态和文件读写过程是否正常}protected:// 创建一个新文件,不需要用户去创建,所有我们把权限设置为私有std::string createNewFile(){// 获取系统时间,以时间来构造文件名的扩展名time_t t = log::util::Date::now();struct tm lt;localtime_r(&t, &lt);std::stringstream filename;filename << _basename;filename << lt.tm_year + 1900;filename << lt.tm_mon + 1;filename << lt.tm_mday;filename << lt.tm_hour;filename << lt.tm_min;filename << lt.tm_sec;filename << ".log";return filename.str();}
private:std::string _basename; // 基本文件名std::ofstream _ofs;    // 会默认以写的方式打开文件size_t _cur_gap;       // 当前是第几个时间段size_t _gap_size;      // 时间段的大小
};

三秒,写三个文件:

image-20240911115731393

日志器模块设计 – 涉及建造者模式

功能:对前面所有功能进行整合,向外提供接口完成不同等级日志的输出。

管理的成员:

  1. 格式化模块对象
  2. 落地模块对象
  3. 默认的日志输出限制等级(大于等于限制输出等级的日志才能输出)
  4. 互斥锁(保证日志输出过程的线程安全,不会出现交叉日志)
  5. 日志名称(日志器的唯一标识,方便查找)

提供的操作:

  • debug等级日志的输出操作(分别封装日志消息LogMsg——各个接口日志等级不同)
  • info等级日志的输出操作
  • warn等级日志的输出操作
  • error等级日志的输出操作
  • fatal等级日志的输出操作

实现:

  1. 实现Logger基类(派生出同步日志器和异步日志器)
  2. 因为两种日志器的落地方式不同,需要将落地操作给抽象出来,不同的日志器调用不同的落地操作进行日志落地
  3. 模块关联过程中使用基类指针对子类日志器对象进行日志管理和操作

当前日志系统支持同步日志&异步日志,它们的不同点在于日志的落地方式上不同:

  • 同步日志器:直接对日志消息进行输出
  • 异步日志器:先将日志消息放到缓冲区,然后异步线程进行输出

因此 :日志器类在设计的时候,先要设计一个Logger的基类,在Logger基类的基础上,继承出同步日志器(SyncLogger)和异步日志器(AsyncLoggrr)。

同步日志器

在这里插入图片描述

框架:

/*完成日志器模块1. 抽象日志器基类2. 派生出不同的子类(同步日志器嘞 & 异步日志器类)
*/#pragma once
#include "util.hpp"
#include "level.hpp"
#include "format.hpp"
#include "sink.hpp"
#include <thread>
#include <atomic>
#include <mutex>namespace log
{class Logger{public:using ptr = std::shared_ptr<Logger>;void debug(const std::string& file, size_t line, const std::string& fmt, ...);void info(const std::string& file, size_t line, const std::string& fmt, ...);void warn(const std::string& file, size_t line, const std::string& fmt, ...);void error(const std::string& file, size_t line, const std::string& fmt, ...);void fatal(const std::string& file, size_t line, const std::string& fmt, ...);protected:// 抽象接口完成实际的落地输出 -- 不同日志器会有不同的实际落地方式 virtual void log(const char* data, size_t len) = 0;private:std::mutex _mutex; // 互斥锁std::string _logger_name; // 日志器名称std::atomic<LogLevel::value> _limit_level; // 限制日志等级, atomic原子操作Formatter::ptr _formatter; // 格式化模块对象std::vector<LogSink::ptr> _sinks; // 用一个数组来存放日志落地方向 -- 一个日志器中可能有多个日志落地方向};class SynLogger : public Logger{protected:void log(const char* data, size_t len);};
}

Logger.hpp:

/*完成日志器模块1. 抽象日志器基类2. 派生出不同的子类(同步日志器嘞 & 异步日志器类)
*/#pragma once#include "util.hpp"
#include "level.hpp"
#include "sink.hpp"
#include "format.hpp"
#include <memory>
#include <mutex>
#include <atomic>
#include <cstdarg>namespace log
{//  设计日志器基类class Logger{// 公有public:// 基类指针,用来控制继承子类的对象using ptr = std::shared_ptr<Logger>;// 构造函数Logger(const std::string &logger_name, LogLevel::value level, Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks) : _logger_name(logger_name), _limit_level(level), _formatter(formatter), _sliks(sinks.begin(), sinks.end()) {}// 操作方法//获取日志器名称const std::string& name(){return _logger_name;}// 构造日志消息对象并进行格式化,得到格式化后的日志消息字符串--然后进行落地输出,5个等级void debug(const std::string &file, size_t line, const std::string &fmt, ...){ // 日志的输出操作// 1.判断当前的日志是否达到输出等级,没达到等级就 return if (LogLevel::value::DEBUG < _limit_level){return;} // 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串va_list ap;va_start(ap, fmt);char *res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1){std::cout << "vasprintf failed!!\n";return;}va_end(ap); // 代码一样,可以封装成一个函数serialize(LogLevel::value::DEBUG, file, line, res);free(res); // 将指针释放掉,防止内存泄漏}void info(const std::string &file, size_t line, const std::string &fmt, ...){ // 日志的输出操作// 1.判断当前的日志是否达到输出等级if (LogLevel::value::INFO < _limit_level){return;} // 没有达到输出等级// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串va_list ap;va_start(ap, fmt);char *res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1){std::cout << "vasprintf failed!!\n";return;}va_end(ap); // 将ap指针置空// 代码一样,可以封装成一个函数serialize(LogLevel::value::INFO, file, line, res);free(res); // 将指针释放掉}void warn(const std::string &file, size_t line, const std::string &fmt, ...){ // 日志的输出操作// 1.判断当前的日志是否达到输出等级if (LogLevel::value::WARN < _limit_level){return;} // 没有达到输出等级// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串va_list ap;va_start(ap, fmt);char *res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1){std::cout << "vasprintf failed!!\n";return;}va_end(ap); // 将ap指针置空// 代码一样,可以封装成一个函数serialize(LogLevel::value::WARN, file, line, res);free(res); // 将指针释放掉}void error(const std::string &file, size_t line, const std::string &fmt, ...){ // 日志的输出操作// 1.判断当前的日志是否达到输出等级if (LogLevel::value::ERROR < _limit_level){return;} // 没有达到输出等级// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串va_list ap;va_start(ap, fmt);char *res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1){std::cout << "vasprintf failed!!\n";return;}va_end(ap); // 将ap指针置空// 代码一样,可以封装成一个函数serialize(LogLevel::value::ERROR, file, line, res);free(res); // 将指针释放掉}void fatal(const std::string &file, size_t line, const std::string &fmt, ...){ // 日志的输出操作// 1.判断当前的日志是否达到输出等级if (LogLevel::value::FATAL < _limit_level){return;} // 没有达到输出等级// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串va_list ap;va_start(ap, fmt);char *res;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1){std::cout << "vasprintf failed!!\n";return;}va_end(ap); // 将ap指针置空// 代码一样,可以封装成一个函数serialize(LogLevel::value::FATAL, file, line, res);free(res); // 将指针释放掉}void serialize(LogLevel::value level, const std::string &file, size_t line, char *str){// 3.构造LogMsg对象LogMsg msg(level, line, file, _logger_name, str); // 传入等级、行号、文件、日志器、有效信息// 4.通过格式化工具对LogMsg进行格式化,得到格式化后的日志字符串std::stringstream ss;_formatter->format(ss, msg);// 5.进行日志落地log(ss.str().c_str(), ss.str().size()); // 日志字符串和长度,调用派生类的 log}// 日志落地,抽象接口完成实际的落地输出——不同的日志器会有不同的实际落地方式virtual void log(const char *data, size_t len) = 0;protected:std::mutex _mutex;                         // 互斥锁std::string _logger_name;                  // 日志器的名字std::atomic<LogLevel::value> _limit_level; // 限制日志等级,atomic原子操作的意思是该操作执行过程中不能被中断,该操作要么不执行,要么全部执行,不存在执行一部分的情况。Formatter::ptr _formatter;                 // 控制格式化模块的对象std::vector<LogSink::ptr> _sliks;          // 这是一个数组,数组里存放日志落地方式的对象};// 派生出同步日志器class SyncLogger : public Logger{public:// 构造函数SyncLogger(const std::string &logger_name, LogLevel::value level, Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks) : Logger(logger_name, level, formatter, sinks) {}protected:// 重写虚函数,同步日志器是将日志通过落地模块句柄进行日志落地void log(const char *data, size_t len){std::unique_lock<std::mutex> lock(_mutex);// 是空if (_sliks.empty()){return;}// 不是空for (auto &sink : _sliks){sink->log(data, len); // 调用 sink 中的 log}}};
}

测试代码:

// 测试代码
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "logger.hpp"
#include <unistd.h>int main()
{// 日志器模块:同步日志器std::string logger_name = "sync_logger";log::LogLevel::value limit = log::LogLevel::value::WARN;log::Formatter::ptr fmt(new log::Formatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"));log::LogSink::ptr stdout_lsp = log::SinkFactory::create<log::StdoutSink>();                                 // 标准输出落地log::LogSink::ptr file_lsp = log::SinkFactory::create<log::FileSink>("./logfile/test.log");                 // 文件落地方式log::LogSink::ptr roll_lsp = log::SinkFactory::create<log::RollBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式std::vector<log::LogSink::ptr> sinks = {stdout_lsp, file_lsp, roll_lsp};log::Logger::ptr logger(new log::SyncLogger(logger_name, limit, fmt, sinks));logger->debug(__FILE__, __LINE__, "%s", "测试日志");logger->info(__FILE__, __LINE__, "%s", "测试日志");logger->warn(__FILE__, __LINE__, "%s", "测试日志");logger->error(__FILE__, __LINE__, "%s", "测试日志");logger->fatal(__FILE__, __LINE__, "%s", "测试日志");size_t cursize = 0;size_t count = 0;while (cursize < 1024 * 1024 * 10){logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);cursize += 20;}return 0;
}

image-20240913093154108

image-20240913093518190

日志器建造者类:

框架:

// 使用建造者模式来建造日志器,而不要让用户直接去构造日志器,简化用户的使用成本 // 1. 抽象一个日志器建造者类//     1 - 设置日志器类型//     2 - 将不同类型日志器的创建都放到同一个日志器建造者类中完成 enum class LoggerType{LOGGER_SYNC, // 同步日志器LOGGER_ASYNC // 异步日志器};class LoggerBuilder{public:void buildLoggerType(LoggerType type);void buildLoggerName(std::string &name);void buildLoggerLevel(LogLevel::value level);// 构造一个格式化器void buildLoggerFormatter(const std::string &pattern);// 一个日志器可以有多个不同的落地方式template <typename SinkType, typename... Args>void buildSink(Args &&...arg);// 完成我们的日志器构建virtual Logger::ptr build() = 0;protected:LoggerType _logger_type;std::string _logger_name;LogLevel::value _limit_level;Formatter::ptr _formatter;std::vector<LogSink::ptr> _sinks;};// 2.派生出具体的建造者类——局部日志器的建造者 & 全局日志器建造者(后边添加了全局单例管理之后,将日志器添加全局管理) // 局部日志器的建造者class LocalLoggerBuilder : public LoggerBuilder{public:Logger::ptr build() override;};

code:

enum class LoggerType{LOGGER_SYNC, // 同步日志器LOGGER_ASYNC // 异步日志器};class LoggerBuilder{public:LoggerBuilder() :_logger_type(LoggerType::LOGGER_SYNC),_limit_level(LogLevel::value::DEBUG){}void buildLoggerType(LoggerType type) { _logger_type = type;}void buildLoggerName(std::string &name) {_logger_name = name;}void buildLoggerLevel(LogLevel::value level){_limit_level = level;}// 构造一个格式化器void buildLoggerFormatter(const std::string &pattern){_formatter = std::make_shared<Formatter>(pattern);}// 一个日志器可以有多个不同的落地方式template <typename SinkType, typename... Args>void buildSink(Args &&...arg){LogSink::ptr psink = SinkFactory::create<SinkType>(std::forward<Args>(args)...);_sinks.push_back(psink);}// 完成我们的日志器构建virtual Logger::ptr build() = 0;protected:LoggerType _logger_type;std::string _logger_name;LogLevel::value _limit_level;Formatter::ptr _formatter;std::vector<LogSink::ptr> _sinks;};// 2.派生出具体的建造者类——局部日志器的建造者 & 全局日志器建造者(后边添加了全局单例管理之后,将日志器添加全局管理) // 局部日志器的建造者class LocalLoggerBuilder : public LoggerBuilder{public:Logger::ptr build() override{// 必须要有日志器名称assert(_logger_name.empty());// 必须要有格式化器,没有就要创建if (_formatter.get() == nullptr){_formatter = std::make_shared<Formatter>();}// 如果没有落地方式,默认添加一个标准输出的落地方式if (_sinks.empty()){buildSink<StdoutSink>();}// 类型为异步日志器if (_logger_type == LoggerType::LOGGER_ASYNC){// 之后写}return std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);}};
}

我们这个建造者模式没有指挥者。因为我们构造对象的零部件没有顺序的要求,只管构造就可以了,所有只要建造者。

测试代码:

#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "logger.hpp"
#include <unistd.h>
int main()
{//同步日志器建造者模式的测试//先要构造一个建造者出来std::unique_ptr<log::LoggerBuilder> builder(new log::LocalLoggerBuilder());//建造者构建零部件builder->buildLoggerType(log::LoggerType::LOGGER_SYNC);builder->buildLoggerName("sync_logger");builder->buildLoggerLevel(log::LogLevel::value::WARN);builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");builder->buildSink<log::StdoutSink>();                                 // 标准输出落地builder->buildSink<log::FileSink>("./logfile/test.log"); // 文件落地方式builder->buildSink<log::RollBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式//零部件构建好后,用建造者建筑对象log::Logger::ptr logger=builder->build();//测试日志打印logger->debug(__FILE__, __LINE__, "%s", "测试日志");logger->info(__FILE__, __LINE__, "%s", "测试日志");logger->warn(__FILE__, __LINE__, "%s", "测试日志");logger->error(__FILE__, __LINE__, "%s", "测试日志");logger->fatal(__FILE__, __LINE__, "%s", "测试日志");size_t cursize = 0;size_t count = 0;while (cursize < 1024 * 1024 * 10){logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);cursize += 20;}return 0;
}

异步日志器

思想:为了避免写日志的过程中阻塞,导致影响业务线程的执行效率。异步的思想就是不让业务线程进行日志的实际落地,而是将日志消息放到缓冲区(一块指定的内存)中接下来有一个专门的异步线程,去针对缓冲区中的数据进行处理(实际的落地操作)

实现:

  1. 实现一个线程安全的缓冲区
  2. 创建一个异步工作线程,专门用来负责缓冲区中日志信息的落地操作。

在这里插入图片描述

缓冲区详情设计:

1.使用队列缓存日志消息,逐条处理

  • 要求:不能涉及空间的频繁申请与释放,否则会降低效率。
  • 结果:设计一个环形队列(提前将空间申请好,然后对空间循环利用)
  • 存在问题:这个缓冲区的操作会涉及到多线程,因此缓冲区的操作必须保证线程安全。
  • 线程安全实现:对于缓冲区的读写加锁
  • 因此写日志操作,在实际开发中,不好分配太多资源,工作线程只需要一个日志器就行
  • 涉及到的锁冲突:生产者与生产者之间的互斥&生产者与消费者的互斥。
  • 问题:锁冲突较为严重,所有线程之间都存在互斥关系
  • 解决方案:双缓冲区

两个缓冲区,写入和处理:

在这里插入图片描述

固定时间,对两个缓冲区做交换。

单个缓冲区设计

设计一个缓冲区:直接存放格式化后的日志消息字符串,而不是放的 LogMsg 都西昂

好处:

  1. 减少了LogMsg对象频繁的构造的消耗
  2. 可以针对缓冲区中的日志消息,一次性进行IO操作,减少IO次数,提高效率

缓冲区类的设计:

1.管理一个存放字符串数据的缓冲区(使用vecotor进行空间管理)
2.当前的写入数据位置的指针(指向可写区域的起始位置,避免数据的写入覆盖)
3.当前的读取数据位置的指针(指向可读数据区域的起始位置,当读取指针与写入指针指向相同位置

提供的操作:

1.向缓冲区写入数据
2.获取可读数据起始地址的接口
3.获取可读数据长度的接口
4.移动读写位置的接口
5.初始化缓冲区的操作(将读写位置初始化——将一个缓冲区所有数据处理完毕之后)
6.提供交换缓冲区的接口(交换空间地址,并不交换空间数据)

框架:

/*实现异步日志缓冲区*/
#include "util.hpp"
#include <vector>
namespace logslearn
{
// 定义宏,表示缓冲区的大小
#define DEFAULT_BUFFER_SIZE (1 * 1024 * 1024)
#define THRESHOLD_BUFFER_SIZE (8 * 1024 * 1024)
#define INCREMENT_BUFFER_SIZE (1 * 1024 * 1024)// 异步缓冲区class Buffer{public:// 构造函数Buffer() {}// 1.向缓冲区写入数据void push(const char *data, size_t len);// 2.返回可读数据起始地址的接口const char *begin();// 3.返回可读数据的长度的接口;返回可写数据的长度的接口size_t readAbleSize();size_t writeAbleSize();// 4.移动读写指针进行向后偏移的接口void moveWriter(size_t len);void moveReader(size_t len);// 5.重置读写位置,初始化缓冲区的操作void reset();// 6.交换缓冲区的接口void swap( Buffer &buffer);// 判断缓冲区是否为空bool empty();private:// 1.存放字符串数据的缓冲区std::vector<char> _buffer;// 2.当前可写数据的指针--本质是下标size_t _reader_idx;// 3.当前可读数据的指针size_t _writer_idx;};
}

buffer.hpp:

// 实现异步日志缓冲区
#pragma once#include "util.hpp"
#include <vector>
#include <cassert>namespace log
{#define DEFAULT_BUFFER_SIZE (10 * 1024 * 1024)#define THRESHOLD_BUFFER_SIZE (80 * 1024 * 1024) // 阈值大小,小于阈值翻倍增长,达到阈值线性增长#define INCREMENT_BUFFER_SIZE (1 * 1024 * 1024)class Buffer{public:Buffer() : _buffer(DEFAULT_BUFFER_SIZE), _reader_idx(0), _writer_idx(0) {}// 向缓冲区写入数据,容量不够就扩容(两种方式,极限测试的时候使用扩容,实际使用过程中固定空间大小,空间不够阻塞)void push(const char* data, size_t len){// 缓冲区剩余空间不够的情况下:扩容。// // 1.固定大小,直接返回// if (len > writeAbleSize())//     return;// 2.动态空间,用于极限测试--扩容ensureEnoughSize(len);// 将数据拷贝到缓冲区std::copy(data, data + len, &_buffer[_writer_idx]);// 将写入位置向后偏移moveWriter(len);}// 返回可读数据起始地址的接口const char *begin(){return &_buffer[_reader_idx];}// 返回可读取数据的长度;返回可写数据的长度size_t readAbleSize(){return (_writer_idx - _reader_idx);}size_t writeAbleSize(){// 对于扩容的思路来说,不存在可写空间大小,因为总是可写的。// 因此这个接口只提供给固定大小缓冲区。return (_buffer.size() - _writer_idx);}// 移动读写指针进行向后偏移的接口void moveWriter(size_t len){assert((len + _writer_idx) <= _buffer.size());_writer_idx += len;}void moveReader(size_t len){assert(len <= readAbleSize());_reader_idx += len;}// 重置读写位置,初始化缓冲区void reset(){// 读写为 0_writer_idx = 0; // 缓冲区所有空间都是空闲的_reader_idx = 0; //_reader_idx与_writer_idx相等就表示没有数据可以读}// 对 Buffer 实现交换操作void swap(Buffer& buffer){_buffer.swap(buffer._buffer);std::swap(_reader_idx, buffer._reader_idx);std::swap(_writer_idx, buffer._writer_idx);}// 判断缓冲区是否为空bool empty(){return (_reader_idx == _writer_idx);}private:void ensureEnoughSize(size_t len){// 不需要扩容if (len < writeAbleSize()) return ;size_t new_size = 0;if (_buffer.size() < THRESHOLD_BUFFER_SIZE){// 小于阈值翻倍增长new_size = _buffer.size() * 2;} else{// 否则线性增长new_size = _buffer.size() + INCREMENT_BUFFER_SIZE;}// 重新调整空间大小_buffer.resize(new_size);}private:// 存放字符串数据的缓冲区std::vector<char> _buffer;// 当前可写数据的指针--本质是下标size_t _reader_idx;// 当前可读数据的指针size_t _writer_idx;};
}

测试代码:

测试思想:读取文件数据,一点一点写入缓冲区,最终将缓冲区数据写入文件,判断生成的新文件是否和读取的文件一致

code:

int main()
{// 异步日志器缓冲区测试// 读取文件数据,一点一点写入缓冲区,最终将缓冲区数据写入文件,判断生成的新文件与源文件是否一致std::ifstream ifs("./logfile/test.log", std::ios::binary); // 打开一个文件if (ifs.is_open() == false){return -1;} // 文件打开失败返回-1// 让读写位置跳转到末尾ifs.seekg(0, std::ios::end);// 获取当前读写位置相对于起始位置的偏移量size_t fsize = ifs.tellg();// 重新让指针跳转到起始位置ifs.seekg(0, std::ios::beg);std::string body;body.resize(fsize);ifs.read(&body[0], fsize);if (ifs.good() == false){std::cout << "read error!\n";return -1;}// 打开文件,也要关闭ifs.close();std::cout << fsize << std::endl; // 读取文件的数据大小log::Buffer buffer;              // 定义一个缓冲区for (int i = 0; i < body.size(); i++){buffer.push(&body[i], 1);}std::cout << buffer.readAbleSize() << std::endl; // buffer里面可读的数据大小std::ofstream ofs("./logfile/tem.log", std::ios::binary);size_t rsize = buffer.readAbleSize();for (int i = 0; i < rsize; i++){ ofs.write(buffer.begin(), 1);if (ofs.good() == false){std::cout << "writer error!\n";return -1;}buffer.moveReader(1);}ofs.close(); // 关闭文件return 0;
}

image-20240918185546248

md5sum 可以验证文件的完整性,tem.log和test.log文件的md5值一样,说明文件内容一模一样。

异步工作器的设计 – 双缓冲区思想

异步工作器:

异步工作使用双缓冲区的思想

  • 外界将任务数据,添加到输入缓冲区中
  • 异步线程对处理缓冲区中的数据进行处理,若处理缓冲区中没有数据了则交换缓冲区

管理的成员:

1.双缓冲区(生产,消费)
2.互斥锁 – 保证线程安全
3.条件变量-生产&消费(生产缓冲区没有数据,处理完消费缓冲区数据后就休眠)
4.回调函数(针对缓冲区中数据的处理接口-外界传入一个函数,告诉异步工作器数据该如何处理)

  • 提供的操作:

    a.停止异步工作器

    b.添加数据到缓冲区

  • 私有操作:

    创建线程,线程入口函数中,交换缓冲区,对消费缓冲区数据使用回调函数进行处理,处理完后再次交换

框架:

/*实现异步工作器
*/
#pragma once
#include "buffer.hpp"         //缓冲区
#include <thread>             //线程库
#include <mutex>              //互斥锁
#include <condition_variable> //条件变量
#include <functional>         //包装器
#include <atomic>             //原子类型
namespace logslearn
{// 异步工作器类using Functor = std::function<void(Buffer &)>;class AsyncLooper{public:using ptr = std::shared_ptr<AsyncLooper>;AsyncLooper();void stop();void push(const char *data, size_t len);private:void threadEntry(); // 线程入口函数private:Functor _callBack; // 具体对缓冲区数据进行处理的回调函数,由异步工作器使用者传入。private:std::atomic<bool> _stop;           // 让工作器停止标准变成原子性操作,提高了线程安全Buffer _pro_buf;                   // 生产缓冲区Buffer _con_buf;                   // 消费缓冲区std::mutex _mutex;                 // 互斥锁std::condition_variable _cond_pro; // 两个pcb的等待队列,这是生产者,等待队列的条件变量std::condition_variable _cond_con; // 这是消费者,等待队列的条件变量std::thread _thread;               // 异步工作器对应的工作线程};
}

looper.hpp:

/*实现异步工作器
*/
#pragma once#include "buffer.hpp"         //缓冲区
#include <thread>             //线程库
#include <mutex>              //互斥锁
#include <condition_variable> //条件变量
#include <functional>         //包装器
#include <atomic>             //原子类型namespace log
{// 异步工作器类using Functor = std::function<void(Buffer &)>;enum class AsyncType{ASYNC_SAFE,  // 安全状态,表示缓冲区满了就阻塞,避免了资源耗尽的风险ASUNC_UNSAFE // 非安全状态,不考虑资源耗尽的情况,可以无限扩容,常用与测试};class AsyncLooper{public:using ptr = std::shared_ptr<AsyncLooper>;AsyncLooper(const Functor &cb, AsyncType looper_type = AsyncType::ASUNC_UNSAFE) : _looper_type(looper_type), _stop(false), _thread(std::thread(&AsyncLooper::threadEntry, this)),_callBack(cb) {}~AsyncLooper() { stop(); }void stop() {_stop = true;_cond_con.notify_all(); // 唤醒所有的工作线程_thread.join(); // 等待工作线程退出}void push(const char *data, size_t len){// 1.无限扩容-非安全(极限压力测试的情况下使用)2.固定大小std::unique_lock<std::mutex> lock(_mutex);// 条件变量空值,若缓冲区剩余空间大小大于数据长度,就可以添加数据// 如果是安全状态就把这个代码加上,非安全状态就把这个代码屏蔽if (_looper_type == AsyncType::ASYNC_SAFE)_cond_pro.wait(lock, [&](){ return _pro_buf.writeAbleSize() >= len; }); // 被唤醒之后,如果缓冲区有剩余空间,则添加数据;否则继续休眠(lambda表达式结果为假)// 能够走下来代表满足了条件,可以向缓冲区添加数据_pro_buf.push(data, len);// 唤醒一个消费者对缓冲区中的数据进行处理_cond_con.notify_one();}private:void threadEntry() // 线程入口函数{while (!_stop){// 要为互斥锁设置一个生命周期,将缓冲区交换完毕后就解锁(不对数据的处理过程加锁保护){// 1.判断生产缓冲区里有没有数据,有则交换,无则阻塞std::unique_lock<std::mutex> lock(_mutex);//退出标志被设置,且生产缓冲区无数据,这时候在退出,否则有可能会造成生产缓冲区有数据,但是没有被完全处理                   if (_stop && _pro_buf.empty()) break;// 若退出前被唤醒,或者有数据被唤醒,则返回真,继续向下运行,否则重新陷入休眠_cond_con.wait(lock, [&](){ return !_pro_buf.empty() || _stop; }); //_stop是真表示程序退出,把剩余的数据进行交换// 等待完毕,消费者与生产者进行地址交换_con_buf.swap(_pro_buf);// 2.唤醒生产者if (_looper_type == AsyncType::ASYNC_SAFE)_cond_pro.notify_all();}// 3.被唤醒后,对消费缓冲区进行数据处理_callBack(_con_buf);// 4.初始化消费缓冲区_con_buf.reset();}}private:Functor _callBack; // 具体对缓冲区数据进行处理的回调函数,由异步工作器使用者传入。private:AsyncType _looper_type;            // 默认是安全模式std::atomic<bool> _stop;           // 让工作器停止标志变成原子性操作,提高了线程安全Buffer _pro_buf;                   // 生产缓冲区Buffer _con_buf;                   // 消费缓冲区std::mutex _mutex;                 // 互斥锁std::condition_variable _cond_pro; // 生产者,等待队列的条件变量std::condition_variable _cond_con; // 消费者,等待队列的条件变量std::thread _thread;               // 异步工作器对应的工作线程};
}
异步日志器设计

1.继承于Logger日志器类 对于写日志操作进行函数重写(不再将数据写入文件,而是通过异步消息处理器,放到缓冲区中)
2.通过异步消息处理器,进行日志数据的实际落地

管理成员:

异步工作器(异步消息处理器)
完成后,完善日志器建造者,进行异步日志器安全模式的选择,提供异步日志器的创建

框架:

//派生出异步日志器class AsyncLogger:public Logger{public:AsyncLogger( const std::string &logger_name, LogLevel::value level, Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks,AsyncType looper_type) :Logger(logger_name, level, formatter, sinks) {}//将数据写入缓冲区void log(const char*data,size_t len);//设计一个实际落地函数(将缓冲区里的数据进行落地)void realLog(Buffer &buf);private:AsyncLooper::ptr _looper;};

完善日志器建造者功能:

// 使用建造者模式来建造日志器,而不要让用户直接去构造日志器,简化用户的使用成本// 1. 抽象一个日志器建造者类//     1 - 设置日志器类型//     2 - 将不同类型日志器的创建都放到同一个日志器建造者类中完成enum class LoggerType{LOGGER_SYNC, // 同步日志器LOGGER_ASYNC // 异步日志器};class LoggerBuilder{public:LoggerBuilder() : _logger_type(LoggerType::LOGGER_SYNC),_limit_level(LogLevel::value::DEBUG),_looper_type(AsyncType::ASYNC_SAFE) {}void buildLoggerType(LoggerType type){_logger_type = type;}void buildLoggerName(const std::string &name){_logger_name = name;}void buildLoggerLevel(LogLevel::value level){_limit_level = level;}void buildEnableUnSafeAsync(){_looper_type = AsyncType::ASUNC_UNSAFE;}// 构造一个格式化器void buildLoggerFormatter(const std::string &pattern){_formatter = std::make_shared<Formatter>(pattern);}// 一个日志器可以有多个不同的落地方式template <typename SinkType, typename... Args>void buildSink(Args &&...args){LogSink::ptr psink = SinkFactory::create<SinkType>(std::forward<Args>(args)...);_sinks.push_back(psink);}// 完成我们的日志器构建virtual Logger::ptr build() = 0;protected:AsyncType _looper_type;LoggerType _logger_type;std::string _logger_name;LogLevel::value _limit_level;Formatter::ptr _formatter;std::vector<LogSink::ptr> _sinks;};// 2.派生出具体的建造者类——局部日志器的建造者 & 全局日志器建造者(后边添加了全局单例管理之后,将日志器添加全局管理)// 局部日志器的建造者class LocalLoggerBuilder : public LoggerBuilder{public:Logger::ptr build() override{// 必须要有日志器名称assert(_logger_name.empty() == false);// 必须要有格式化器,没有就要创建if (_formatter.get() == nullptr){_formatter = std::make_shared<Formatter>();}// 如果没有落地方式,默认添加一个标准输出的落地方式if (_sinks.empty()){buildSink<StdoutSink>();}// 类型为异步日志器if (_logger_type == LoggerType::LOGGER_ASYNC){// 返回异步日志器对象return std::make_shared<AsyncLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);}return std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);}};
}

测试代码:

int main()
{//异步日志器的测试//异步日志器和异步工作器进行联调// //先要构造一个建造者出来std::unique_ptr<log::LoggerBuilder> builder(new log::LocalLoggerBuilder());//建造者构建零部件builder->buildLoggerType(log::LoggerType::LOGGER_ASYNC);builder->buildLoggerName("async_logger");builder->buildLoggerLevel(log::LogLevel::value::WARN);builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");builder->buildSink<log::StdoutSink>();                                 // 标准输出落地builder->buildSink<log::FileSink>("./logfile/async.log"); // 文件落地方式// builder->buildSink<log::RoolBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式//零部件构建好后,用建造者建筑对象log::Logger::ptr logger=builder->build();//测试日志打印logger->debug(__FILE__, __LINE__, "%s", "测试日志");logger->info(__FILE__, __LINE__, "%s", "测试日志");logger->warn(__FILE__, __LINE__, "%s", "测试日志");logger->error(__FILE__, __LINE__, "%s", "测试日志");logger->fatal(__FILE__, __LINE__, "%s", "测试日志");size_t count = 0;while (count < 100000){logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);}return 0;
}

image-20240921082156581

日志器管理模块 – 单例模式

⽇志的输出,我们希望能够在任意位置都可以进⾏,但是当我们创建了⼀个⽇志器之后,就会受到⽇志器所在作⽤域的访问区域限制。

因此,为了突破访问区域的限制,我们创建⼀个⽇志器管理类,且这个类是⼀个单例类,这样的话, 我们就可以在任意位置来通过管理器单例获取到指定的⽇志器来进⾏⽇志输出了。

日志器管理器:

作用1:对所有创建的日志器进行管理
特性:将管理器设计为单例

作用2:可以在程序的任意位置,获取相同的单例对象,获取其中的日志器进行日志输出

拓展:单例管理器创建的时候,默认先创建一个日志器(用于进行标准输出的打印)

拓展目的:让用户在不创建任何日志器的情况下,也能进行标准输出的打印,方便用户使用

设计:
管理的成员:

1.默认日志器
2.所管理的日志器数组
3.互斥锁

提供的接口:

1添加日志器管理
2.判断是否管理了指定名称的日志器
3.获取指定名称的日志器
4.获取默认日志器

日志管理器设计

在 looger.hpp 中:

框架:

    // 日志器管理模块class LoggerManager{public:// 1添加日志器管理void addLogger(Logger::ptr &logger);// 2.判断是否管理了指定名称的日志器bool hasLogger(const std::string &name);// 3.获取指定名称的日志器Logger::ptr getLogger(const std::string &name);// 4.获取默认日志器Logger::ptr rootLogger();// 5. 获取单例句柄static LoggerManager &getInstance();private:// 构造函数私有化LoggerManager() {}private:// 1.默认日志器Logger::ptr _root_logger;// 2.所管理的日志器 -- 日志器名称和日志器对象(智能指针)的映射std::unordered_map<std::string, Logger::ptr> _loggers;// 3.互斥锁std::mutex _mutex;};

code:

// 日志器管理模块class LoggerManager{public:// 1添加日志器管理void addLogger(Logger::ptr &logger){if (hasLogger(logger->name())) return ;std::unique_lock<std::mutex> lock(_mutex);_loggers.insert(std::make_pair(logger->name(), logger));}// 2.判断是否管理了指定名称的日志器bool hasLogger(const std::string &name){std::unique_lock<std::mutex> lock(_mutex); // 判断之前加锁auto it = _loggers.find(name);             // 查找日志器if (it == _loggers.end()){// 代表没找到return false;}return true;}// 3.获取指定名称的日志器Logger::ptr getLogger(const std::string &name){std::unique_lock<std::mutex> lock(_mutex); // 判断之前加锁auto it = _loggers.find(name);             // 查找日志器if (it == _loggers.end()){// 代表没找到,返回一个空的智能指针return Logger::ptr();}return it->second;}// 4.获取默认日志器Logger::ptr rootLogger(){return _root_logger;}// 5. 获取单例句柄static LoggerManager &getInstance(){// 在c++11之后,针对静态局部变量,编译器在编译的层面上实现了线程安全// 当静态局部变量在没有构造完成之前,其他的线程进入就会阻塞static LoggerManager eton;return eton;}private:// 构造函数私有化LoggerManager(){// 构造一个日志器建造者std::unique_ptr<log::LoggerBuilder> builder(new log::LocalLoggerBuilder());builder->buildLoggerName("root");_root_logger = builder->build();// 把默认构造的日志器管理起来_loggers.insert(std::make_pair("root", _root_logger));}private:// 1.默认日志器Logger::ptr _root_logger;// 2.所管理的日志器std::unordered_map<std::string, Logger::ptr> _loggers;// 3.互斥锁std::mutex _mutex;};

设计一个全局的日志器建造者

在局部的日志器建造者上增加一个功能:将日志器添加到单例对象中;就是全局的日志器建造者

class GlobalLoggerBuilder : public LoggerBuilder{public:Logger::ptr build() override{// 必须要有日志器名称assert(_logger_name.empty() == false);// 必须要有formatter//必须要有格式化器,没有就要创建if (_formatter.get() == nullptr){_formatter = std::make_shared<Formatter>();}// 如果没有落地方式就给它添加一个标准输出的默认落地方式if (_sinks.empty()){buildSink<StdoutSink>();}//默认日志器Logger::ptr logger;// 如果类型为LOGGER_ASYNC,那么日志器为异步日志器if (_logger_type == LoggerType::LOGGER_ASYNC){// 返回异步日志器对象logger=std::make_shared<AsyncLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);}else{// 返回同步日志器的对象logger=std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks); // 日志器名字,等级,格式化,落地方式}//把日志器添加到日志器管理器中LoggerManager::getInstance().addLogger(logger);// 返回同步日志器的对象return logger;}};

测试日志器管理器的接口和全局建造者类

void test_log(){//日志器管理器log::Logger::ptr logger=log::LoggerManager::getInstance().getLogger("async_logger");//测试日志打印logger->debug(__FILE__, __LINE__, "%s", "测试日志");logger->info(__FILE__, __LINE__, "%s", "测试日志");logger->warn(__FILE__, __LINE__, "%s", "测试日志");logger->error(__FILE__, __LINE__, "%s", "测试日志");logger->fatal(__FILE__, __LINE__, "%s", "测试日志");size_t count = 0;while (count < 100000){logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);}}
int main()
{  //测试日志器管理模块//先要构造一个建造者出来//全局建造者构造日志器std::unique_ptr<log::LoggerBuilder> builder(new log::GlobalLoggerBuilder());//建造者构建零部件builder->buildLoggerName("async_logger");builder->buildLoggerLevel(log::LogLevel::value::WARN);builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");builder->buildLoggerType(log::LoggerType::LOGGER_ASYNC);builder->buildEnableUnSafeAsync();//切换模式builder->buildSink<log::StdoutSink>();                                 // 标准输出落地builder->buildSink<log::FileSink>("./logfile/async.log"); // 文件落地方式builder->build();// builder->buildSink<log::RoolBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式test_log();return 0;
}

image-20240921160519566

宏函数和全局接口设计 – 涉及代理模式

提供全局接口&宏函数, 对日志系统接口,进行使用便捷性优化

思想:

1.提供获取指定日志器的全局接口(避免用户自己操作单例对象)
2.使用宏函数对日志器的接口进行代理(代理模式)
3.提供宏函数,直接通过默认日志器进行日志的标准输出打印(不要获取日志器了)

新建一个.h,文件,文件里面放我们写的.hpp(各个模块文件)

log.h:

方便外界使用者进行调用,用户使用时直接包含该头文件就行,不需要包含各个模块的头文件了。

#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "logger.hpp"
#include <unistd.h>
#include "buffer.hpp"
#include "looper.hpp"
#include <fstream>namespace log
{//  1.提供获取指定日志器的全局接口(避免用户自己操作单例对象)Logger::ptr getLogger(const std::string &name) // 指定日志器{return log::LoggerManager::getInstance().getLogger(name);}Logger::ptr rootLogger() // 默认日志器{return log::LoggerManager::getInstance().rootLogger();}
//  2.使用宏函数对日志器的接口进行代理(代理模式)
#define debug(fmt, ...) debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define info(fmt, ...) info(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define warn(fmt, ...) warn(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define error(fmt, ...) error(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define fatal(fmt, ...) fatal(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
//  3.提供宏函数,直接通过默认日志器进行日志的标准输出打印(不要获取日志器了)
#define DEBUG(fmt, ...) log::rootLogger()->debug(fmt, ##__VA_ARGS__)
#define INFO(fmt, ...) log::rootLogger()->info(fmt, ##__VA_ARGS__)
#define WARN(fmt, ...) log::rootLogger()->warn(fmt, ##__VA_ARGS__)
#define ERROR(fmt, ...) log::rootLogger()->error(fmt, ##__VA_ARGS__)
#define FATAL(fmt, ...) log::rootLogger()->fatal(fmt, ##__VA_ARGS__)}

对宏函数与全局接口进行功能测试

测试第一组宏是否正确:

#include "log.h"void test_log(){//日志器管理器log::Logger::ptr logger=log::LoggerManager::getInstance().getLogger("async_logger");//测试日志打印logger->debug("%s", "测试日志");logger->info( "%s", "测试日志");logger->warn("%s", "测试日志");logger->error( "%s", "测试日志");logger->fatal( "%s", "测试日志");size_t count = 0;while (count < 100000){logger->fatal( "测试日志-%d", count++);}}
int main()
{  //测试日志器管理模块//先要构造一个建造者出来//全局建造者构造日志器std::unique_ptr<log::LoggerBuilder> builder(new log::GlobalLoggerBuilder());//建造者构建零部件builder->buildLoggerName("async_logger");builder->buildLoggerLevel(log::LogLevel::value::WARN);builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");builder->buildLoggerType(log::LoggerType::LOGGER_ASYNC);builder->buildEnableUnSafeAsync();//切换模式builder->buildSink<log::StdoutSink>();                                 // 标准输出落地builder->buildSink<log::FileSink>("./logfile/async.log"); // 文件落地方式builder->build();// builder->buildSink<log::RoolBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式test_log();return 0;
}

测试第二组宏:


#include "log.h"
void test_log(){// 使用默认日志器打印DEBUG("%s", "测试日志");INFO("%s", "测试日志");WARN("%s", "测试日志");ERROR("%s", "测试日志");FATAL("%s", "测试日志");size_t count = 0;while (count < 100000){FATAL("测试日志-%d", count++);}
}
int main()
{test_log();return 0;
}

image-20240921164601288

项目目录结构

example:如何使用项目的具体样例
logs:项目的各个模块,项目源码
pratice:练习代码,项目前置学习的代码
bench:用来进行性能测试

整理完成后,目录结构如下:

image-20240923103830510

功能测试

#include "../logs/log.h"//进行功能测试
void test_log(const std::string &name){INFO( "%s", "测试开始");log::Logger::ptr logger=log::LoggerManager::getInstance().getLogger(name);//测试日志打印logger->debug( "%s", "测试日志");logger->info( "%s", "测试日志");logger->warn( "%s", "测试日志");logger->error( "%s", "测试日志");logger->fatal("%s", "测试日志");INFO( "%s", "测试结束");
}
int main()
{std::unique_ptr<log::LoggerBuilder> builder(new log::GlobalLoggerBuilder());//建造者构建零部件builder->buildLoggerName("async_logger");builder->buildLoggerLevel(log::LogLevel::value::DEBUG);builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%p]%m%n");builder->buildLoggerType(log::LoggerType::LOGGER_SYNC);builder->buildSink<log::StdoutSink>();                                 // 标准输出落地builder->buildSink<log::FileSink>("./logfile/async.log"); // 文件落地方式builder->buildSink<log::RollBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式builder->build();test_log("async_logger");return 0;
}

image-20240921183817074

image-20240921182751586

性能测试

测试环境:

CPU:11th Gen Intel® Core™ i7-11800H @ 2.30GHz

RAM:16G DDR4 3200

ROM:512GB-SSD

OS:CentOS7.6(2核2G)

测试三要素:

1.测试环境
2.测试方法
3.测试结果

测试工具的编写:

1.可以控制写日志线程数量
2.可以控制写日志的总数量

分别对于同步日志器 & 异步日志器进行各自的性能测试,

​ 需要测试单写日志线程的性能

​ 需要测试多写日志线程的性能

实现:

封装一个接口,传入日志器名称,线程数量,日志数量,单条日志大小 在接口内,创建指定数量的线程,各自负责一部分日志的输出,在输出之前计时开始,在输出完毕后计时结束。
所耗时间=结束时间-起始时间
每秒输出量 =日志数量/总耗时
每秒输出大小 =日志数量*单条日志大小/总耗时
注意:异步日志输出这里,我们启动非安全模式,纯内存写入(不去考虑实际落地的时间)

项目性能测试工具实现

在对项目测试之前,需要编写测试工具,测试工具的具体实现放到bench目录下。

bench.cc:

#include "../logs/log.h"
// c++11提供的时间特性
#include <chrono>
// 设计性能测试功能
// logger_name日志器的名字,thr_count线程数的个数,msg_counr日志消息的总条数,len日志消息的长度
void bench(const std::string &logger_name, size_t thr_count, size_t msg_count, size_t msg_len)
{// 1.获取日志器log::Logger::ptr logger = log::getLogger(logger_name);// 如果没找到日志器就返回空if (logger.get() == nullptr){return;}// 2.组织指定长度的日志消息// 留一个字符,放换行符std::string msg(msg_len - 1, 'A');// 3.创建指定数量的线程// 创建一个存放线程的数组std::vector<std::thread> threads;// 存放每个线程打印日志需要消耗的时间std::vector<double> cost_arry(thr_count);// 每个线程需要打印的日志数=总日志数/线程数size_t msg_per_thr = msg_count / thr_count;// 创建指定数量的线程,push_back()构造然后拷贝,插入元素到末尾,emplace_back()构造并插入元素到末尾// 打印测试日志总条数,总大小std::cout << "\t测试日志:" << msg_count << "条,\t总大小:" << (msg_count * msg_len) / 1024 << "KB\n";for (int i = 0; i < thr_count; i++){// 插入元素时用lambad表达式threads.emplace_back([&, i](){// 4.线程函数内部开始计时,高精度获得当前的系统时间auto start=std::chrono::high_resolution_clock::now();// 5.开始循环写日志for(int i=0;i<msg_per_thr;i++){//打印日志logger->fatal("%s",msg.c_str());}// 6.线程函数内部结束计时,高精度获得当前的系统时间auto end=std::chrono::high_resolution_clock::now();//每个线程需要的时间std::chrono::duration<double> cost=end-start;cost_arry[i]=cost.count();std::cout<<"\t线程"<<i<<":"<<"\t输出数量日志:"<<msg_per_thr<<",\t耗时:"<<cost.count()<<"s"<<std::endl; });}// 要记住,创建线程那么就要等待线程退出for (int i = 0; i < thr_count; i++){threads[i].join();}// 7.计算总耗时:在多线程中,每个线程都会耗时间,但是线程是并发运行处理的,因此耗时最高的线程就是总时间。// 创建的子线程已经全部退出了double max_cost = cost_arry[0];for (int i = 0; i < thr_count; i++){max_cost = max_cost < cost_arry[i] ? cost_arry[i] : max_cost;}// 每秒输出日志数量=总日志数/总消耗时间size_t msg_per_sec = msg_count / max_cost;// 每秒输出日志大小=总日志的长度/总消耗时间size_t size_per_sec = (msg_count * msg_len) / (max_cost*1024);// 8.进行输出打印std::cout << "\t总耗时:" << max_cost << "s\n";std::cout << "\t每秒输出日志数量:" << msg_per_sec << "条\n";std::cout << "\t每秒输出日志大小:" << size_per_sec << "KB\n";
} 

测试同步日志器:

void sync_bench()
{// 创建一个同步日志器建造者std::unique_ptr<log::LoggerBuilder> builder(new log::GlobalLoggerBuilder());// 建造者构建零部件builder->buildLoggerName("sync_logger");builder->buildLoggerFormatter("%m%n");builder->buildLoggerType(log::LoggerType::LOGGER_SYNC);builder->buildSink<log::FileSink>("./logfile/sync.log"); // 文件落地方式// builder->buildSink<log::StdoutSink>();                                   // 标准输出落地// builder->buildSink<log::RoolBySizeSink>("./logfile/roll-sync-", 1024 * 1024); // 滚动文件落地方式builder->build();//  测试单线程情况bench("sync_logger", 1, 1000000, 100);// bench("sync_logger", 3, 1000000, 100); // 多线程
}

单线程:

image-20240923141643963

多线程:

image-20240923142548523

测试异步日志器:

本质:多线程和单线程都是向内存写数据要考虑 cpu和内存的性能,不去考虑磁盘落地的情况。

单线程:

void async_bench()
{
// 创建一个同步日志器建造者std::unique_ptr<log::LoggerBuilder> builder(new log::GlobalLoggerBuilder());// 建造者构建零部件builder->buildLoggerName("async_logger");builder->buildLoggerFormatter("%m%n");builder->buildLoggerType(log::LoggerType::LOGGER_ASYNC);builder->buildEnableUnSafeAsync();//开启非安全模式————主要是为了将实际落地时间排除在外builder->buildSink<log::FileSink>("./logfile/async.log"); // 文件落地方式// builder->buildSink<log::StdoutSink>();                                   // 标准输出落地// builder->buildSink<log::RoolBySizeSink>("./logfile/roll-sync-", 1024 * 1024); // 滚动文件落地方式builder->build();//  测试单线程情况bench("async_logger", 1, 1000000, 100);//bench("async_logger", 3, 1000000, 100);}

image-20240923144724814

多线程:

image-20240923145139376

cpu和内存的性能越好打印日志越快,日志的多少和线程数无关(不会因为落地而阻塞)。

同步日志器:多线程<单线程

异步日志器:多线程>单线程

// 每个线程需要打印的日志数=总日志数/线程数
size_t msg_per_thr = msg_count / thr_count;
// 创建指定数量的线程,push_back()构造然后拷贝,插入元素到末尾,emplace_back()构造并插入元素到末尾
// 打印测试日志总条数,总大小
std::cout << "\t测试日志:" << msg_count << "条,\t总大小:" << (msg_count * msg_len) / 1024 << "KB\n";
for (int i = 0; i < thr_count; i++)
{// 插入元素时用lambad表达式threads.emplace_back([&, i](){// 4.线程函数内部开始计时,高精度获得当前的系统时间auto start=std::chrono::high_resolution_clock::now();// 5.开始循环写日志for(int i=0;i<msg_per_thr;i++){//打印日志logger->fatal("%s",msg.c_str());}// 6.线程函数内部结束计时,高精度获得当前的系统时间auto end=std::chrono::high_resolution_clock::now();//每个线程需要的时间std::chrono::duration<double> cost=end-start;cost_arry[i]=cost.count();std::cout<<"\t线程"<<i<<":"<<"\t输出数量日志:"<<msg_per_thr<<",\t耗时:"<<cost.count()<<"s"<<std::endl; });
}
// 要记住,创建线程那么就要等待线程退出
for (int i = 0; i < thr_count; i++)
{threads[i].join();
}
// 7.计算总耗时:在多线程中,每个线程都会耗时间,但是线程是并发运行处理的,因此耗时最高的线程就是总时间。
// 创建的子线程已经全部退出了
double max_cost = cost_arry[0];
for (int i = 0; i < thr_count; i++)
{max_cost = max_cost < cost_arry[i] ? cost_arry[i] : max_cost;
}
// 每秒输出日志数量=总日志数/总消耗时间
size_t msg_per_sec = msg_count / max_cost;
// 每秒输出日志大小=总日志的长度/总消耗时间
size_t size_per_sec = (msg_count * msg_len) / (max_cost*1024);
// 8.进行输出打印
std::cout << "\t总耗时:" << max_cost << "s\n";
std::cout << "\t每秒输出日志数量:" << msg_per_sec << "条\n";
std::cout << "\t每秒输出日志大小:" << size_per_sec << "KB\n";

}


测试同步日志器:```cpp
void sync_bench()
{// 创建一个同步日志器建造者std::unique_ptr<log::LoggerBuilder> builder(new log::GlobalLoggerBuilder());// 建造者构建零部件builder->buildLoggerName("sync_logger");builder->buildLoggerFormatter("%m%n");builder->buildLoggerType(log::LoggerType::LOGGER_SYNC);builder->buildSink<log::FileSink>("./logfile/sync.log"); // 文件落地方式// builder->buildSink<log::StdoutSink>();                                   // 标准输出落地// builder->buildSink<log::RoolBySizeSink>("./logfile/roll-sync-", 1024 * 1024); // 滚动文件落地方式builder->build();//  测试单线程情况bench("sync_logger", 1, 1000000, 100);// bench("sync_logger", 3, 1000000, 100); // 多线程
}

单线程:

image-20240923141643963

多线程:

image-20240923142548523

测试异步日志器:

本质:多线程和单线程都是向内存写数据要考虑 cpu和内存的性能,不去考虑磁盘落地的情况。

单线程:

void async_bench()
{
// 创建一个同步日志器建造者std::unique_ptr<log::LoggerBuilder> builder(new log::GlobalLoggerBuilder());// 建造者构建零部件builder->buildLoggerName("async_logger");builder->buildLoggerFormatter("%m%n");builder->buildLoggerType(log::LoggerType::LOGGER_ASYNC);builder->buildEnableUnSafeAsync();//开启非安全模式————主要是为了将实际落地时间排除在外builder->buildSink<log::FileSink>("./logfile/async.log"); // 文件落地方式// builder->buildSink<log::StdoutSink>();                                   // 标准输出落地// builder->buildSink<log::RoolBySizeSink>("./logfile/roll-sync-", 1024 * 1024); // 滚动文件落地方式builder->build();//  测试单线程情况bench("async_logger", 1, 1000000, 100);//bench("async_logger", 3, 1000000, 100);}

image-20240923144724814

多线程:

image-20240923145139376

cpu和内存的性能越好打印日志越快,日志的多少和线程数无关(不会因为落地而阻塞)。

同步日志器:多线程<单线程

异步日志器:多线程>单线程

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

高校大数据实训管理平台怎么选择?

泰迪智能科技大数据实训管理平台分为多个方向包括&#xff1a;人工智能方向、大数据方向、商务数据分析方向&#xff0c;不同高校可以结合高校情况选择合适自己院校的相关产品平台。 高校实训管理平台是实验室模块的核心母平台&#xff0c;对实验室的所有课程及实训资源进行统…

【Linux】手把手教你制作一个简易shell——(进程创建fork进程替换wait与进程等待exec的应用)(自定义shell程序设计)

前言 大家好吖&#xff0c;欢迎来到 YY 滴Linux系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C Linux的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; YY的《C》专栏YY的《C11》专栏YY的…

HTTP协议:发展、请求响应、状态码 等

文章目录 HTTP发展历程HTTP请求URL和URIHTTP协议版本HTTP请求方法GET 和 POST 区别HTTP状态码HTTP 请求与响应报文HTTP 请求流程 HTTP 超文本传输协议&#xff08;Hypertext Transfer Protocol&#xff0c;HTTP&#xff09;是一个简单的请求-响应协议&#xff0c;它通常运行在…

快速数据检索最佳闪存驱动器恢复下载

当你意识到你的闪存盘丢失了重要文件时&#xff0c;你是否曾有过心脏停跳的时刻&#xff1f;丢失数据可能会毁掉你的一天&#xff0c;并带来很大的压力&#xff0c;无论是重要的工作文件&#xff0c;你喜欢的照片&#xff0c;还是备份你需要保持。好消息是&#xff0c;在闪存驱…

Leetcode 合并区间

我们借助一个辅助链表(元素类型是一维数组)来进行结果统计。 这个算法解决了“合并区间”的问题&#xff0c;具体要求是给定一组区间&#xff08;每个区间有开始和结束位置&#xff09;&#xff0c;如果两个区间有重叠&#xff0c;那么需要将它们合并成一个区间&#xff0c;并…

Cisco Packet Tracer超详细下载安装教程(附中文版插件)

一、安装包下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1RK8iQ9lJG__vBEGCYVYNSA 提取码&#xff1a;1lvb 压缩包解压密码&#xff1a;66668888&#xff0c;不能正常解压的&#xff0c;推荐使用360压缩解压 二、安装教程&#xff1a; 1.双击启动安装包 2.点击N…

使用功率谱密度 (PSD) 表征噪声

传递函数塑造噪声 图 1 显示了假设噪声源的频谱&#xff0c;该噪声源在所有频率下均表现出相同的平均功率&#xff0c;即 &#xff0c;其中 η 是常数。 假设噪声源的频谱。 图 1. 假设噪声源的频谱。 如果我们将此噪声应用于 LTI 系统&#xff0c;系统的传递函数将决定不同…

基于丹摩智算平台-手把手拿下经典目标检测模型 Faster-Rcnn

文章目录 1. 前言1. 1 丹摩智算平台1.2 经典目标检测模型 Faster-Rcnn 2. 前置准备2.1 WindTerm&#xff08;远程连接服务器&#xff09;2.2 项目源码 3. 服务器平台配置3.1 创建实例3.2 远程链接 4. Faster-rcnn 的环境配置4.1 上传文件&#xff0c;解压4.2 安装所需环境 5. 数…

springboot框架VUE3学院网站系统开发mysql数据库设计java编程计算机网页源码maven项目

博主介绍&#xff1a;专注于Java vue .net php phython 小程序 等诸多技术领域和毕业项目实战、企业信息化系统建设&#xff0c;从业十五余年开发设计教学工作 ☆☆☆ 精彩专栏推荐订阅☆☆☆☆☆不然下次找不到哟 我的博客空间发布了1000毕设题目 方便大家学习使用 感兴趣的…

专业软件测试服务机构介绍:软件确认测试的类型和方法

随着现代科技的迅猛发展&#xff0c;软件开发逐渐成为各类企业发展的核心。然而&#xff0c;软件的质量直接关系到企业的运营效率和用户体验。因此&#xff0c;软件确认测试作为确保软件质量的重要环节&#xff0c;正受到越来越多的关注。 软件确认测试是指在软件开发周期的最…

tensorboard展示不同运行的曲线结果

运行tensorboard曲线如下&#xff1a; tensorboard --logdir .有时候&#xff0c;曲线图会展示多条曲线&#xff0c;以至于我们想分辨哪条线来自哪次训练都做不到了。如下图是设置smoothing-0.6的结果&#xff1a; smoothing可以在页面找到设置按钮&#xff0c;呼出设置侧边…

Llama 3.1 技术研究报告-2

3.3 基础设施、扩展性和效率 我们描述了⽀持Llama 3 405B⼤规模预训练的硬件和基础设施&#xff0c;并讨论了⼏项优化措施&#xff0c;这些措施提⾼了训练效率。 3.3.1 训练基础设施 Llama 1和2模型在Meta的AI研究超级集群&#xff08;Lee和Sengupta&#xff0c;2022&#x…

直播美颜工具的开发详解:基于视频美颜SDK的解决方案

视频美颜SDK的出现&#xff0c;为开发直播美颜工具提供了一种高效的解决方案。本文将详细解析如何基于视频美颜SDK&#xff0c;开发一款性能优越、功能齐全的直播美颜工具。 1.视频美颜SDK的核心功能 视频美颜SDK是实现实时美颜的关键技术&#xff0c;其核心功能包括人脸检测、…

mysql逗号分隔的一行数据转为多行数据

原表&#xff1a; 结果&#xff1a; 方法一&#xff1a;如果每条数据的被逗号分隔的数量在637条以内&#xff0c;使用 mysql.help_topic&#xff08;mysql自带的表&#xff0c;只有637个序号&#xff09;。 select a.id,a.enclosure_ids,SUBSTRING_INDEX(SUBSTRING_INDEX(a.en…

harmonyOS 原来构建还有这么多弯弯绕绕

随着用户需求的不断增长&#xff0c;我们的 APP 已发展成功能丰富的超级APP&#xff0c;这也导致打包构建变得非常耗时&#xff0c;可能需要数小时&#xff0c;严重影响开发效率和产品迭代。通过采用模块化设计、增量构建、并行处理、缓存机制、优化依赖管理&#xff0c;以及云…

使用 Docker 部署 RStudio 的终极教程

一.介绍 在现代数据科学和统计分析领域&#xff0c;RStudio 是一个广受欢迎的集成开发环境&#xff08;IDE&#xff09;&#xff0c;为用户提供了强大的工具来编写、调试和可视化 R 代码。然而&#xff0c;传统的 RStudio 安装可能面临环境配置复杂、版本兼容性等问题。Docker…

2.4K star的GOT-OCR2.0:端到端OCR 模型

GOT-OCR2.0是一款新一代的光学字符识别&#xff08;OCR&#xff09;技术&#xff0c;标志着人工智能在文本识别领域的重大进步。作为一款开源模型&#xff0c;GOT-OCR2.0不仅支持传统的文本和文档识别&#xff0c;还能够处理乐谱、图表以及复杂的数学公式&#xff0c;为用户提供…

报错解决方案

大模型-报错解决方案 百度千帆大模型 仅个人笔记使用&#xff0c;感谢点赞关注 百度千帆大模型 未开通付费模型 qianfan.errors.APIError: api return error, req_id: code: 17, msg: Open api daily request limit reached 可能的原因: 未开通所调用服务的付费权限&#xff0…

代码随想录算法day38 | 动态规划算法part11 | 1143.最长公共子序列,1035.不相交的线,53. 最大子序和,392.判断子序列

1143.最长公共子序列 体会一下本题和 718. 最长重复子数组 的区别 力扣题目链接(opens new window) 给定两个字符串 text1 和 text2&#xff0c;返回这两个字符串的最长公共子序列的长度。 一个字符串的 子序列 是指这样一个新的字符串&#xff1a;它是由原字符串在不改变字符的…

掌握Python自动化办公的3个核心技能,全是干货建议收藏

随着Python在办公自动化领域的广泛应用&#xff0c;掌握Python的相关技能变得越来越重要。本文将详细介绍Python在文件操作、数据处理以及Excel操作方面的核心技能&#xff0c;帮助读者提升工作效率。 掌握Python自动化办公的核心技能&#xff0c;主要包括以下几个方面&#x…