文章目录

专栏导读

C++项目实战——基于多设计模式下的同步&异步日志系统(总集篇)-LMLPHP

项目介绍

  • 本项目为实现一个日志系统,支持以下功能:
    • 支持多级别日志消息
    • 支持同步日志异步日志
    • 支持可靠写入日志到标准输出指定文件以及滚动文件中;
    • 支持多线程并发写日志;
    • 支持拓展不同的日志落地方向

开发环境

  • CentOS 7;
  • vscode/vim;
  • g++;
  • Makefile;

核心技术

  • 继承和多态;
  • C++11(多线程、auto、智能指针、右值引用、包装器、lambda等);
  • 双缓冲区;
  • 生产消费模型;
  • 多线程;
  • 设计模式(单例、工厂、代理、建造者);

环境搭建

本项目不需要依赖任何第三方库。

日志系统介绍

1.为什么需要日志系统

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

2.日志系统技术实现

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

  • 利用printfstd::cout 等输出函数将日志信息打印到控制台
  • 对于大型商业化项目,为了方便排查问题,我们一般会将日志输出到文件或者是数据库系统方便查询和分析日志,主要分为同步日志异步日志方式;
    • 同步写日志;
    • 异步写日志;
2.1同步写日志
  • 同步日志是指当输出日志时,必须等待日志输出语句执行完毕后,才能执行后面的业务逻辑与语句,日志输出语句将在同一个线程运行。每次调用一次打印日志API就对应一次系统调用write写日志文件。

C++项目实战——基于多设计模式下的同步&异步日志系统(总集篇)-LMLPHP

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

  • 一方面,大量的日志打印陷入等量的write系统调用,有一定的系统开销
  • 另一方面,使得打印日志的进程附带了大量同步的磁盘IO,影响程序性能
2.2异步写日志

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

C++项目实战——基于多设计模式下的同步&异步日志系统(总集篇)-LMLPHP

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

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

前置知识补充

不定参函数

C语言中的不定参数函数是一种特殊类型的函数,它允许你定义一个函数,可以接受不定数量的参数。这些函数通常用于处理不确定数量的输入,例如printfscanf等标准库函数,它们可以接受不同数量和类型的参数。

不定参函数的声明格式如下:

	return_type func_name (format_string, ...);
  • return_type为函数返回值类型;
  • func_name为函数名;
  • format_string是一个格式化字符串,用于指定参数的数量和类型;
  • ... 表示不定数量的参数;

在函数内部我们可以用一些宏或函数来对不定参数进行分解,常见的宏有va_startva_argva_end

    void va_start(va_list ap, last);
    type va_arg(va_list ap, type);
    void va_end(va_list ap);
  • va_list:va_list是一个类型,用于声明一个参数列表的对象,它的本质其实是void*;
  • va_start:用于初始化va_list对象,使其指向不定参数列表的第一个参数;
  • va_arg:用于获取不定参数列表中的参数;
  • va_end:用于清空可变参数列表;

示例1

#include <stdio.h>
#include <stdarg.h>

void printNum(int count, ...)
{
    va_list ap;
    va_start(ap, count); // 初始化ap指针,使其指向不定参数列表的第一个参数
    for(int i = 0; i < count; i++)
    {
        int num = va_arg(ap, int); // 从不定参数列表中抽取int类型的参数
        printf("%d ", num);
    }
    printf("\n");
    va_end(ap); // 将ap置空
}

int main()
{
    printNum(5, 1, 2, 3, 4, 5);
    return 0;
}

示例2

#define _GNU_SOURCE

#include <stdio.h>
#include <stdarg.h>

void myprintf(const char* format, ...)
{
    va_list ap;
    va_start(ap, format);
    char* res;
    int ret = vasprintf(&res, format, ap);
    if(ret != -1)
    {
        printf(res);
        free(res); // res指向动态开辟的空间,需要手动释放
    }
    va_end(ap);
}
int main()
{
    myprintf("%s-%d\n", "huaxiangyun", 100);
    return 0;
}

注意
示例中vasprintf 是一个 C 库函数,它可以通过可变参数创建一个格式化的字符串,并将其存储在动态分配的内存中。它的使用方法与 printf类似,但它不会将结果打印到标准输出流中,而是将其存储在一个指向字符数组的指针中

函数原型如下:

#include <stdarg.h>
#include <stdio.h>

int vasprintf(char **str, const char *format, va_list ap);

C++风格不定参函数

在C++中,有两种主要方式来创建不定参数函数:

  • C风格的不定参数函数:这种方式与C语言中的不定参数函数类似,使用了C标准库中的va_list、va_start、va_arg和va_end宏。这种方式在C++中仍然有效,但不够类型安全,不太推荐在现代C++代码中使用。

  • C++11引入的可变参数模板:这是更现代和类型安全的方式,使用了C++的模板和新的语法特性。可变参数模板允许你定义一个接受不定数量参数的函数,并且能够在编译时进行类型检查。这种方式更灵活,并且是C++推荐的方式。

示例3

#include <iostream>

// 无参特化
void xprintf()
{
    std::cout << std::endl;
}

template <typename T, typename ...Args>
void xprintf(const T &v, Args &&...args)
{
    std::cout << v;
    if((sizeof ...(args)) > 0)
    {
        xprintf(std::forward<Args>(args)...); // 递归分解参数包
    }
    else
    {
        xprintf();
    }
}

int main()
{
    xprintf("huaxinagyun");
    xprintf("aaaa", 1000);
    return 0;
}

不定参宏函数

示例4

#include <stdio.h>

#define LOG(format, ...) /
	printf("[%s : %d]\n", format, __FILE__, __LINE__, ##__VA_ARGS__)

int main()
{
    LOG("日志消息");
    return 0;
}

设计模式

设计模式是一种在软件设计中常见的可重用解决方案的方法。它们是经过反复验证和证明的最佳实践,可以帮助软件开发人员解决特定问题或应对常见的设计挑战。设计模式提供了一种通用的框架,有助于构建高质量、易于维护和可扩展的软件系统

六大原则

  • 单一职责原则

    • 类的职责应该单一,一个方法只做一件事。职责划分清晰明了,每次改动到最小的方法或类;
    • 使用建议:两个完全不一样的功能不应该放在一个类中,一个类应该是一组相关性很高的函数、数据的封装;
    • 用例:网络聊天:网络通信&聊天,应该分割为网络通信类或聊天类;
  • 开闭原则

    • 对扩展开放,对修改封闭
    • 使用建议:对软件实体的改动,最好使用拓展而非修改的方式;
    • 用例:超市卖货:商品价格时长需要改动,但不是修改原来商品的价格而是新增促销价格;
  • 里氏替换原则

    • 通俗点讲,就是只要父类能出现的地方,子类就可以出现,而且替换为子类也不会出现任何错误或异常
    • 在继承类时,务必重写父类中所有的方法,尤其需要注意父类的protected方法,子类尽量不要暴露自己的public方法供外界调用;
    • 使用建议:子类必须完全实现父类的方法,孩子可以有自己的个性。覆盖或实现父类的方法时,输入参数可以被放大,输出可以缩小;
    • 用例:跑步运动员类会跑步,子类长跑运动员会跑步且擅长长跑,子类短跑运动员会跑步且擅长短跑;
  • 依赖倒置原则

    • 高层模块不应该依赖低层模块,两者都应该依赖其抽象。不可分割的原子逻辑就是低层模式,原子逻辑组装成的就是高层模块;
    • 模块间依赖通过抽象(接口)发生,具体类之间不之间依赖
    • 使用建议:每个类都尽量有抽象类,任何类都不应该从具体类派生。尽量不要重写基类的方法。结合里氏替换原则使用。
    • 用例:奔驰车司机类只能开奔驰,司机类给什么车就开什么车,开车的人:司机——依赖于抽象;
  • 迪米特法则

    • 尽量减少对象之间的交互,从而减少类之间的耦合。一个对象应该对其他对象有最少的了解。对类的低耦合提出了明确的要求:
      • 只和直接的朋友交流,朋友之间也是有距离的。自己的就是自己的(如果一个方法放在本类中,既不增加类间关系,也不产生负面影响,那就放置在本类中;
    • 用例:老师让班长点名,老师给班长一个名单,班长点完名勾选,返回结果,而不是班长点名,老师勾选;
  • 接口隔离原则

    • 客户端不应该依赖他不需要的接口,类间的依赖关系应该建立在最小的接口上
    • 使用建议:接口设计尽量精简单一,但是不要对外暴露没有实际意义的接口;
    • 用例:修改密码:不应该提供修改用户信息接口,而就是单一的最小修改密码的接口;

从整体上来了解六大原则,可以简要的概括为一句话:用抽象构建框架,用实现拓展细节。具体到每一条设计原则,则对应一条注意事项:

  • 单一职责原则告诉我们实现类要职责单一
  • 里氏替换原则告诉我们不要破坏继承体系
  • 依赖倒置原则告诉我们要面向接口编程
  • 接口隔离原则告诉我们在设计接口时要降低耦合
  • 开闭原则是总纲,告诉我们要对拓展开放,对修改关闭

单例模式

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

单例模式有两种实现模式:饿汉模式和懒汉模式。

饿汉模式
  • 程序启动时就会创建一个唯一的实例对象。因为单例对象已经确定,所以比较适用于多线程环境中,多线程获取单例对象不需要加锁可以有效的避免资源竞争,提高性能
#include <iostream>
#include <string>

// 饿汉模式
class Singleton
{
public:
    Singleton(const Singleton &) = delete;
    Singleton& operator=(const Singleton&) = delete;
    static Singleton& getInstance()
    {
        return _eton;
    }
    std::string &getData()
    {
        return _data;
    }
private:
    Singleton() :_data("Singleton") 
    {
        std::cout << "获取单例对象" <<std::endl;
    }
    ~Singleton() {}
private:
    static Singleton _eton;
    std::string _data;
};

Singleton Singleton::_eton;

int main()
{
    std::cout << Singleton::getInstance().getData() << std::endl;
    return 0;
}
懒汉模式
  • 第一次要使用单例对象的时候创建实例对象。如果单例对象构造特别耗时或者耗费资源(加载插件、加载网络资源等),可以选择懒汉模式,在第一次使用的时候才创建对象。
    • C++11后,静态变量能够满足在线程安全的前提下唯一的被构造和析构
// 懒汉模式
class Singleton
{
public:
    Singleton(const Singleton &) = delete;
    Singleton &operator=(const Singleton &) = delete;
    static Singleton &getInstance()
    {
        static Singleton _eton; 
        return _eton;
    }
    std::string &getData()
    {
        return _data;
    }
private:
    Singleton() : _data("Singleton")
    {
        std::cout << "单例对象构造" << std::endl;
    }
    ~Singleton() {}
private:
    static Singleton _eton;
    std::string _data;
};

工厂模式

  • 工厂模式是一种创建型设计模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们创建对象时不会对上层暴露创建逻辑,而是通过使用一个共同结构来指向新创建的对象,以此实现创建与使用的分离

工厂模式可以分为:

  • 简单工厂模式;
  • 工厂方法模式;
  • 抽象工厂模式;
简单工厂模式
  • 简单工厂模式实现由一个工厂对象通过类型来决定创建出来指定产品类的实例
  • 假设有个工厂能生产出水果,当客户生产水果的时候明确告诉工厂生产哪类水果,工厂需要接收用户提供的类别信息,当新增产品的时候,工厂内部去添加新产品的生产方式。
/*
    简单工厂模式:通过参数控制可以生产任何商品
        优点:简单粗暴、直观易懂。使用一个工厂生产同一等级结构下的任意商品
        缺点:
            1.所有东西生产在一起,产品太多会导致代码量庞大;
            2.开闭原则遵守不是太好,要新增产品就必须修改工厂方法。
*/
#include <iostream>

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 FruitFactory
{
public:
    static std::shared_ptr<Fruit> create(const std::string &name)
    {
        if (name == "苹果")
            return std::make_shared<Apple>();
        else if (name == "香蕉")
            return std::make_shared<Banana>();
        return std::shared_ptr<Fruit>();
    }
};

int main()
{
    std::shared_ptr<Fruit> ff = FruitFactory::create("苹果");
    ff->name();

    ff = FruitFactory::create("香蕉");
    ff->name();
    return 0;
}

工厂方法模式
  • 在简单工厂模式下新增多个工厂,多个产品,每个产品对应一个工厂
  • 假设现在有A、B两种产品,则开两个工厂,工厂A负责生产产品A,工厂B负责生产产品B,用户只知道产品的工厂名,而不知道具体的产品信息,工厂不需要再接收客户的产品类别,而只负责生产产品。
/* 
    工厂方法模式:定义一个创建对象的接口,但是由子类来决定创建哪种对象,使用多个工厂分别生产指定的固定产品
        优点:
            1.减轻了工厂类的负担,将某产品的生产交给指定的工厂来进行;
            2.开闭原则遵循较好,添加新产品只需要新增产品的工厂即可,不需要修改原先的工厂类。
        缺点:
            对于某种可以形成一组产品族的情况处理较为复杂,需要创建大量的工厂类。
    
*/
#include <iostream>
#include <string>
#include <memory>

class Fruit
{
public:
    Fruit() {}
    virtual void name() = 0;
};

class Apple : public Fruit
{
public:
    Apple() {}
    virtual void name()
    {
        std::cout << "苹果" << std::endl;
    }
private:
    std::string _color;
};

class Banana : public Fruit
{
public:
    Banana() {}
    virtual void name()
    {
        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()
    {
        return std::make_shared<Apple>();
    }
};

class BananaFactory : public FruitFactory
{
public:
    virtual std::shared_ptr<Fruit> create()
    {
        return std::make_shared<Banana>();
    }
};

int main()
{
    std::shared_ptr<FruitFactory> factory(new AppleFactory());
    std::shared_ptr<Fruit> fruit = factory->create();
    fruit->name();

    factory.reset(new BananaFactory());
    fruit = factory->create();
    fruit->name();

    return 0;
}
抽象工厂模式
  • 工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时,我们可以考虑将一些相关的产品组成一个产品族(位于不同产品等级结构中功能相关联的产品组成的家族),由同一个工厂来统一生产,这就是抽象工厂模式的基本思想。
/*
    抽象工厂:围绕一个超级工厂去创建其他工厂。每个生成的工厂按照工厂模式提供对象。
        思想:将工厂抽象成两层,抽象工厂 & 具体工厂子类,在工厂子类中生产不同类型的子产品
*/
#include <iostream>
#include <string>
#include <memory>

class Fruit
{
public:
    Fruit() {}
    virtual void name() = 0;
};

class Apple : public Fruit
{
public:
    Apple() {}
    void name() override
    {
        std::cout << "苹果" << std::endl;
    }

private:
    std::string _color;
};

class Banana : public Fruit
{
public:
    Banana() {}
    void name() override
    {
        std::cout << "香蕉" << std::endl;
    }
};

class Animal
{
public:
    virtual void voice() = 0;
};

class Cat : public Animal
{
public:
    void voice() override
    {
        std::cout << "喵喵喵" << std::endl;
    }
};

class Dog : public Animal
{
public:
    void voice() override
    {
        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:
    std::shared_ptr<Animal> getAnimal(const std::string &name) override
    {
        return std::shared_ptr<Animal>();
    }

    std::shared_ptr<Fruit> getFruit(const std::string &name) override
    {
        if (name == "苹果")
            return std::make_shared<Apple>();
        else if (name == "香蕉")
            return std::make_shared<Banana>();
        return std::shared_ptr<Fruit>();
    }
};

class AnimalFactory : public Factory
{
public:
    std::shared_ptr<Fruit> getFruit(const std::string &name) override
    {
        return std::shared_ptr<Fruit>();
    }

    std::shared_ptr<Animal> getAnimal(const std::string &name) override
    {
        if (name == "小猫")
            return std::make_shared<Cat>();
        else if (name == "小狗")
            return std::make_shared<Dog>();
        return std::shared_ptr<Animal>();
    }
};

class FactoryProducer
{
public:
    static std::shared_ptr<Factory> getFactory(const std::string &name)
    {
        if (name == "水果")
            return std::make_shared<FruitFactory>();
        else if(name == "动物")
            return std::make_shared<AnimalFactory>();
    }
};

int main()
{
    std::shared_ptr<Factory> fruit_factory = FactoryProducer::getFactory("水果");
    std::shared_ptr<Fruit> fruit = fruit_factory->getFruit("苹果");
    fruit->name();
    fruit = fruit_factory->getFruit("香蕉");
    fruit->name();

    std::shared_ptr<Factory> animal_factory = FactoryProducer::getFactory("动物");
    std::shared_ptr<Animal> animal = animal_factory->getAnimal("小猫");
    animal->voice();
    animal = animal_factory->getAnimal("小狗");
    animal->voice();
    
    return 0;
}

建造者模式

  • 建造者模式是一种创建型设计模式使用多个简单的对象一步一步构建出一个复杂的对象,能够将一个复杂的对象的构建与它的表示分离,提供一种创建对象的最佳方式。主要用于解决对象的构建过于复杂的问题。

建造者模式主要基于4个核心类实现:

  • 抽象产品类;
  • 具体产品类:一个具体的产品对象类;
  • 抽象pubic类:创建一个产品对象所需的各个部件的抽象接口;
  • 具体产品的Builder类:实现抽象接口,构建各个部件;
  • 指挥者Director类:统一组建过程,提供给调用者使用,通过指挥者来构建产品;
#include <iostream>
#include <memory>

/* 抽象电脑类 */
#include <iostream>
#include <memory>

class Computer
{
public:
    using ptr = std::shared_ptr<Computer>;

    Computer() {};

    void setBoard(const std::string &board) { _board = board ;}
    void setDisplay(const std::string &display) {_display = display;}
    virtual void setOs() = 0;
    std::string show()
    {
        std::string computer = "Computer:[\n";
        computer += "\tboard: " + _board + "\n";
        computer += "\tdisplay: " + _display + "\n";
        computer += "\tos: " + _os + "\n";
        computer += "]\n";
        return computer;
    }
protected:
    std::string _board;
    std::string _display;
    std::string _os;
};

/* 具体产品类 */
class MacBook : public Computer
{
public:
    using ptr = std::shared_ptr<MacBook>;
    MacBook() {}
    virtual void setOs() 
    {
        _os = "Mac Os X12";
    }
};

/* 抽象建造者类:包含创建一个产品对象各个部件的抽象接口 */
class Builder
{
public:
    using ptr = std::shared_ptr<Builder>;
    virtual void buildBoard(const std::string &board) = 0;
    virtual void buildDisplay(const std::string &display) = 0;
    virtual void buildOS() = 0;
    virtual Computer::ptr build() = 0;
};

/* 具体产品的具体建造者类:实现抽象接口,构建和组装各个部件 */
class MacBookBuilder : public Builder
{
public:
    using ptr = std::shared_ptr<MacBookBuilder>;
    MacBookBuilder() : _computer(new MacBook()) {}
    virtual void buildBoard(const std::string &board) 
    {
        _computer->setBoard(board);
    }

    virtual void buildDisplay(const std::string &display)
    {
        _computer->setDisplay(display);
    }

    virtual void buildOS()
    {
        _computer->setOs();
    }

    virtual Computer::ptr build()
    {
        return _computer;
    }
private:
    Computer::ptr _computer;
};

/* 指挥者类:提供给调用者使用,通过指挥者来构造复杂的产品 */
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:
    Builder::ptr _builder;
};

int main()
{
    Builder* builder = new MacBookBuilder();
    std::unique_ptr<Director> pd(new Director(builder));
    pd->construct("华为主板", "VOC显示器");
    Computer::ptr computer = builder->build();
    std::cout << computer->show() ;

    return 0;
}

代理模式

  • 代理模式指代理控制对其他对象的访问,也就是代理对象控制对原对象的引用。在某些情况下,一个对象不能或不适合直接被引用访问,而代理对象可以在客户端与目标对象之间起到中介作用

  • 代理模式的结构包括一个真正的你要访问的对象(目标类)、一个是代理对象。目标对象与代理对象实现同一个接口,先访问代理类,再通过代理类来访问目标对象。代理模式分为静态代理、动态代理:

    • 静态代理指的是:在编译时就已经确定好了代理类和被代理类的关系。也就是说,在编译时就已经确定了代理类要代理的是哪一个类。

    • 动态代理指的是:在运行时才动态生产代理类,并将其与被代理类绑定。这意味着在运行时才能确定代理类要代理的是哪一个类。

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

#include <iostream>
#include <string>
/*房东要把⼀个房⼦通过中介租出去理解代理模式*/

/* 租房类 */
class RentHouse
{
public:
    virtual void rentHouse() = 0;
};

/* 房东类 */
class Landlord : public RentHouse
{
    public:
        void rentHouse() 
        {
            std::cout << "将房子租出去\n" << std::endl;
        }
};

/* 中介类 */
class Intermedirary : public RentHouse
{
    public:
        void rentHouse()
        {
            std::cout << "发布告示" << std::endl;
            std::cout << "带人看房" << std::endl;
            _landlord.rentHouse();
            std::cout << "负责租后维修" << std::endl;

        }
    private:
        Landlord _landlord;
};

int main()
{
    Intermedirary intermedirary;
    intermedirary.rentHouse();

    return 0;
}

框架设计

本项目实现的是一个多日志器日志系统,主要实现的功能是让程序员能够轻松的将程序运行日志信息落地到指定位置,且支持同步与异步两种方式的日志落地方式。

项目的框架设计将项目分为以下几个模块来实现。

(具体模块在代码实现中详细讲解)

模块划分

日志等级模块

  • 对输出日志的等级进行划分,以便于控制日志的输出,并提供等级枚举转字符串的功能

日志消息分为以下等级:

  • OFF:关闭;
  • DEBUG:调式,调试时的关键信息输出;
  • INFO:提示,普通的提示性日志信息;
  • WARN:警告,不影响运行,但是需要注意的日志;
  • ERROR:错误,程序运行时出现错误的日志;
  • FATAL:致命,一般是代码导致程序无法正常运行的日志。

日志消息模块

  • 中间存储日志输出所需的各项要素信息
    • 时间:描述本条日志输出的时间;
    • 线程ID:描述本条日志是哪一个线程输出的;
    • 日志等级:描述本条日志的等级;
    • 日志文件名:描述本条日志在哪一个源码文件中输出的;
    • 日志行号:描述本条日志在源码文件的哪一行输出的;
    • 日志数据:本条日志的有效数据载荷。

日志消息格式化模块

  • 设置日志输出格式,并提供对日志消息格式化的功能。

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

    • %d 表示日期,包含子格式 {%H:%M:%S}
    • %t 表示线程ID;
    • %c 表示日志器名称;
    • %f 表示源码文件名;
    • %l 表示源码行号;
    • %p 表示日志级别;
    • %m 表示主体消息;
    • %n 表示换行;
  • 设计思想:设计不同的子类,不同的子类从日志消息中取出不同的数据进行处理。

日志消息落地模块

  • 决定了日志消息的落地方向(即输出位置),可以是标准输出。也可以是日志文件,也可以是滚动文件进行输出。
    • 标准输出:表示将日志信息进行标准输出打印;
    • 日志文件输出:表示将日志写入指定文件末尾;
    • 滚动文件输出:当前以文件大小进行控制,当一个日志文件大小达到指定大小,则切换下一个文件进行输出;
  • 后期,也可以拓展远程日志输出,创建客户端将日志消息发送到远程的的日志分析服务器;
  • 设计思想:设计不同的子类,不同的子类控制不同的日志落地方向。

日志器模块

  • 此模块是对以上几个模块的整合,用户通过日志器进行日志的输出,有效降低用户的使用难度。
  • 此模块包含有:日志消息落地模块对象,日志消息格式化模块对象,日志输出等级。

日志器管理模块

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

异步线程模块

  • 实现对日志的异步输出功能,用户只需要将输出的日志任务放入任务池,异步线程负责日志的落地输出功能,以此提供更加高效的非阻塞日志输出。

模块关系图

C++项目实战——基于多设计模式下的同步&amp;异步日志系统(总集篇)-LMLPHP

实用工具类设计

在项目中,我们时常会需要用到一些与业务无关的功能,如:获取系统时间、创建目录、获取路径等。我们将这些零碎的功能接口提前完成,以便于项目中会用到。

实用工具类主要包含以下功能:

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

获取系统时间

我们将获取系统时间的接口单独封装在一个Date类中。获取系统时间,我们可以使用库函数time来实现。

time介绍

C语言中的time函数是一个用于获取当前系统时间的标准库函数,它定义在<time.h>头文件中。time函数通常返回自1970年1月1日以来经过的秒数,这被称为Unix时间戳(或Epoch时间)。它的函数原型如下:

time_t time(time_t *tloc);
  • time_t是一种数据类型,通常是一个整数类型(如long),用来存储时间值;
  • tloc是一个指向time_t类型的指针,用于存储获取的时间值。你可以将它设置为NULL,如果你不需要获取时间值的副本;

time函数返回一个表示当前时间的时间戳,单位是。如果传递了非空的tloc指针,它还会将时间戳的副本存储在tloc指向的地址中,以便你可以稍后使用。

getTime函数设计

#include <iostream>
#include <ctime>

class Date
{
public:
    static size_t getTime()
    {
        return (size_t)time(nullptr);
    }
};

判断文件是否存在

判断文件是否存在、获取文件所在路径、创建目录这三个功能都与文件相关,因此我们将三者在一个File类中实现。

实现思路:

  • 函数参数为一个路径字符串,表示所要判断的文件的路径;
  • 函数返回值为bool类型,若该文件存在则返回true
  • 通过系统调用stat来实现;

stat介绍

认识stat函数之前我们首先认识一下struct stat类型。

在C语言中,struct stat是一个用于表示文件或文件系统对象属性的结构体类型。这个结构体通常用于与文件和目录相关的操作,例如获取文件的大小、访问权限、最后修改时间等信息。struct stat类型的定义通常由操作系统提供,因此其具体字段可能会因操作系统而异。

以下是一个典型的struct stat结构体的字段,尽管具体字段可能会因操作系统而异:

struct stat {
    dev_t     st_dev;         // 文件所在设备的ID
    ino_t     st_ino;         // 文件的inode号
    mode_t    st_mode;        // 文件的访问权限和类型
    nlink_t   st_nlink;       // 文件的硬链接数量
    uid_t     st_uid;         // 文件的所有者的用户ID
    gid_t     st_gid;         // 文件的所有者的组ID
    off_t     st_size;        // 文件的大小(以字节为单位)
    time_t    st_atime;       // 文件的最后访问时间
    time_t    st_mtime;       // 文件的最后修改时间
    time_t    st_ctime;       // 文件的最后状态改变时间
    blksize_t st_blksize;     // 文件系统I/O操作的最佳块大小
    blkcnt_t  st_blocks;      // 文件占用的块数
};

struct stat结构体中的这些字段提供了关于文件或目录的各种信息。不同的操作系统可能会提供额外的字段,或者字段的意义可能会有所不同。

stat函数用于获取与指定路径名相关联的文件或目录的属性,并将这些属性填充到一个struct stat结构体中。以下是stat函数的函数原型:

int stat(const char *pathname, struct stat *statbuf);
  • pathname是要获取属性的文件或目录的路径名;
  • statbuf是一个指向struct stat结构体的指针,用于存储获取到的属性信息;
  • stat函数返回一个整数值,如果操作成功,返回0;如果出现错误,返回-1,并设置errno全局变量以指示错误的类型。

注意需要包含头文件<sys/stat.h><unistd.h>来使用stat函数。

exists函数设计

static bool exists(const std::string &pathname)
{
    struct stat st;
    if (stat(pathname.c_str(), &st) < 0) // 打开文件失败则代表文件不存在
    {
        return false;
    }
    return true;
}

获取文件所在路径

假设存在文件“user/aaa/bbb/ccc/test.cc”,我们需要获取文件test.cc所在的路径即"user/aaa/bbb/ccc"

实现这个功能我们将会用到库函数find_last_of

find_last_of介绍

C++标准库中的find_last_of函数是用于在字符串中查找指定字符集中最后一个出现的字符,并返回其位置或索引。这个函数通常用于字符串操作,允许你在字符串中查找某些字符集的最后一个匹配字符。

函数格式如下:

size_t find_last_of(const string& str, size_t pos = string::npos) const;
size_t find_last_of(const char* s, size_t pos = string::npos) const;
size_t find_last_of(const char* s, size_t pos, size_t n) const;

其中,

  • str是要搜索的字符串;
  • s是要查找的字符集;
  • pos是可选的参数,用于指定搜索的起始位置,默认为string::npos,表示从字符串的末尾开始向前搜索。

这个函数返回匹配字符集中任何字符的最后一个位置的索引,如果未找到匹配字符,则返回string::npos。需要注意的是,返回的索引是从0开始的。

path函数设计

就以文件“user/aaa/bbb/ccc/test.cc”为例,要想获取文件路径,我们只需要找到最后一个‘/ 或者 \\所在位置,并将在这之前的内容全部返回即可。若不存在路径分隔符/或者 \\,则证明该文件在当前目录,返回. 即可。

static std::string path(const std::string &pathname)
{
    size_t pos = pathname.find_last_of("/\\");
    if (pos == std::string::npos)
        return ".";
    return pathname.substr(0, pos + 1);
}

创建文件所在目录

以文件路径'' user/aaa/bbb/ccc/test.cc '',给函数传递该路径字符串,函数的任务是依次创建目录useraaabbbccc

需要注意的小细节是,每次要创建一个目录时,都要判断该目录是否存在,使用之前实现的exists函数即可。

我们依靠系统调用mkdir来完成目录的创建,首先来认识一下mkdir

mkdir介绍

mkdir 函数是一个系统调用,用于在文件系统中创建新的目录(文件夹)。它通常用于在文件系统中创建一个新的目录,以便存储文件或其他目录。

函数原型如下:

#include <sys/stat.h>
#include <sys/types.h>

int mkdir(const char *pathname, mode_t mode);
  • pathname 是一个字符串,表示要创建的目录的路径。这个路径可以是相对路径绝对路径

  • mode 是一个权限掩码,用于指定新目录的权限。这个权限掩码通常是八进制数;

mkdir 函数的功能是创建一个新的目录,并根据指定的权限设置来设置目录的权限。如果成功创建目录,函数将返回0,否则返回-1,并设置 errno 变量以指示错误的原因。

find_first_of介绍

在 C++ 中,find_first_of 是字符串(std::string)和其他序列容器中的成员函数,用于在目标字符串中查找第一个匹配源字符串中任何字符的位置。它的功能是找到目标字符串中的任何一个字符在源字符串中第一次出现的位置。

函数类型如下:

size_t find_first_of(const std::basic_string& str, size_t pos = 0) const;
size_t find_first_of(const CharT* s, size_t pos = 0) const;
size_t find_first_of(const CharT* s, size_t pos, size_t n) const;
size_t find_first_of(CharT ch, size_t pos = 0) const;
  • str:一个字符串,表示源字符串,函数将在目标字符串中查找源字符串中的任何字符;
  • s:一个字符数组或 C 字符串,表示源字符序列,函数将在目标字符串中查找数组中的任何字符;
  • ch:一个字符,表示要查找的字符;
  • pos:可选参数,表示开始查找的位置。默认为0,即从字符串的开头开始查找;
  • n:可选参数,与 s 一起使用,表示要查找的字符数量;

find_first_of 函数返回目标字符串中第一个匹配源字符序列中任何字符的位置(索引),如果没有找到匹配的字符,则返回 std::string::npos

函数createDirectory设计

static void createDirectory(const std::string pathname)
{
    size_t pos = 0, idx = 0;
    while(idx < pathname.size())
    {
        pos = pathname.find_first_of("/\\", idx);
        if(pos == std::string::npos)
        {
            mkdir(pathname.c_str(), 0777);
        }
        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;
    }
}

实用工具类整理

在项目实现中,我们最好使用自己的命名空间。我们将各个类整体放入LOG(名称自行决定)命名空间中的util命名空间中。

#ifndef __M_UTIL_H__
#define __M_UTIL_H__
/* 
    使用工具类实现:
    1. 获取系统时间
    2. 判断文件是否存在
    3. 获取文件所在路径
    4. 创建文件所在目录
*/
#include <iostream>
#include <ctime>
#include <sys/stat.h>
namespace LOG
{
    namespace util
    {
        class Date
        {
        public:
            static size_t getTime()
            {
                return (size_t)time(nullptr);
            }
        };
       
        class File
        {
        public:
            static bool exists(const std::string &pathname)
            {
                struct stat st;
                if (stat(pathname.c_str(), &st) < 0) // 打开文件失败则代表文件不存在
                {
                    return false;
                }
                return true;
            }

            static std::string path(const std::string &pathname)
            {
                size_t pos = pathname.find_last_of("/\\");
                if (pos == std::string::npos)
                    return ".";
                return pathname.substr(0, pos + 1);
            }

            static void createDirectory(const std::string pathname)
            {
                size_t pos = 0, idx = 0;
                while(idx < pathname.size())
                {
                    pos = pathname.find_first_of("/\\", idx);
                    if(pos == std::string::npos)
                    {
                        mkdir(pathname.c_str(), 0777);
                    }
                    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;
                }
            }
        };
    }
}
#endif

日志等级类设计

对于日志等级类,我们主要完成两个功能:

  • 定义枚举类
  • 实现转换接口

日志等级类中使用枚举来设置日志等级,并提供一个将日志等级转化为对应字符串的功能。

日志等级划分

日志等级分为以下7个等级:

  • OFF:关闭;
  • DEBUG:调式,调试时的关键信息输出;
  • INFO:提示,普通的提示性日志信息;
  • WARN:警告,不影响运行,但是需要注意的日志;
  • ERROR:错误,程序运行时出现错误的日志;
  • FATAL:致命,一般是代码导致程序无法正常运行的日志;
  • UNKOW:未知,表示未知错误。
enum class value
{
    UNKNOW = 0,
    DEBUG,
    INFO,
    WARN,
    ERROR,
    FATAL,
    OFF
};

to_string函数设计

函数设计思路较为简单,函数参数为value类型,表示日志等级。使用switch语句进行选择输出对应字符串。

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"; 
    default: return "UNKNOW";
    }
}

日志等级类整理

/*
    日志等级类实现:
    1.定义枚举类
    2.转换接口
*/

#ifndef __M_LEVEL_H__
#define __M_LEVEL_H__

namespace 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"; 
                default: return "UNKNOW";
                }
            }
    };
}
#endif

日志消息类设计

日志消息类主要是封装一条完整的日志消息所需的内容,其中包括:

  • 时间:描述本条日志输出的时间;
  • 线程ID:描述本条日志是哪一个线程输出的;
  • 日志等级:描述本条日志的等级;
  • 日志文件名:描述本条日志在哪一个源码文件中输出的;
  • 日志行号:描述本条日志在源码文件的哪一行输出的;
  • 日志器名称:描述打印本条日志的日志器名称;
  • 日志数据:本条日志的有效数据载荷。

日志消息类设计较为简单,如下:

#ifndef __M_MSG_H__
#define __M_MSG_H__

/*
    定义日志消息类:
    1.日志的输出时间
    2.日志等级
    3.源文件名称
    4.源代码行号
    5.线程ID
    6.日志主体消息
    7.日志器名称
*/

#include <iostream>
#include <string>
#include <thread>
#include "util.hpp"
#include "level.hpp"

namespace LOG
{
    struct LogMsg
    {
        time_t _ctime; // 日志输出时间
        LogLevel::value _level; // 日志等级
        std::string _file; // 源文件名称
        size_t _line; // 源文件行号
        std::thread::id _tid; // 线程ID
        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::getTime()),
        _level(level),
        _line(line),
        _tid(std::this_thread::get_id()), // get_id 获取当前线程的ID
        _file(file),
        _logger(logger),
        _payload(msg) {}
    };
}
#endif

日志格式化类设计

日志格式化类成员介绍

日志消息格式化类主要负责将日志消息进行格式化。类中包含以下成员:

pattern

  • pattern:保存格式化规则字符串
    • 默认日志输出格式[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n
    • %d 表示日期,包含子格式 {%H:%M:%S}
    • %T 表示缩进;
    • %t 表示线程ID;
    • %c 表示日志器名称;
    • %f 表示源码文件名;
    • %l 表示源码行号;
    • %p 表示日志级别;
    • %m 表示主体消息;
    • %n 表示换行;

格式化规则字符串控制了日志的输出格式。定义格式化字符,就是为了方便让用户自己决定以何种形式将日志消息进行输出。

例如,在默认输出格式下,输出的日志消息为:
C++项目实战——基于多设计模式下的同步&amp;异步日志系统(总集篇)-LMLPHP

items

  • std::vector< FormatItem::ptr > items:用于按序保存格式化字符串对应的格式化子项对象。
    • MsgFormatItem:表示要从LogMsg中取出有效载荷;
    • LevelFormatItem:表示要从LogMsg中取出日志等级;
    • LoggerFormatItem:表示要从LogMsg中取出日志器名称;
    • ThreadFormatItem:表示要从LogMsg中取出线程ID;
    • TimeFormatItem:表示要从LogMsg中取出时间戳并按照指定格式进行格式化;
    • FileFormatItem:表示要从LogMsg中取出源码所在文件名;
    • LineFormatItem:表示要从LogMsg中取出源码所在行号;
    • TabFormatItem:表示一个制表符缩进;
    • NLineFormatItem:表示一个换行;
    • OtherFormatItem:表示非格式化的原始字符串;

一个日志消息对象包含许多元素,如时间、线程ID、文件名等等,我们针对不同的元素设计不同的格式化子项。

换句话说,不同的格式化子项从日志消息对象中提取出指定元素,转换为规则字符串并按顺序保存在一块内存空间中。

/*
    %d 表示日期,包含子格式 {%H:%M:%S}
    %t 表示线程ID
    %c 表示日志器名称
    %f 表示源码文件名
    %l 表示源码行号
    %p 表示日志级别
    %m 表示主体消息
    %n 表示换行
*/
class Formatter
{
public:
	// ...
private:
    std::string _pattern; // 格式化规则字符串
    std::vector<FormatItem::ptr> _items;
};

格式化子项类的设计

刚才提到对于一条日志消息message,其中包含很多元素(时间、线程ID等)。我们通过设计不同的格式化子项来取出指定的元素,并将它们追加到一块内存空间中。

但是由于不同的格式化子项类对象类型也各不相同,我们就采用多态的思想,抽象出一个格式化子项基类,基于基类派生出不同的格式化子项类。这样就可以定义父类指针的数组,指向不同的格式化子项子类对象。

抽象格式化子项基类
class FormatItem
{
public:
    using ptr = std::shared_ptr<FormatItem>;
    virtual void format(std::ostream &out, const LogMsg &msg) = 0;
};
  • 在基类中定义一个智能指针对象,方便管理;
  • format的参数为一个IO流对象,一个LogMsg对象。作用为提取LogMsg对象中的指定元素追加到流对象中。
日志主体消息子项
class MsgFormatItem : public FormatItem
{
public:
    void format(std::ostream &out, const LogMsg &msg) override
    {
        out << msg._payload;
    }
};
日志等级子项
class MsgFormatItem : public FormatItem
{
public:
    void format(std::ostream &out, const LogMsg &msg) override
    {
        out << msg._payload;
    }
};
时间子项
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);
        char tmp[32] = {0};
        strftime(tmp, 31, _time_fmt.c_str(), &t);
        out << tmp;
    }

private:
    std::string _time_fmt; // %H:%M:%S
};
  • 时间子项可以设置子格式,在构造函数中需要传递一个子格式字符串来控制时间子格式;
localtime_r介绍

在LogMsg对象中,时间元素是一个时间戳数字,不方便观察时间信息。我们需要将该时间戳转化为易于观察的时分秒的格式。

localtime_r函数是C标准库中的一个函数,用于将时间戳(表示自1970年1月1日以来的秒数)转换为本地时间的表示。这个函数是线程安全的版本,它接受两个参数:一个指向时间戳的指针和一个指向struct tm类型的指针,它会将转换后的本地时间信息存储在struct tm结构中。

函数原型如下:

struct tm *localtime_r(const time_t *timep, struct tm *result);
  • timep参数是指向时间戳的指针;
  • result参数是指向struct tm类型的指针,用于存储转换后的本地时间信息;
  • localtime_r函数返回一个指向struct tm结构的指针,同时也将结果存储在result参数中;
  • struct tm结构包含了年、月、日、时、分、秒等时间信息的成员变量,可用于格式化和输出时间;

struct tm类型
struct tm是C语言中的一个结构体类型,用于表示日期和时间的各个组成部分。

struct tm结构包含以下成员变量:

struct tm
{
	int tm_sec; // 秒(0-59)
	int tm_min; // 分钟(0-59)
	int tm_hour; // 小时(0-23)
	int tm_mday; // 一个月中的日期(1-31)
	int tm_mon; // 月份(0-11,0代表1月)
	int tm_year; // 年份(从1900年起的年数,例如,121表示2021年)
	int tm_wday; // 一周中的星期几(0-6,0代表星期日)
	int tm_yday; // 一年中的第几天(0-365)
	int tm_isdst; // 是否为夏令时(正数表示是夏令时,0表示不是,负数表示夏令时信息不可用)
}
strftime介绍

strftime函数是C标准库中的一个函数,用于将日期和时间按照指定的格式进行格式化,并将结果存储到一个字符数组中。这个函数在C语言中非常常用,特别是在需要将日期和时间以不同的格式输出到屏幕、文件或其他输出设备时。

函数原型如下:

size_t strftime(char *s, size_t maxsize, const char *format, const struct tm *timeptr);
  • s:一个指向字符数组的指针,用于存储格式化后的日期和时间字符串;
  • maxsize:指定了字符数组 s 的最大容量,以防止缓冲区溢出;
  • format:一个字符串,用于指定日期和时间的输出格式。该字符串可以包含- 各种格式化控制符,例如%Y表示年份,%m表示月份等等;
  • timeptr:一个指向struct tm 结构的指针,表示待格式化的日期和时间;

返回值:

  • strftime函数返回生成的字符数(不包括空终止符\0),如果生成的字符数大于 maxsize,则返回0,表示字符串无法完全存储在给定的缓冲区中。
源码文件名子项
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;
    }
};
线程ID子项
class ThreadFormatItem : public FormatItem
{
public:
    void format(std::ostream &out, const LogMsg &msg) override
    {
        out << msg._tid;
    }
};
日志器名称子项
class LoggerFormatItem : public FormatItem
{
public:
    void format(std::ostream &out, const LogMsg &msg) override
    {
        out << msg._logger;
    }
};
制表符子项
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";
    }
};
原始字符串子项
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;
};

解释一下原始字符串:

  • 例如一个格式化字符串为[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n,其中[]:等符号是不属于上述任何子项的,这些符号不需要被解析,它们会作为日志内容被直接输出。

日志格式化类的设计

设计思想

日志格式化Formatter类中提供四个接口:

class Formatter
{
public:
    using ptr = std::shared_ptr<Formatter>;
    Formatter(const std::string &pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");
    void format(std::ostream &out, const LogMsg &msg);
    std::string format(const LogMsg &msg);
private:
    bool parsePattern();
    // 根据不同的格式化字符创建不同得格式化子项对象
    FormatItem::ptr createItem(const std::string &key, const std::string &val);
private:
    std::string _pattern; // 格式化规则字符串
    std::vector<FormatItem::ptr> _items;
};
  • Formatter:构造函数,构造一个formatter对象。函数参数为一个格式化字符串用来初始化成员pattern
  • format:提供两个重载函数,函数作用为将LogMsg中元素提取出来交由对应的格式化子项处理;可以将LogMsg进行格式化,并追加到流对象当中,也可以直接返回格式化后的字符串;
  • parsePattern:用于解析规则字符串_pattern
  • createItem:用于根据不同的格式化字符串创建不同的格式化子项对象

接口实现

Formatter

// 时间{年-月-日 时:分:秒}缩进 线程ID 缩进 [日志级别] 缩进 [日志名称] 缩进 文件名:行号 缩进 消息换行
Formatter(const std::string &pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n")
    : _pattern(pattern)
{
    assert(parsePattern()); // 确保格式化字符串有效
}

format

// 对msg进行格式化
void format(std::ostream &out, const LogMsg &msg)
{
    for (auto &item : _items)
    {
        item->format(out, msg);
    }
}

std::string format(const LogMsg &msg)
{
    std::stringstream ss;
    format(ss, msg);
    return ss.str();
}

parsePattern

函数设计思想

  • 函数的主要逻辑是从前往后的处理格式化字符串。以默认格式化字符串"[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"为例:
    • 从前往后遍历,如果没有遇到%则说明之前的字符都是原始字符串;
    • 遇到%,则看紧随其后的是不是另一个%,如果是,则认为%就是原始字符串;
    • 如果%后面紧挨着的是格式化字符(c、f、l、S等),则进行处理;
    • 紧随格式化字符之后,如果有{,则认为在{之后、}之前都是子格式内容;

在处理过程中,我们需要将得到的结果保存下来,于是我们可以创建一个vector,类型为一个键值对(key,val)。如果是格式化字符,则key为该格式化字符valnull;若为原始字符串keynullval原始字符串内容

得到数组之后,根据数组内容,调用createItem函数创建对应的格式化子项对象,添加到items成员中。

bool parsePattern()
{
    std::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;
        }

        if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%')
        {
            val.push_back('%');
            pos += 2;
            continue;
        }

        if (val.empty() == false)
        {
            fmt_order.push_back(std::make_pair("", val));
            val.clear();
        }

        pos += 1;
        if (pos == _pattern.size())
        {
            std::cout << "%之后没有格式化字符\n";
            return false;
        }

        key = _pattern[pos];
        pos += 1;

        if (pos < _pattern.size() && _pattern[pos] == '{')
        {
            pos += 1;
            while (pos < _pattern.size() && _pattern[pos] != '}')
            {
                val.push_back(_pattern[pos++]);
            }

            if (pos == _pattern.size())
            {
                std::cout << "子规则{}匹配出错\n";
                return false;
            }

            pos += 1;
        }

        fmt_order.push_back(std::make_pair(key, val));
        key.clear();
        val.clear();
    }

    for (auto &it : fmt_order)
    {
        _items.push_back(createItem(it.first, it.second));
    }
    return true;
}

createItem

  • 根据不同的格式化字符创建不同得格式化子项对象;
// 根据不同的格式化字符创建不同得格式化子项对象
FormatItem::ptr createItem(const std::string &key, const std::string &val)
{
    if (key == "d")
        return std::make_shared<TimeFormatItem>(val);
    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 == "")
        return std::make_shared<OtherFormatItem>(val);
    std::cout << "没有对应的格式化字符串:%" << key << std::endl;
    abort();
}

至此,日志消息格式化类已经全部实现完毕。


日志输出格式化类整理

#ifndef __M_FMT_H__
#define __M_FMT_H__

#include "level.hpp"
#include "message.hpp"
#include <vector>
#include <sstream>
#include <ctime>
#include <cassert>

namespace LOG
{
    // 抽象格式化子项基类
    class FormatItem
    {
    public:
        using ptr = std::shared_ptr<FormatItem>;
        virtual void format(std::ostream &out, const LogMsg &msg) = 0;
    };

    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);
            char tmp[32] = {0};
            strftime(tmp, 31, _time_fmt.c_str(), &t);
            out << tmp;
        }

    private:
        std::string _time_fmt; // %H:%M:%S
    };

    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 ThreadFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << msg._tid;
        }
    };

    class LoggerFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << msg._logger;
        }
    };

    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";
        }
    };

    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;
    };

    /*
        %d 表示日期,包含子格式 {%H:%M:%S}
        %t 表示线程ID
        %c 表示日志器名称
        %f 表示源码文件名
        %l 表示源码行号
        %p 表示日志级别
        %m 表示主体消息
        %n 表示换行
    */
    class Formatter
    {
    public:
        using ptr = std::shared_ptr<Formatter>;
        // 时间{年-月-日 时:分:秒}缩进 线程ID 缩进 [日志级别] 缩进 [日志名称] 缩进 文件名:行号 缩进 消息换行
        Formatter(const std::string &pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n")
            : _pattern(pattern)
        {
            assert(parsePattern());
        }

        // 对msg进行格式化
        void format(std::ostream &out, const LogMsg &msg)
        {
            for (auto &item : _items)
            {
                item->format(out, msg);
            }
        }

        std::string format(const LogMsg &msg)
        {
            std::stringstream ss;
            format(ss, msg);
            return ss.str();
        }

    private:
        // 解析格式化字符串
        bool parsePattern()
        {
            std::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;
                }

                if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%')
                {
                    val.push_back('%');
                    pos += 2;
                    continue;
                }

                if (val.empty() == false)
                {
                    fmt_order.push_back(std::make_pair("", val));
                    val.clear();
                }

                pos += 1;
                if (pos == _pattern.size())
                {
                    std::cout << "%之后没有格式化字符\n";
                    return false;
                }

                key = _pattern[pos];
                pos += 1;

                if (pos < _pattern.size() && _pattern[pos] == '{')
                {
                    pos += 1;
                    while (pos < _pattern.size() && _pattern[pos] != '}')
                    {
                        val.push_back(_pattern[pos++]);
                    }

                    if (pos == _pattern.size())
                    {
                        std::cout << "子规则{}匹配出错\n";
                        return false;
                    }

                    pos += 1;
                }

                fmt_order.push_back(std::make_pair(key, val));
                key.clear();
                val.clear();
            }

            for (auto &it : fmt_order)
            {
                _items.push_back(createItem(it.first, it.second));
            }
            return true;
        }

        // 根据不同的格式化字符创建不同得格式化子项对象
        FormatItem::ptr createItem(const std::string &key, const std::string &val)
        {
            if (key == "d")
                return std::make_shared<TimeFormatItem>(val);
            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 == "")
                return std::make_shared<OtherFormatItem>(val);
            std::cout << "没有对应的格式化字符串:%" << key << std::endl;
            abort();
        }

    private:
        std::string _pattern; // 格式化规则字符串
        std::vector<FormatItem::ptr> _items;
    };
}

#endif

为了避免因为写日志的过程阻塞,导致业务线程在写日志的时候影响其效率(例如由于网络原因导致日志写入阻塞,进而导致业务线程阻塞),因此我们需要设计一个异步日志器

异步的思想就是不让业务线程进行日志的实际落地操作,而是将日志消息放到缓冲区(一块指定内存)当中,接下来有一个专门的异步线程,去针对缓冲区中的数据进行处理(实际落地操作)

所以,异步日志器的实现思想:

  • 设计一个线程安全的缓冲区
  • 创建一个异步工作线程,专门负责缓冲区中日志消息落地操作。

异步缓冲区设计思想

在任务池的设计中,有很多备选方案,比如队列、循环队列等,但是不管哪一种都会涉及到锁冲突的情况,因为在生产者与消费者模型中,任何两个角色之间具有互斥关系,因此每一次任务的添加与取出都有可能涉及锁的冲突

所以我们采用双缓冲区的的设计思想,优势在于:

  • 避免了空间的频繁申请与释放,且尽可能的减少了生产者与消费则之间锁冲突的概率,提高了任务处理效率

双缓冲区的设计思想是:采用两个缓冲区,一个用来进行任务写入(push pool),一个进行任务处理(pop pool)。当异步工作线程(消费者)将缓冲区中的数据全部处理完毕之后,然后交换两个缓冲区,重新对新的缓冲区中的任务进行处理,虽然同时多线程写入也会产生冲突,但是冲突并不会像每次只处理一条的时候频繁(减少了消费者与生产者之间的锁冲突),且不涉及到空间的频繁申请释放所带来的的消耗。

C++项目实战——基于多设计模式下的同步&amp;异步日志系统(总集篇)-LMLPHP

异步缓冲区类设计

类中包含的成员:

  • 一个存放字符串数据的缓冲区(使用vector进行空间管理);
  • 当前写入数据位置的指针(指向可写区域的起始位置,避免数据的写入覆盖);
  • 当前读取数据位置的指针(指向可读区域的起始位置,当读取指针与写入指针指向相同的位置表示数据读取完了);

类中提供的操作:

  • 向缓冲区中写入数据
  • 获取可读数据起始地址的接口
  • 获取可读数据长度的接口
  • 移动读写位置的接口
  • 初始化缓冲区的操作(将读写位置初始化–在一个缓冲区所有数据处理完毕之后);
  • 提供交换缓冲区的操作(交换空间地址,并不交换空间数据)。

注意,缓冲区中直接存放格式化后的日志消息字符串,而不是LogMsg对象,这样做有两个好处:

  • 减少了LogMsg对象频繁的构造的消耗;
  • 可以针对缓冲区中的日志消息,一次性进行IO操作,减少IO次数,提高效率。
#ifndef __M_BUFFER_H__
#define __M_BUFFER_H__
#include "util.hpp"
#include <vector>
#include <cassert>

namespace LOG
{
    #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() : _buffer(DEFAULT_BUFFER_SIZE), _writer_idx(0), _reader_idx(0) {}
        // 向缓冲区中写入数据
        void push(const char* data, size_t len)
        {
            // 1.考虑空间不够则扩容
            ensureEnoughSize(len);
            // 2.将数据拷贝到缓冲区
            std::copy(data, data + len, &_buffer[_writer_idx]);
            // 3.将当前写入位置向后偏移
            moveWriter(len);
        }
        // 返回可读数据的起始地址
        const char* begin()
        {
            return &_buffer[_reader_idx];
        }
        // 返回可写数据的长度
        size_t writeAbleSize()
        {
            // 对于扩容思路并没有用, 仅针对固定大小缓冲区
            return (_buffer.size() - _writer_idx);
        }
        // 返回可读数据的长度
        size_t readAbleSize()
        { 
            return (_writer_idx - _reader_idx);
        }
        // 重置读写位置
        void reset()
        {
            _writer_idx = 0;
            _reader_idx = 0;
        }
        // 对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 + len; // 小于阈值则翻倍增长
            }
            else
            {
                new_size = _buffer.size() + INCREMENT_BUFFER_SIZE + len;
            }
            _buffer.resize(new_size);
        }
        // 对读指针进行向后偏移操作
        void moveReader(size_t len)
        {
            assert(len <= readAbleSize());
            _reader_idx += len;
        }
        // 对写指针进行向后偏移操作
        void moveWriter(size_t len)
        {
            assert(len + _writer_idx<= _buffer.size());
            _writer_idx += len;
        }
    private:
        std::vector<char> _buffer;
        size_t _reader_idx; // 当前可读数据的指针
        size_t _writer_idx; // 当前可写数据的指针
    };
}
#endif

异步工作器类设计

异步工作器的主要任务是,对缓冲区中的数据进行处理,若处理缓冲区中没有数据了则交换缓冲区

异步工作器类管理的成员有:

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

异步工作器类提供的操作有:

  • 停止异步工作器
  • 添加数据到缓冲区

私有操作:

  • 创建线程
  • 线程入口函数:在线程入口函数中交换缓冲区,对消费缓冲区数据使用回调函数进行处理,处理完后再次交换;
#ifndef __M_LOOPER_H__
#define __M_LOOPER_H__
#include "buffer.hpp"
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <condition_variable>
#include <functional>
#include <memory>

namespace LOG
{
    using Functor = std::function<void(Buffer &)>;
    enum class AsyncType
    {
        ASYNC_SAFE, // 安全状态, 表示缓冲区满了则阻塞,避免资源耗尽的风险
        ASYNC_UNSAFE // 不考虑资源耗尽的问题
    };

    class AsyncLooper
    {
    public:
        using ptr = std::shared_ptr<AsyncLooper>;

        AsyncLooper(const Functor& cb, AsyncType looper_type = AsyncType::ASYNC_SAFE)
            :_looper_type(looper_type),
            _stop(false),
            _call_back(cb),
            _thread(std::thread(&AsyncLooper::threadEntry, this))
        {}

        ~AsyncLooper() { stop(); }

        void stop() 
        {
            _stop = true; // 将退出标志设置为true
            _con_cond.notify_all(); // 唤醒所有工作线程
            _thread.join(); // 等待工作线程退出
        }
        void push(const char* data, size_t len)
        {
            std::unique_lock<std::mutex> lock(_mutex);
            // 条件变量控制,若缓冲区剩余空间大小等于数据长度,则可以添加数据
            if(_looper_type == AsyncType::ASYNC_SAFE)
                _pro_cond.wait(lock, [&](){ return _pro_buf.writeAbleSize() >= len; });
            // 能够走下来说明条件满足,可以向缓冲区添加数据了
            _pro_buf.push(data, len);
            // 唤醒消费者对缓冲区中的数据进行处理
            _con_cond.notify_one();
        }
    private:
        // 线程入口函数 -- 对消费者缓冲区中的数据进行处理,处理完毕后,初始化缓冲区,交换缓冲区
        void threadEntry()
        {
            // 为互斥锁设置一个声明周期,当缓冲区交换完毕就解锁
            while(1)
            {
                {
                    // 1.判断生产缓冲区有没有数据,有则交换,无则阻塞
                    std::unique_lock<std::mutex> lock(_mutex);
                    if(_stop && _pro_buf.empty()) break;
                    // 若当前是退出前被唤醒或者是有数据被唤醒,则返回真,继续向下运行,否则重新进入休眠
                    _con_cond.wait(lock, [&](){ return _stop || !_pro_buf.empty(); });
                    _con_buf.swap(_pro_buf);
                    // 2.唤醒生产者
                    if(_looper_type == AsyncType::ASYNC_SAFE)
                        _pro_cond.notify_all();
                }
                // 3.被唤醒后,对消费者缓冲区进行数据处理
                _call_back(_con_buf);
                // 4.初始化消费者缓冲区
                _con_buf.reset();
            }
        }
    private:
        Functor _call_back;
    private:
        AsyncType _looper_type;
        std::atomic<bool> _stop;
        Buffer _pro_buf;
        Buffer _con_buf;
        std::mutex _mutex;
        std::condition_variable _pro_cond;
        std::condition_variable _con_cond;
        std::thread _thread; // 异步工作器对应的工作线程
    };
}
#endif

异步日志器设计

异步日志器继承自日志器类,并在同步日志器类上拓展了异步工作器。当我们需要异步输出日志的时候,需要创建异步日志器和消息处理器,调用异步日志器的log、debug、error、info、fatal等函数输出不同级别日志。

  • log函数为重写Logger类的函数,主要实现将日志日志数据加入异步缓冲区;
  • realLog函数主要由异步线程调用(是为异步工作器设置的回调函数),完成日志的实际落地操作。
class AsyncLogger : public Logger
{
public:
    AsyncLogger(const std::string &logger_name,
                LogLevel::value level,
                LOG::Formatter::ptr &formatter,
                std::vector<LogSink::ptr> &sinks,
                AsyncType looper_type)
        : Logger(logger_name, level, formatter, sinks),
          _looper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::realLog, this, std::placeholders::_1), looper_type))
    {}
    // 将数据写入缓冲区 
    void log(const char *data, size_t len)
    {
        _looper->push(data, len);
    }
    // 设计一个实际落地函数
    void realLog(Buffer &buf)
    {
        if (_sinks.empty())
            return;
        for (auto &sink : _sinks)
        {
            sink->log(buf.begin(), buf.readAbleSize());
        }
    }

private:
    AsyncLooper::ptr _looper; // 异步工作器
};

异步缓冲区类整理

#ifndef __M_BUFFER_H__
#define __M_BUFFER_H__
#include "util.hpp"
#include <vector>
#include <cassert>

namespace LOG
{
    #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() : _buffer(DEFAULT_BUFFER_SIZE), _writer_idx(0), _reader_idx(0) {}
        // 向缓冲区中写入数据
        void push(const char* data, size_t len)
        {
            // 1.考虑空间不够则扩容
            ensureEnoughSize(len);
            // 2.将数据拷贝到缓冲区
            std::copy(data, data + len, &_buffer[_writer_idx]);
            // 3.将当前写入位置向后偏移
            moveWriter(len);
        }
        // 返回可读数据的起始地址
        const char* begin()
        {
            return &_buffer[_reader_idx];
        }
        // 返回可写数据的长度
        size_t writeAbleSize()
        {
            // 对于扩容思路并没有用, 仅针对固定大小缓冲区
            return (_buffer.size() - _writer_idx);
        }
        // 返回可读数据的长度
        size_t readAbleSize()
        { 
            return (_writer_idx - _reader_idx);
        }
        // 重置读写位置
        void reset()
        {
            _writer_idx = 0;
            _reader_idx = 0;
        }
        // 对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 + len; // 小于阈值则翻倍增长
            }
            else
            {
                new_size = _buffer.size() + INCREMENT_BUFFER_SIZE + len;
            }
            _buffer.resize(new_size);
        }
        // 对读指针进行向后偏移操作
        void moveReader(size_t len)
        {
            assert(len <= readAbleSize());
            _reader_idx += len;
        }
        // 对写指针进行向后偏移操作
        void moveWriter(size_t len)
        {
            assert(len + _writer_idx<= _buffer.size());
            _writer_idx += len;
        }
    private:
        std::vector<char> _buffer;
        size_t _reader_idx; // 当前可读数据的指针
        size_t _writer_idx; // 当前可写数据的指针
    };
}
#endif

异步工作器类整理

#ifndef __M_LOOPER_H__
#define __M_LOOPER_H__
#include "buffer.hpp"
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <condition_variable>
#include <functional>
#include <memory>

namespace LOG
{
    using Functor = std::function<void(Buffer &)>;
    enum class AsyncType
    {
        ASYNC_SAFE, // 安全状态, 表示缓冲区满了则阻塞,避免资源耗尽的风险
        ASYNC_UNSAFE // 不考虑资源耗尽的问题
    };
    class AsyncLooper
    {
    public:
        using ptr = std::shared_ptr<AsyncLooper>;

        AsyncLooper(const Functor& cb, AsyncType looper_type = AsyncType::ASYNC_SAFE)
            :_looper_type(looper_type),
            _stop(false),
            _call_back(cb),
            _thread(std::thread(&AsyncLooper::threadEntry, this))
        {}

        ~AsyncLooper() { stop(); }

        void stop() 
        {
            _stop = true; // 将退出标志设置为true
            _con_cond.notify_all(); // 唤醒所有工作线程
            _thread.join(); // 等待工作线程退出
        }
        void push(const char* data, size_t len)
        {
            std::unique_lock<std::mutex> lock(_mutex);
            // 条件变量控制,若缓冲区剩余空间大小等于数据长度,则可以添加数据
            if(_looper_type == AsyncType::ASYNC_SAFE)
                _pro_cond.wait(lock, [&](){ return _pro_buf.writeAbleSize() >= len; });
            // 能够走下来说明条件满足,可以向缓冲区添加数据了
            _pro_buf.push(data, len);
            // 唤醒消费者对缓冲区中的数据进行处理
            _con_cond.notify_one();
        }
    private:
        // 线程入口函数 -- 对消费者缓冲区中的数据进行处理,处理完毕后,初始化缓冲区,交换缓冲区
        void threadEntry()
        {
            // 为互斥锁设置一个生命周期,当缓冲区交换完毕就解锁
            while(1)
            {
                {
                    // 1.判断生产缓冲区有没有数据,有则交换,无则阻塞
                    std::unique_lock<std::mutex> lock(_mutex);
                    if(_stop && _pro_buf.empty()) break;
                    // 若当前是退出前被唤醒或者是有数据被唤醒,则返回真,继续向下运行,否则重新进入休眠
                    _con_cond.wait(lock, [&](){ return _stop || !_pro_buf.empty(); });
                    _con_buf.swap(_pro_buf);
                    // 2.唤醒生产者
                    if(_looper_type == AsyncType::ASYNC_SAFE)
                        _pro_cond.notify_all();
                }
                // 3.被唤醒后,对消费者缓冲区进行数据处理
                _call_back(_con_buf);
                // 4.初始化消费者缓冲区
                _con_buf.reset();
            }
        }
    private:
        Functor _call_back;
    private:
        AsyncType _looper_type; // 选择异步工作器工作模式(安全与非安全模式)
        std::atomic<bool> _stop; // 工作器退出标志
        Buffer _pro_buf; 
        Buffer _con_buf; 
        std::mutex _mutex;
        std::condition_variable _pro_cond;
        std::condition_variable _con_cond;
        std::thread _thread; // 异步工作器对应的工作线程
    };
}
#endif

日志器建造者类完善

实现了异步日志器设计之后,将异步日志器添加到日志器建造者类当中。

// 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 buildEnableUnSafeAsync() { _looper_type = AsyncType::ASYNC_UNSAFE; }
    void buildLoggerName(const std::string &name) { _logger_name = name; }
    void buildLoggerLevel(LogLevel::value level) { _limit_level = level; }
    void buildFormatter(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;
    std::atomic<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);
    }
};

单例日志器管理类设计思想

通过局部日志器建造者创建的日志器受到作用域的限制。但是日志的输出,我们希望能够在任意位置。

因此为了突破日志器作用域的限制,我们创建一个日志器管理类,且该类是一个单例类,这样我们就可以在任意位置通过单例管理器单例获取到指定的日志器进行输出了。

基于单例日志器管理类的设计思想,我们对于日志器建造者类进行继承,继承出一个全局日志器建造者类,实现一个日志器在创建完毕后,直接将其添加到单例的日志器管理器当中,以便于能够在任意位置通过日志器名称能够获取到指定的日志器进行输出。

日志器管理器的作用

  • 对所有创建的日志器进行管理
  • 可以在程序的任意位置进,获取相同的单例对象,获取其中的日志器进行日志输出

单例日志器管理类设计

管理的成员:

  • 默认日志器
  • 所管理的日志器数组(使用哈希表,日志器名称为key,日志器对象为value);
  • 互斥锁

提供的操作:

  • 添加日志器管理
  • 判断是否管理了指定名称的日志器
  • 获取指定名称的日志器
  • 获取默认日志器
class LoggerManager
{
public:
    static LoggerManager& getInstance()
    {
        // c++11之后,静态局部变量,编译器在编译的层面实现了线程安全
        // 当静态局部变量在没有构造完成之前,其他线程进入就会阻塞
        static LoggerManager eton;
        return eton;
    }
    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));
    }
    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;
    }
    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;
    }
    Logger::ptr rootLogger()
    {
        return _root_logger;
    }
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:
    std::mutex _mutex;
    Logger::ptr _root_logger; // 默认日志器
    std::unordered_map<std::string, Logger::ptr> _loggers // 日志器数组;
};

全局建造者类设计

为了降低用户的使用复杂度,我们提供一个全局日志器建造者类。全局建造者类的设计思想非常简单,即在局部的基础上增加了一个功能:

  • 将日志器添加到单例对象中
class GlobalLoggerBuilder : 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>();
        }

        Logger::ptr logger;
        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;
    }
};

日志器类、建造者类整理

#ifndef __M_LOGGER_H__
#define __M_LOGGER_H__
#include "util.hpp"
#include "level.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "looper.hpp"
#include <cstdarg>
#include <atomic>
#include <thread>
#include <mutex>
#include <unordered_map>

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),
               _sinks(sinks.begin(), sinks.end())
        {}

        const std::string& name(){ return _logger_name; }
        void debug(const std::string &file, size_t line, const std::string &fmt, ...)
        {
            // 通过传入的参数构造出一个日志消息对象, 进行日志格式化,最终落地
            // 判断当前的日志是否达到了输出等级
            if (LogLevel::value::DEBUG < _limit_level)
            {
                return;
            }

            // 对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, ...)
        {
            // 通过传入的参数构造出一个日志消息对象, 进行日志格式化,最终落地
            if (LogLevel::value::INFO < _limit_level)
            {
                return;
            }

            // 对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::INFO, file, line, res);
            free(res);
        }
        void warn(const std::string &file, size_t line, const std::string &fmt, ...)
        {
            // 通过传入的参数构造出一个日志消息对象, 进行日志格式化,最终落地
            if (LogLevel::value::WARN < _limit_level)
            {
                return;
            }
            // 对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::WARN, file, line, res);
            free(res);
        }
        void error(const std::string &file, size_t line, const std::string &fmt, ...)
        {
            // 通过传入的参数构造出一个日志消息对象, 进行日志格式化,最终落地
            if (LogLevel::value::ERROR < _limit_level)
            {
                return;
            }
            // 对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::ERROR, file, line, res);
            free(res);
        }
        void fatal(const std::string &file, size_t line, const std::string &fmt, ...)
        {
            // 通过传入的参数构造出一个日志消息对象, 进行日志格式化,最终落地
            if (LogLevel::value::FATAL < _limit_level)
            {
                return;
            }
            // 对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::FATAL, file, line, res);
            free(res);
        }

    protected:
        void serialize(LogLevel::value level, const std::string &file, size_t line, char *str)
        {
            // 构造LogMsg对象
            LogMsg msg(level, line, file, _logger_name, str);
            // 通过格式化工具对LogMsg进行格式化, 得到格式化后的日志字符串
            std::stringstream ss;
            _formatter->format(ss, msg);
            // 对日志进行落地
            log(ss.str().c_str(), ss.str().size());
        }
        virtual void log(const char *data, size_t len) = 0;

    protected:
        std::mutex _mutex;
        std::string _logger_name;                  // 日志器名称
        std::atomic<LogLevel::value> _limit_level; // 限制输出等级
        Formatter::ptr _formatter;
        std::vector<LogSink::ptr> _sinks;
    };

    class SyncLogger : public Logger
    {
    public:
        SyncLogger(const std::string &logger_name,
                   LogLevel::value level,
                   LOG::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 (_sinks.empty())
                return;
            for (auto &sink : _sinks)
            {
                sink->log(data, len);
            }
        }
    };

    class AsyncLogger : public Logger
    {
    public:
        AsyncLogger(const std::string &logger_name,
                    LogLevel::value level,
                    LOG::Formatter::ptr &formatter,
                    std::vector<LogSink::ptr> &sinks,
                    AsyncType looper_type)
            : Logger(logger_name, level, formatter, sinks),
              _looper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::realLog, this, std::placeholders::_1), looper_type))
        {}
        // 将数据写入缓冲区 
        void log(const char *data, size_t len)
        {
            _looper->push(data, len);
        }
        // 设计一个实际落地函数
        void realLog(Buffer &buf)
        {
            if (_sinks.empty())
                return;
            for (auto &sink : _sinks)
            {
                sink->log(buf.begin(), buf.readAbleSize());
            }
        }

    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 buildEnableUnSafeAsync() { _looper_type = AsyncType::ASYNC_UNSAFE; }
        void buildLoggerName(const std::string &name) { _logger_name = name; }
        void buildLoggerLevel(LogLevel::value level) { _limit_level = level; }
        void buildFormatter(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;
        std::atomic<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);
        }
    };

    class LoggerManager
    {
    public:
        static LoggerManager& getInstance()
        {
            // c++11之后,静态局部变量,编译器在编译的层面实现了线程安全
            // 当静态局部变量在没有构造完成之前,其他线程进入就会阻塞
            static LoggerManager eton;
            return eton;
        }
        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));
        }
        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;
        }
        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;
        }
        Logger::ptr rootLogger()
        {
            return _root_logger;
        }
    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:
        std::mutex _mutex;
        Logger::ptr _root_logger; // 默认日志器
        std::unordered_map<std::string, Logger::ptr> _loggers // 日志器数组;
    };

    class GlobalLoggerBuilder : 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>();
            }

            Logger::ptr logger;
            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;
        }
    };
}

#endif

日志器管理类测试

#include "logger.hpp"

void log_test()
{
    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 < 300000)
    {   
        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->buildFormatter("[%c]%m%n");
    builder->buildLoggerType(LOG::LoggerType::LOGGER_ASYNC);
    builder->buildEnableUnSafeAsync();
    builder->buildSink<LOG::FileSink>("./logfile/async.log");
    builder->buildSink<LOG::StdOutSink>();
    builder->build();
    log_test();
    return 0;
}

日志宏&全局接口设计

本章我们将完成提供全局接口&宏函数,对日志系统接口进行使用便捷性优化(避免用户自己创建单例)。

设计思想:

  • 提供获取指定日志器的全局接口(避免用户自己操作单例对象);
  • 使用宏函数对日志器的接口进行代理(代理模式);
  • 提供宏函数,直接通过默认日志器进行日志的标准输出打印(省去获取日志器的操作);
#ifndef __MY_LOG__
#define __MY_LOG__
#include "logger.hpp"

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__)

}
#endif

全局接口测试

测试代码

void log_test()
{
    DEBUG("%s", "测试日志");
    INFO("%s", "测试日志");
    WARN("%s", "测试日志");
    ERROR("%s", "测试日志");
    FATAL("%s", "测试日志");

    size_t count = 0;
    while(count < 300000)
    {
        FATAL("测试日志-%d", count++);
    }
}
int main()
{
	log_test();
	return 0;
}

项目目录结构整理

C++项目实战——基于多设计模式下的同步&amp;异步日志系统(总集篇)-LMLPHP
C++项目实战——基于多设计模式下的同步&amp;异步日志系统(总集篇)-LMLPHP

10-18 08:57