1.项目介绍
项⽬介绍
本项⽬主要实现⼀个⽇志系统, 其主要⽀持以下功能:
• ⽀持多级别⽇志消息
• ⽀持同步⽇志和异步⽇志
• ⽀持可靠写⼊⽇志到控制台、⽂件以及滚动⽂件中
• ⽀持多线程程序并发写⽇志
• ⽀持扩展不同的⽇志落地⽬标地
2.开发环境
• CentOS 7
• vscode/vim
• g++/gdb
• Makefile
3.核心技术
• 类层次设计(继承和多态的应⽤)
• C++11(多线程、auto、智能指针、右值引⽤等)
• 双缓冲区
• ⽣产消费模型
• 多线程
• 设计模式(单例、⼯⼚、代理、模板等)
4.环境搭建
本项⽬不依赖其他任何第三⽅库, 只需要安装好CentOS/Ubuntu + vscode/vim环境即可开发。
5.日志系统介绍
5.1为什么需要日志系统
• ⽣产环境的产品为了保证其稳定性及安全性是不允许开发⼈员附加调试器去排查问题, 可以借助⽇志系统来打印⼀些⽇志帮助开发⼈员解决问题
• 上线客⼾端的产品出现bug⽆法复现并解决, 可以借助⽇志系统打印⽇志并上传到服务端帮助开发⼈员进⾏分析
• 对于⼀些⾼频操作(如定时器、⼼跳包)在少量调试次数下可能⽆法触发我们想要的⾏为,通过断
点的暂停⽅式,我们不得不重复操作⼏⼗次、上百次甚⾄更多,导致排查问题效率是⾮常低下, 可以借助打印⽇志的⽅式查问题
• 在分布式、多线程/多进程代码中, 出现bug⽐较难以定位, 可以借助⽇志系统打印log帮助定位bug
• 帮助⾸次接触项⽬代码的新开发⼈员理解代码的运⾏流程
5.2日志系统技术实现
⽇志系统的技术实现主要包括三种类型:
• 利⽤printf、std::cout等输出函数将⽇志信息打印到控制台
• 对于⼤型商业化项⽬, 为了⽅便排查问题,我们⼀般会将⽇志输出到⽂件或者是数据库系统⽅便查
询和分析⽇志, 主要分为同步⽇志和异步⽇志⽅式
◦ 同步写⽇志
◦ 异步写⽇志
5.2.1同步写日志
同步⽇志是指当输出⽇志时,必须等待⽇志输出语句执⾏完毕后,才能执⾏后⾯的业务逻辑语句,⽇志输出语句与程序的业务逻辑语句将在同⼀个线程运⾏。每次调⽤⼀次打印⽇志API就对应⼀次系统调⽤write写⽇志⽂件。
在⾼并发场景下,随着⽇志数量不断增加,同步⽇志系统容易产⽣系统瓶颈:
• ⼀⽅⾯,⼤量的⽇志打印陷⼊等量的write系统调⽤,有⼀定系统开销.
• 另⼀⽅⾯,使得打印⽇志的进程附带了⼤量同步的磁盘IO,影响程序性能
5.2.2异步写日志
异步⽇志是指在进⾏⽇志输出时,⽇志输出语句与业务逻辑语句并不是在同⼀个线程中运⾏,⽽是有专⻔的线程⽤于进⾏⽇志输出操作。业务线程只需要将⽇志放到⼀个内存缓冲区中不⽤等待即可继续执⾏后续业务逻辑(作为⽇志的⽣产者),⽽⽇志的落地操作交给单独的⽇志线程去完成(作为⽇志的消费者), 这是⼀个典型的⽣产-消费模型。
这样做的好处是即使⽇志没有真的地完成输出也不会影响程序的主业务,可以提⾼程序的性能:
• 主线程调⽤⽇志打印接⼝成为⾮阻塞操作
• 同步的磁盘IO从主线程中剥离出来交给单独的线程完成
6.相关技术支持补充
6.1不定参函数
6.1不定参宏函数
#include <iostream>
#include <cstdarg>
#define LOG(fmt, ...) printf("[%s:%d] " fmt "\n", __FILE__, __LINE__,##__VA_ARGS__)
int main()
{
LOG("%s-%s", "hello", "dhy");
return 0;
}
6.2c风格不定参函数
void printNum(int n, ...)
{
va_list al;
va_start(al, n);//让al指向n参数之后的第⼀个可变参数
for (int i = 0; i < n; i++)
{
int num = va_arg(al, int);//从可变参数中取出⼀个整形参数
std::cout << num << std::endl;
}
va_end(al);//清空可变参数列表--其实是将al置空
}
int main()
{
//LOG("%s-%s", "hello", "dhy");
printNum(3, 11,22,33);
printNum(5, 44,55,66,77,88);
return 0;
}
vasprintf会根据format字符串和可变参数列表ap的内容动态的分配足够的内存来存储格式化后的字符串,并将地址存储在strp指针中,如果成功,就会返回格式化后的字符串的长度
void myprintf(const char *fmt, ...)
{
//int vasprintf(char **strp, const char *fmt, va_list ap);
char *res;
va_list al;
va_start(al, fmt);
int len = vasprintf(&res, fmt, al);
va_end(al);
std::cout << res << std::endl;
free(res);
}
int main()
{
myprintf("%s-%d", "dhy", 22);
return 0;
}
6.3C++⻛格不定参函数
#include <iostream>
#include <cstdarg>
#include <memory>
#include <functional>
void xprintf()
{
std::cout << std::endl;
}
template<typename T, typename ...Args>
void xprintf(const T &value, Args &&...args) //万能引用
{
std::cout << value << " ";
if ((sizeof ...(args)) > 0)
{
xprintf(std::forward<Args>(args)...);//完美转发
}else
{
xprintf();
}
}
int main()
{
//相当于每次都传入第一个字符串给value,然后在递归,直到打印结束
xprintf("dhy");
xprintf("dhy", "好困");
xprintf("dhy", "每天都", "好困");
return 0;
}
6.2 设计模式
设计模式是前辈们对代码开发经验的总结,是解决特定问题的⼀系列套路。它不是语法规定,⽽是⼀套⽤来提⾼代码可复⽤性、可维护性、可读性、稳健性以及安全性的解决⽅案。
六⼤原则:
• 单⼀职责原则 (Single Responsibility Principle);
◦ 类的职责应该单⼀,⼀个⽅法只做⼀件事。职责划分清晰了,每次改动到最⼩单位的⽅法或类。
◦ 使⽤建议:两个完全不⼀样的功能不应该放⼀个类中,⼀个类中应该是⼀组相关性很⾼的函数、数据的封装
◦ ⽤例:⽹络聊天:⽹络通信 & 聊天,应该分割成为⽹络通信类 & 聊天类
• 开闭原则 (Open Closed Principle);
◦ 对扩展开放,对修改封闭
◦ 使⽤建议:对软件实体的改动,最好⽤扩展⽽⾮修改的⽅式。
◦ ⽤例:超时卖货:商品价格---不是修改商品的原来价格,⽽是新增促销价格。
• ⾥⽒替换原则 (Liskov Substitution Principle);
◦ 通俗点讲,就是只要⽗类能出现的地⽅,⼦类就可以出现,⽽且替换为⼦类也不会产⽣任何错误或异常。
◦ 在继承类时,务必重写⽗类中所有的⽅法,尤其需要注意⽗类的protected⽅法,⼦类尽量不要暴露⾃⼰的public⽅法供外界调⽤。
◦ 使⽤建议:⼦类必须完全实现⽗类的⽅法,孩⼦类可以有⾃⼰的个性。覆盖或实现⽗类的⽅法时,输⼊参数可以被放⼤,输出可以缩⼩
◦ ⽤例:跑步运动员类-会跑步,⼦类⻓跑运动员-会跑步且擅⻓⻓跑, ⼦类短跑运动员-会跑步且擅⻓短跑
• 依赖倒置原则 (Dependence Inversion Principle)。
◦ ⾼层模块不应该依赖低层模块,两者都应该依赖其抽象. 不可分割的原⼦逻辑就是低层模式,原⼦逻辑组装成的就是⾼层模块。
◦ 模块间依赖通过抽象(接⼝)发⽣,具体类之间不直接依赖
◦ 使⽤建议:每个类都尽量有抽象类,任何类都不应该从具体类派⽣。尽量不要重写基类的⽅法。结合⾥⽒替换原则使⽤。
◦ ⽤例:奔驰⻋司机类--只能开奔驰; 司机类 -- 给什么⻋,就开什么⻋; 开⻋的⼈:司机--依赖于抽象
• 迪⽶特法则 (Law of Demeter),⼜叫"最少知道法则";
◦ 尽量减少对象之间的交互,从⽽减⼩类之间的耦合。⼀个对象应该对其他对象有最少的了解。
对类的低耦合提出了明确的要求:
▪ 只和直接的朋友交流, 朋友之间也是有距离的。⾃⼰的就是⾃⼰的(如果⼀个⽅法放在本类
中,既不增加类间关系,也对本类不产⽣负⾯影响,那就放置在本类中)。
◦ ⽤例:⽼师让班⻓点名--⽼师给班⻓⼀个名单,班⻓完成点名勾选,返回结果,⽽不是班⻓点名,⽼师勾选
• 接⼝隔离原则 (Interface Segregation Principle);
◦ 客⼾端不应该依赖它不需要的接⼝,类间的依赖关系应该建⽴在最⼩的接⼝上
◦ 使⽤建议:接⼝设计尽量精简单⼀,但是不要对外暴露没有实际意义的接⼝。
◦ ⽤例:修改密码,不应该提供修改⽤⼾信息接⼝,⽽就是单⼀的最⼩修改密码接⼝,更不要暴
露数据库操作
从整体上来理解六⼤设计原则,可以简要的概括为⼀句话,⽤抽象构建框架,⽤实现扩展细节,具体
到每⼀条设计原则,则对应⼀条注意事项:
• 单⼀职责原则告诉我们实现类要职责单⼀;
• ⾥⽒替换原则告诉我们不要破坏继承体系;
• 依赖倒置原则告诉我们要⾯向接⼝编程;
• 接⼝隔离原则告诉我们在设计接⼝的时候要精简单⼀;
• 迪⽶特法则告诉我们要降低耦合;
• 开闭原则是总纲,告诉我们要对扩展开放,对修改关闭。
6.2.1单例模式
⼀个类只能创建⼀个对象,即单例模式,该设计模式可以保证系统中该类只有⼀个实例,并提供⼀个访问它的全局访问点,该实例被所有程序模块共享。⽐如在某个服务器程序中,该服务器的配置信息
存放在⼀个⽂件中,这些配置数据由⼀个单例对象统⼀读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种⽅式简化了在复杂环境下的配置管理。
单例模式有两种实现模式:饿汉模式和懒汉模式
6.2.1.1饿汉模式
饿汉模式: 程序启动时就会创建⼀个唯⼀的实例对象。 因为单例对象已经确定, 所以⽐较适⽤于多线程环境中, 多线程获取单例对象不需要加锁, 可以有效的避免资源竞争, 提⾼性能。
//饿汉模式
template<typename T>
class Singleton
{
private:
static Singleton _eton;
private:
Singleton(){}
~Singleton(){}
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static T& getInstance()
{
return _eton;
}
};
Singleton Singleton::_eton;
6.2.1.2 懒汉模式
懒汉模式:第⼀次使⽤要使⽤单例对象的时候创建实例对象。如果单例对象构造特别耗时或者耗费济源(加载插件、加载⽹络资源等), 可以选择懒汉模式, 在第⼀次使⽤的时候才创建对象。
◦ 这⾥介绍的是《Effective C++》⼀书作者 Scott Meyers 提出的⼀种更加优雅简便的单例模式
Meyers' Singleton in C++。
◦ C++11 Static local variables 特性以确保C++11起,静态变量将能够在满⾜ thread-safe 的前提下唯⼀地被构造和析构
// 懒汉模式
template <typename T>
class Singleton
{
private:
Singleton(){}
~Singleton(){}
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static T& getInstance()
{
static Singleton _eton;
return _eton;
}
};
6.2.2 工厂模式
⼯⼚模式
⼯⼚模式是⼀种创建型设计模式, 它提供了⼀种创建对象的最佳⽅式。在⼯⼚模式中,我们创建对象时不会对上层暴露创建逻辑,⽽是通过使⽤⼀个共同结构来指向新创建的对象,以此实现创建-使⽤的分离。
⼯⼚模式可以分为:
6.2.2.1简单工厂模式
• 简单⼯⼚模式: 简单⼯⼚模式实现由⼀个⼯⼚对象通过类型决定创建出来指定产品类的实例。假设有个⼯⼚能⽣产出⽔果,当客⼾需要产品的时候明确告知⼯⼚⽣产哪类⽔果,⼯⼚需要接收⽤⼾提供的类别信息,当新增产品的时候,⼯⼚内部去添加新产品的⽣产⽅式。
//简单⼯⼚模式:通过参数控制可以⽣产任何产品
// 优点:简单粗暴,直观易懂。使⽤⼀个⼯⼚⽣产同⼀等级结构下的任意产品
// 缺点:
// 1. 所有东西⽣产在⼀起,产品太多会导致代码量庞⼤
// 2. 开闭原则遵循(开放拓展,关闭修改)的不是太好,要新增产品就必须修改⼯⼚⽅法。
#include <iostream>
#include <string>
#include <memory>
class Fruit
{
public:
Fruit(){}
virtual void show() = 0;
virtual ~Fruit() {} // 添加虚析构函数以支持多态删除
};
class Apple : public Fruit {
public:
Apple() {}
virtual void show() override { // 使用 override 关键字(可选,但有助于发现错误)
std::cout << "我是一个苹果" << std::endl;
}
};
class Banana : public Fruit {
public:
Banana() {}
virtual void show() override { // 使用 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>();
} else if (name == "香蕉") { // 修正了这里的字符
return std::make_shared<Banana>();
}
return std::shared_ptr<Fruit>();
}
};
int main()
{
std::shared_ptr<Fruit> fruit = FruitFactory::create("苹果");
fruit->show();
fruit = FruitFactory::create("香蕉"); // 修正了这里的字符
fruit->show();
return 0;
}
这个模式的结构和管理产品对象的⽅式⼗分简单, 但是它的扩展性⾮常差,当我们需要新增产品的时候,就需要去修改⼯⼚类新增⼀个类型的产品创建逻辑,违背了开闭原则。
6.2.2.2工厂方法模式
⼯⼚⽅法模式: 在简单⼯⼚模式下新增多个⼯⼚,多个产品,每个产品对应⼀个⼯⼚。假设现在有A、B 两种产品,则开两个⼯⼚,⼯⼚ A 负责⽣产产品 A,⼯⼚ B 负责⽣产产品 B,⽤⼾只知道产品的⼯⼚名,⽽不知道具体的产品信息,⼯⼚不需要再接收客⼾的产品类别,⽽只负责⽣产产品。
#include <iostream>
#include <string>
#include <memory>
// 定义 Fruit 基类
class Fruit {
public:
Fruit() {}
virtual void show() = 0;
virtual ~Fruit() {} // 虚析构函数以支持多态删除
};
// 定义 Apple 类
class Apple : public Fruit {
public:
Apple() {}
virtual void show() override {
std::cout << "我是一个苹果" << std::endl;
}
private:
std::string _color;
};
// 定义 Banana 类
class Banana : public Fruit {
public:
Banana() {}
virtual void show() override {
std::cout << "我是一个香蕉" << std::endl;
}
};
// 定义 FruitFactory 抽象基类
class FruitFactory {
public:
virtual std::shared_ptr<Fruit> create() = 0;
virtual ~FruitFactory() {} // 虚析构函数
};
// 定义 AppleFactory 类
class AppleFactory : public FruitFactory {
public:
std::shared_ptr<Fruit> create() override {
return std::make_shared<Apple>();
}
};
// 定义 BananaFactory 类
class BananaFactory : public FruitFactory {
public:
std::shared_ptr<Fruit> create() override {
return std::make_shared<Banana>();
}
};
int main() {
std::shared_ptr<Fruit> fruit; // 声明 fruit 变量
std::shared_ptr<FruitFactory> factory; // 声明 factory 变量
factory = std::make_shared<AppleFactory>(); // 创建 AppleFactory 对象
fruit = factory->create(); // 通过 AppleFactory 创建 Apple 对象
fruit->show(); // 显示 "我是一个苹果"
factory = std::make_shared<BananaFactory>(); // 创建 BananaFactory 对象
fruit = factory->create(); // 通过 BananaFactory 创建 Banana 对象
fruit->show(); // 显示 "我是一个香蕉"
return 0;
}
⼯⼚⽅法模式每次增加⼀个产品时,都需要增加⼀个具体产品类和⼯⼚类,这会使得系统中类的个数成倍增加,在⼀定程度上增加了系统的耦合度。
6.2.2.3抽象工厂模式
抽象⼯⼚模式: ⼯⼚⽅法模式通过引⼊⼯⼚等级结构,解决了简单⼯⼚模式中⼯⼚类职责太重的问题,但由于⼯⼚⽅法模式中的每个⼯⼚只⽣产⼀类产品,可能会导致系统中存在⼤量的⼯⼚类,势必会增加系统的开销。此时,我们可以考虑将⼀些相关的产品组成⼀个产品族(位于不同产品等级结构中功能相关联的产品组成的家族),由同⼀个⼯⼚来统⼀⽣产,这就是抽象⼯⼚模式的基本思想。
#include <iostream>
#include <string>
#include <memory>
// Fruit 基类及其派生类
class Fruit {
public:
Fruit() {}
virtual void show() = 0;
virtual ~Fruit() {}
};
class Apple : public Fruit {
public:
Apple() {}
void show() override {
std::cout << "我是一个苹果" << std::endl;
}
private:
std::string _color; // 这里_color未使用,但保留作为示例
};
class Banana : public Fruit {
public:
Banana() {}
void show() override {
std::cout << "我是一个香蕉" << std::endl;
}
};
// Animal 基类及其派生类
class Animal {
public:
virtual void voice() = 0;
virtual ~Animal() {}
};
class Sheep : public Animal { // 替换Lamp为Sheep
public:
void voice() override {
std::cout << "咩咩咩\n";
}
};
class Dog : public Animal {
public:
void voice() override {
std::cout << "汪汪汪\n";
}
};
// Factory 抽象基类及其派生类
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;
virtual ~Factory() {}
};
class FruitFactory : public Factory {
public:
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>();
}
std::shared_ptr<Animal> getAnimal(const std::string &name) override {
return std::shared_ptr<Animal>(); // 默认不返回Animal对象
}
};
class AnimalFactory : public Factory {
public:
std::shared_ptr<Animal> getAnimal(const std::string &name) override {
if (name == "小羊") { // 修正字符串
return std::make_shared<Sheep>();
} else if (name == "小狗") {
return std::make_shared<Dog>();
}
return std::shared_ptr<Animal>();
}
std::shared_ptr<Fruit> getFruit(const std::string &name) override {
return std::shared_ptr<Fruit>(); // 默认不返回Fruit对象
}
};
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> fruit_factory = FactoryProducer::getFactory("水果"); // 修正为"水果"
std::shared_ptr<Fruit> fruit = fruit_factory->getFruit("苹果");
fruit->show();
fruit = fruit_factory->getFruit("香蕉"); // 修正为"香蕉"
fruit->show();
// 示例使用AnimalFactory
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;
}
抽象⼯⼚模式适⽤于⽣产多个⼯⼚系列产品衍⽣的设计模式,增加新的产品等级结构复杂,需要对原有系统进⾏较⼤的修改,甚⾄需要修改抽象层代码,违背了"开闭原则"。
6.2.3建造者模式
建造者模式是⼀种创建型设计模式, 使⽤多个简单的对象⼀步⼀步构建成⼀个复杂的对象,能够将⼀个复杂的对象的构建与它的表⽰分离,提供⼀种创建对象的最佳⽅式。主要⽤于解决对象的构建过于复杂的问题。
建造者模式主要基于四个核⼼类实现:
• 抽象产品类:
• 具体产品类:⼀个具体的产品对象类
• 抽象Builder类:创建⼀个产品对象所需的各个部件的抽象接⼝
• 具体产品的Builder类:实现抽象接⼝,构建各个部件
• 指挥者Director类:统⼀组建过程,提供给调⽤者使⽤,通过指挥者来构造产品
#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 toString() {
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() {}
void setOs() override {
_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;
virtual ~Builder() {}
};
/* 具体建造者类 */
class MacBookBuilder : public Builder {
public:
using ptr = std::shared_ptr<MacBookBuilder>;
MacBookBuilder() : _computer(std::make_shared<MacBook>()) {}
void buildBoard(const std::string &board) override {
_computer->setBoard(board);
}
void buildDisplay(const std::string &display) override {
_computer->setDisplay(display);
}
void buildOs() override {
_computer->setOs();
}
Computer::ptr build() override {
return _computer;
}
private:
Computer::ptr _computer;
};
/* 指挥者类 */
class Director {
public:
Director(Builder::ptr 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() {
auto builder = std::make_shared<MacBookBuilder>();
std::unique_ptr<Director> pd(new Director(builder));
pd->construct("英特尔主板", "VOC显示器");
Computer::ptr computer = builder->build();
std::cout << computer->toString();
return 0;
}
6.2.4代理模式
代理模式指代理控制对其他对象的访问, 也就是代理对象控制对原对象的引⽤。在某些情况下,⼀个对象不适合或者不能直接被引⽤访问,⽽代理对象可以在客⼾端和⽬标对象之间起到中介的作⽤。
代理模式的结构包括⼀个是真正的你要访问的对象(⽬标类)、⼀个是代理对象。⽬标对象与代理对象实现同⼀个接⼝,先访问代理类再通过代理类访问⽬标对象。代理模式分为静态代理、动态代理 :
• 静态代理 指的是,在编译时就已经确定好了代理类和被代理类的关系。也就是说,在编译时就已经确定了代理类要代理的是哪个被代理类。
• 动态代理 指的是,在运⾏时才动态⽣成代理类,并将其与被代理类绑定。这意味着,在运⾏时才能确定代理类要代理的是哪个被代理类。
以租房为例,房东将房⼦租出去,但是要租房⼦出去,需要发布招租启⽰, 带⼈看房,负责维修,这些⼯作中有些操作并⾮房东能完成,因此房东为了图省事,将房⼦委托给中介进⾏租赁。 代理模式实现: 、
/*房东要把⼀个房⼦通过中介租出去理解代理模式*/
#include <iostream>
#include <string>
class RentHouse
{
public:
virtual void rentHouse() = 0;
};
/*房东类:将房⼦租出去*/
class Landlord : public RentHouse
{
public:
void rentHouse() {
std::cout << "将房⼦租出去\n";
}
};
/*中介代理类:对租房⼦进⾏功能加强,实现租房以外的其他功能*/
class Intermediary : public RentHouse
{
public:
void rentHouse()
{
std::cout << "发布招租启⽰\n";
std::cout << "带⼈看房\n";
_landlord.rentHouse();
std::cout << "负责租后维修\n";
}
private:
Landlord _landlord;
};
int main()
{
Intermediary intermediary;
intermediary.rentHouse();
return 0;
}
7.⽇志系统框架设计
本项⽬实现的是⼀个多⽇志器⽇志系统,主要实现的功能是让程序员能够轻松的将程序运⾏⽇志信息落地到指定的位置,且⽀持同步与异步两种⽅式的⽇志落地⽅式。
项⽬的框架设计将项⽬分为以下⼏个模块来实现。
7.1模块划分
-
⽇志等级模块:对输出⽇志的等级进⾏划分,以便于控制⽇志的输出,并提供等级枚举转字符串功能。
◦ OFF:关闭
◦ DEBUG:调试,调试时的关键信息输出。
◦ INFO:提⽰,普通的提⽰型⽇志信息。
◦ WARN:警告,不影响运⾏,但是需要注意⼀下的⽇志。
◦ ERROR:错误,程序运⾏出现错误的⽇志
◦ FATAL:致命,⼀般是代码异常导致程序⽆法继续推进运⾏的⽇志
-
⽇志消息模块:中间存储⽇志输出所需的各项要素信息
◦ 时间:描述本条⽇志的输出时间。
◦ 线程ID:描述本条⽇志是哪个线程输出的。
◦ ⽇志等级:描述本条⽇志的等级。
◦ ⽇志数据:本条⽇志的有效载荷数据。
◦ ⽇志⽂件名:描述本条⽇志在哪个源码⽂件中输出的。
◦ ⽇志⾏号:描述本条⽇志在源码⽂件的哪⼀⾏输出的。
-
⽇志消息格式化模块:设置⽇志输出格式,并提供对⽇志消息进⾏格式化功能。
◦ 系统的默认⽇志输出格式:[%d{%H:%M:%S}][%t][%p][%c][%f:%l]%m%n
◦ %d{%H:%M:%S}:表⽰⽇期时间,花括号中的内容表⽰⽇期时间的格式。
◦ %T:表⽰制表符缩进。
◦ %t:表⽰线程ID
◦ %p:表⽰⽇志级别
◦ %c:表⽰⽇志器名称,不同的开发组可以创建⾃⼰的⽇志器进⾏⽇志输出,⼩组之间互不影
响。
◦ %f:表⽰⽇志输出时的源代码⽂件名。
◦ %l:表⽰⽇志输出时的源代码⾏号。
◦ %m:表⽰给与的⽇志有效载荷数据
◦ %n:表⽰换⾏
◦ 设计思想:设计不同的⼦类,不同的⼦类从⽇志消息中取出不同的数据进⾏处理。
-
⽇志消息落地模块:决定了⽇志的落地⽅向,可以是标准输出,也可以是⽇志⽂件,也可以滚动⽂件输出...
◦ 标准输出:表⽰将⽇志进⾏标准输出的打印。
◦ ⽇志⽂件输出:表⽰将⽇志写⼊指定的⽂件末尾。
◦ 滚动⽂件输出:当前以⽂件⼤⼩进⾏控制,当⼀个⽇志⽂件⼤⼩达到指定⼤⼩,则切换下⼀个⽂件进⾏输出
◦ 后期,也可以扩展远程⽇志输出,创建客⼾端,将⽇志消息发送给远程的⽇志分析服务器。
◦ 设计思想:设计不同的⼦类,不同的⼦类控制不同的⽇志落地⽅向。
-
⽇志器模块:
◦ 此模块是对以上⼏个模块的整合模块,⽤⼾通过⽇志器进⾏⽇志的输出,有效降低⽤⼾的使⽤难度。
◦ 包含有:⽇志消息落地模块对象,⽇志消息格式化模块对象,⽇志输出等级
-
⽇志器管理模块:
◦ 为了降低项⽬开发的⽇志耦合,不同的项⽬组可以有⾃⼰的⽇志器来控制输出格式以及落地⽅向,因此本项⽬是⼀个多⽇志器的⽇志系统。
◦ 管理模块就是对创建的所有⽇志器进⾏统⼀管理。并提供⼀个默认⽇志器提供标准输出的⽇志输出。
-
异步线程模块:
◦ 实现对⽇志的异步输出功能,⽤⼾只需要将输出⽇志任务放⼊任务池,异步线程负责⽇志的落地输出功能,以此提供更加⾼效的⾮阻塞⽇志输出。
7.2模块关系图
8.代码设计
8.1 实⽤类设计
提前完成⼀些零碎的功能接⼝,以便于项⽬中会⽤到。
• 获取系统时间
• 判断⽂件是否存在
• 获取⽂件的所在⽬录路径
• 创建⽬录
cpp
#ifndef __M_UTIL_H__
#define __M_UTIL_H__
/*
通⽤功能类,与业务⽆关的功能实现
1. 获取系统时间
2. 获取⽂件⼤⼩
3. 创建⽬录
4. 获取⽂件所在⽬录
*/
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <ctime>
#include <cassert>
#include <sys/stat.h>
namespace dhylog
{
namespace util
{
class date
{
public:
static size_t now()
{
return size_t(time(nullptr));
}
};
class File
{
public:
static bool exists(const std::string& name)
{
struct stat st;
return stat(name.c_str(),&st)==0;
}
static std::string path(const std::string& name)
{
if(name.empty()) return ".";
size_t pos=name.find_last_of("/\\");
if(pos==std::string::npos)
{
return ".";
}
return name.substr(0,pos+1);
}
static void create_directory(const std::string &path)
{
if(path.empty()) return;
if(exists(path)) return;
size_t pos=0,idx=0;
while(idx<path.size())
{
pos=path.find_first_of("/\\",idx);
if(pos==std::string::npos)
{
mkdir(path.c_str(), 0777);
return;
}
std::string parent_dir=path.substr(0,pos+1);
if(exists(parent_dir)) {idx=pos+1; continue;}
mkdir(parent_dir.c_str(),0777);
idx=pos+1;
}
}
};
};
};
#endif
8.2⽇志等级类设计
⽇志等级总共分为7个等级,分别为:
• OFF 关闭所有⽇志输出
• DRBUG 进⾏debug时候打印⽇志的等级
• INFO 打印⼀些⽤⼾提⽰信息
• WARN 打印警告信息
• ERROR 打印错误信息
• FATAL 打印致命信息- 导致程序崩溃的信息
cpp
#ifndef __M_LEVEL_H__
#define __M_LEVEL_H__
namespace dhylog
{
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 "UNKNOW";
}
};
}
#endif
8.3 ⽇志消息类设计
⽇志消息类主要是封装⼀条完整的⽇志消息所需的内容,其中包括⽇志等级、对应的logger name、打印⽇志源⽂件的位置信息(包括⽂件名和⾏号)、线程ID、时间戳信息、具体的⽇志信息等内容。
cpp
#ifndef __M_MESSAGE_H__
#define __M_MESSAGE_H__
#include<iostream>
#include"level.hpp"
#include"util.hpp"
#include<thread>
namespace dhylog
{
struct LogMsg
{
time_t _ctime;//日志产生的时间戳
loglevel::value _level;//日志等级
size_t _line;//行号
std::thread::id _tid;//线程id
std::string _file;//源码文件名
std::string _logger;//日志器名称
std::string _payload;//有效消息数据
LogMsg(loglevel::value level,
size_t line,
std::string file,
std::string logger,
std::string msg):
_ctime(util::date::now()),
_line(line),
_level(level),
_tid(std::this_thread::get_id()),
_file(file),
_logger(logger),
_payload(msg) {}
};
}
#endif
8.4 ⽇志输出格式化类设计
⽇志格式化(Formatter)类主要负责格式化⽇志消息。其主要包含以下内容
• pattern成员:保存⽇志输出的格式字符串。
◦ %d ⽇期
◦ %T 缩进
◦ %t 线程id
◦ %p ⽇志级别
◦ %c ⽇志器名称
◦ %f ⽂件名
◦ %l ⾏号
◦ %m ⽇志消息
◦ %n 换⾏
cpp
#ifndef __M_FMT_H__
#define __M_FMT_H__
#include "util.hpp"
#include "message.hpp"
#include "level.hpp"
#include <memory>
#include <vector>
#include <tuple>
#include <ctime>
namespace dhylog
{
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[128];
strftime(tmp,127,_time_fmt.c_str(),&t);
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 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;
};
class Formatter
{
public:
using ptr=std::shared_ptr<Formatter>;
/*
%d 日期
%T 缩进
%t 线程id
%p 日志级别
%c 日志器名称
%f 文件名
%l 行号
%m 日志消息
%n 换行
*/
//时间{年-月-日 时:分:秒}缩进 线程ID 缩进 [日志级别] 缩进 [日志名称] 缩进 文件名:行号 缩进 消息换行 [%d{%H:%M:%S}][%t][%p][%c][%f:%l]%m%n
Formatter(const std::string &pattern = "[%d{%H:%M:%S}][%t][%p][%c][%f:%l]%m%n"):_pattern(pattern)
{
assert(parsePattern());
}
const std::string pattern() { return _pattern; }
std::string format(const LogMsg &msg)
{
std::stringstream ss;
for(auto &it :_items)
{
it->format(ss,msg);
}
return ss.str();
}
std::ostream& format(std::ostream &out,const LogMsg&msg)
{
for(auto &it :_items)
{
it->format(out,msg);
}
return out;
}
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<TabFormatItem>();
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<ThreadFormatItem>();
if(key=="m") return std::make_shared<MsgFormatItem>();
if(key=="n") return std::make_shared<NlineFormatItem>();
if(key.empty()) return std::make_shared<OtherFormatItem>(val);
std::cout<<"没有对应的格式化字符%"<<key<<std::endl;
abort();
}
private:
bool parsePattern()
{
//[%d{%H:%M:%S}][%t][%p][%c][%f:%l] %m%n
//sg{}fsg%d{%H:%M:%S}%Tsdf%t%T[%p]%T[%c]%T%f:%l%T%m%n
std::vector<std::pair<std::string,std::string>> fmt_order;
size_t pos=0;
std::string key,value;
while(pos<_pattern.size())
{
//寻找%
if(_pattern[pos]!='%')
{
value.push_back(_pattern[pos++]);
continue;
}
//当前位置为%,判断后面是否为%
if(pos+1<_pattern.size()&&_pattern[pos+1]=='%'){value.push_back('%');pos+=2;continue;};
//当前位置为%,且pos+1不为%,剔除前两下项为%d的情况
if(!value.empty())
{
fmt_order.push_back(std::make_pair("",value));
value.clear();
}
//当前位置为%,后面为格式化字符,接下来处理格式化字符
pos+=1;//当前位置为格式化字符,判断越界
if(pos==_pattern.size())
{
std::cout<<"%之后,没有对相应的格式化字符!\n";
return false;
}
key=_pattern[pos];
pos+=1;
//格式化字符完整,当前位置为格式化字符后一位,判断是否为{
if(pos<_pattern.size()&&_pattern[pos]=='{')
{
pos++;
while(pos<_pattern.size()&&_pattern[pos]!='}')
{
value.push_back(_pattern[pos++]);
}
if(pos==_pattern.size())
{
std::cout<<"{不匹配";
return false;
}
pos+=1;
}
fmt_order.push_back(std::make_pair(key,value));
key.clear();
value.clear();
}
for(auto &it:fmt_order)
{
_items.push_back(createItem(it.first,it.second));
}
return true;
}
std::string _pattern;
std::vector<FormatItem::ptr> _items;
};
}
#endif
8.5 ⽇志落地(LogSink)类设计(简单⼯⼚模式)
⽇志落地类主要负责落地⽇志消息到⽬的地。
它主要包括以下内容:
• Formatter⽇志格式化器:主要是负责格式化⽇志消息,
• mutex互斥锁:保证多线程⽇志落地过程中的线程安全,避免出现交叉输出的情况。
这个类⽀持可扩展,其成员函数log设置为纯虚函数,当我们需要增加⼀个log输出⽬标, 可以增加⼀
个类继承⾃该类并重写log⽅法实现具体的落地⽇志逻辑。
⽬前实现了三个不同⽅向上的⽇志落地:
• 标准输出:StdoutSink
• 固定⽂件:FileSink
• 滚动⽂件:RollSink
◦ 滚动⽇志⽂件输出的必要性:
▪ 由于机器磁盘空间有限, 我们不可能⼀直⽆限地向⼀个⽂件中增加数据 ▪ 如果⼀个⽇志⽂件体积太⼤,⼀⽅⾯是不好打开,另⼀⽅⾯是即时打开了由于包含数据巨⼤,也不利于查找我们需要的信息
▪ 所以实际开发中会对单个⽇志⽂件的⼤⼩也会做⼀些控制,即当⼤⼩超过某个⼤⼩时(如1GB),我们就重新创建⼀个新的⽇志⽂件来滚动写⽇志。 对于那些过期的⽇志, ⼤部分企业内部都有专⻔的运维⼈员去定时清理过期的⽇志,或者设置系统定时任务,定时清理过期⽇志。
◦ ⽇志⽂件的滚动思想:
⽇志⽂件滚动的条件有两个:⽂件⼤⼩ 和 时间。我们可以选择:
▪ ⽇志⽂件在⼤于 1GB 的时候会更换新的⽂件
▪ 每天定点滚动⼀个⽇志⽂件
本项⽬基于⽂件⼤⼩的判断滚动⽣成新的⽂件
cpp
#ifndef __M_SINK_H__
#define __M_SINK_H__
#include "util.hpp"
#include "message.hpp"
#include "formatter.hpp"
#include <memory>
#include <mutex>
namespace dhylog
{
class LogSink
{
public:
using ptr = std::shared_ptr<LogSink>;
LogSink() {}
virtual void log(const char *data, size_t len) = 0;
};
class StdoutSink : public LogSink
{
public:
using ptr = std::shared_ptr<StdoutSink>;
void log(const char *data, size_t len)override
{
std::cout.write(data,len);
std::cout<<std::endl;
}
};
class FileSink : public LogSink
{
public:
using ptr = std::shared_ptr<FileSink>;
FileSink(const std::string &filename):_Filename(filename)
{
util::File::create_directory(util::File::path(filename));
_ofs.open(filename, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
void log(const char *data, size_t len)override
{
_ofs.write(data,len);
if(_ofs.good()==false)
{
std::cout<<"日志文件输出失败"<<std::endl;
}
}
private:
std::string _Filename;
std::ofstream _ofs;
};
class RollSink : public LogSink
{
public:
using ptr = std::shared_ptr<RollSink>;
RollSink(std::string basename,size_t max_fsize):_basename(basename),_max_fsize(max_fsize),_cur_fsize(0)
{
util::File::create_directory(util::File::path(basename));
}
void log(const char *data, size_t len)override
{
initLogFile();
_ofs.write(data,len);
_ofs<<'\n';
if (_ofs.good() == false)
{
std::cout << "日志输出文件失败!\n";
}
_cur_fsize += len;
}
private:
void initLogFile()
{
if(_ofs.is_open()==false||_cur_fsize>=_max_fsize)
{
_ofs.close();
std::string name = createFilename();
_ofs.open(name, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
_cur_fsize = 0;
return;
}
return;
}
std::string createFilename()
{
time_t t=util::date::now();
struct tm lt;
localtime_r(&t, <);
std::stringstream ss;
ss << _basename;
ss << lt.tm_year + 1900;
ss << "-";
ss << lt.tm_mon + 1;
ss << "-";
ss << lt.tm_mday;
ss << "-";
ss << lt.tm_hour;
ss << "-";
ss << lt.tm_min;
ss << "-";
ss << lt.tm_sec;
ss << ".log";
return ss.str();
}
std::string _basename;
std::ofstream _ofs;
size_t _max_fsize;
size_t _cur_fsize;
};
class SinkFactory
{
public:
template<typename SinkType, typename ...Args>
static LogSink::ptr create(Args &&...args) {
return std::make_shared<SinkType>(std::forward<Args>(args)...);
}
};
}
#endif
8.6 ⽇志器类(Logger)设计(建造者模式)
⽇志器主要是⽤来和前端交互, 当我们需要使⽤⽇志系统打印log的时候, 只需要创建Logger对象,调⽤该对象debug、info、warn、error、fatal等⽅法输出⾃⼰想打印的⽇志即可,⽀持解析可变参数列表和输出格式, 即可以做到像使⽤printf函数⼀样打印⽇志。
当前⽇志系统⽀持同步⽇志 & 异步⽇志两种模式,两个不同的⽇志器唯⼀不同的地⽅在于他们在⽇志的落地⽅式上有所不同:
同步⽇志器:直接对⽇志消息进⾏输出。
异步⽇志器:将⽇志消息放⼊缓冲区,由异步线程进⾏输出。
因此⽇志器类在设计的时候先设计出⼀个Logger基类,在Logger基类的基础上,继承出SyncLogger同步⽇志器和AsyncLogger异步⽇志器。
且因为⽇志器模块是对前边多个模块的整合,想要创建⼀个⽇志器,需要设置⽇志器名称,设置⽇志输出等级,设置⽇志器类型,设置⽇志输出格式,设置落地⽅向,且落地⽅向有可能存在多个,整个⽇志器的创建过程较为复杂,为了保持良好的代码⻛格,编写出优雅的代码,因此⽇志器的创建这⾥采⽤了建造者模式来进⾏创建。
cpp
#ifndef _M_LOGGER_H_
#define _M_LOGGER_H_
//#define _GNU_SOURCE
#include"util.hpp"
#include"level.hpp"
#include"formatter.hpp"
#include"sink.hpp"
#include"looper.hpp"
#include<atomic>
#include<cstdarg>
#include<unordered_map>
namespace dhylog {
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;}
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;}
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;}
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;}
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;}
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 msg(level,line,file,_logger_name,str);
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,
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:
//using ptr = std::shared_ptr<AsyncLogger>;
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),
_looper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::reallog,this,std::placeholders::_1),looper_type))
{}
protected:
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;
};
enum class LoggerType
{
LOGGER_SYNC = 0,
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;
loglevel::value _limit_level;
Formatter::ptr _formatter;
std::vector<LogSink::ptr> _sinks;
};
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)
{
//std::cout<<(_looper_Type==AsyncType::ASYNC_UNSAFE);
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()
{
static LoggerManager eton;
return eton;
}
void addLogger(Logger::ptr &Logger)
{
if(hasLogger(Logger->name())) return ;
std::unique_lock<std::mutex> lock(_mutex);
_loggers.insert(make_pair(Logger->name(),Logger));
}
bool hasLogger(std::string name)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it=_loggers.find(name);
if(it==_loggers.end()) return false;
else 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();
else return it->second;
}
Logger::ptr rootLogger()
{
return _root_logger;
}
private:
LoggerManager()
{
std::unique_ptr<dhylog::LoggerBuilder> builder(new dhylog::LocalLoggerBuilder());
builder->buildLoggerName("root");
_root_logger=builder->build();
_loggers.insert(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
8.7双缓冲区异步任务处理器(AsyncLooper)设计
设计思想 :异步处理线程 + 数据池
使⽤者将需要完成的任务添加到任务池中,由异步线程来完成任务的实际执⾏操作。
任务池的设计思想 :双缓冲区阻塞数据池
优势:避免了空间的频繁申请释放,且尽可能的减少了⽣产者与消费者之间锁冲突的概率,提⾼了任务处理效率。
在任务池的设计中,有很多备选⽅案,⽐如循环队列等等,但是不管是哪⼀种都会涉及到锁冲突的情况,因为在⽣产者与消费者模型中,任何两个⻆⾊之间都具有互斥关系,因此每⼀次的任务添加与取出都有可能涉及锁的冲突,⽽双缓冲区不同,双缓冲区是处理器将⼀个缓冲区中的任务全部处理完毕后,然后交换两个缓冲区,重新对新的缓冲区中的任务进⾏处理,虽然同时多线程写⼊也会冲突,但是冲突并不会像每次只处理⼀条的时候频繁(减少了⽣产者与消费者之间的锁冲突),且不涉及到空间的频繁申请释放所带来的消耗。
cpp
#ifndef M_BUF_H
#define M_BUF_H
#include"util.hpp"
#include<vector>
namespace dhylog{
#define DEFAULT_BUFFER_SIZE (100*1024*1024)
#define THRESHOLD_BUFFER_SIZE (80*1024*1024)
#define INCREMENNT_BUFFER_SIZE (10*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.扩容
//1.固定大小,则直接返回
//if(len>writeAbleSize()) return;
//2.动态控件,用于极限性能测试--扩容
ensureEnoughSize(len);
//1.将数据拷贝到缓冲区
std::copy(data,data+len,&_buffer[_writer_idx]);
//2.将当前写入位置向后偏移
moveWriter(len);
}
size_t writeAbleSize()
{
return (_buffer.size()-_writer_idx);
}
//返回可读数据得起始地址
const char*begin()
{
return &_buffer[_reader_idx];
}
//返回可读数据的长度
size_t readAbleSize()
{
return (_writer_idx-_reader_idx);
}
void moveReader(size_t len)
{
assert(len<=readAbleSize());
_reader_idx+=len;
}
//重置读写位置,初始化缓冲区
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;
}
else
{
new_size=_buffer.size()+INCREMENNT_BUFFER_SIZE;
}
_buffer.resize(new_size);
}
//对读写指针进行向后偏移操作
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
8.8 异步⽇志器(AsyncLogger)设计
异步⽇志器类继承⾃⽇志器类, 并在同步⽇志器类上拓展了异步消息处理器。当我们需要异步输出⽇志的时候, 需要创建异步⽇志器和消息处理器, 调⽤异步⽇志器的log、error、info、fatal等函数输出不同级别⽇志。
• log函数为重写Logger类的函数, 主要实现将⽇志数据加⼊异步队列缓冲区中
• realLog函数主要由异步线程进⾏调⽤(是为异步消息处理器设置的回调函数),完成⽇志的实际落地⼯作。
cpp
//异步日志器
class AsyncLogger : public Logger
{
public:
//using ptr = std::shared_ptr<AsyncLogger>;
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),
_looper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::reallog,this,std::placeholders::_1),looper_type))
{}
protected:
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;
};
8.9 单例⽇志器管理类设计(单例模式)
⽇志的输出,我们希望能够在任意位置都可以进⾏,但是当我们创建了⼀个⽇志器之后,就会受到⽇志器所在作⽤域的访问属性限制。
因此,为了突破访问区域的限制,我们创建⼀个⽇志器管理类,且这个类是⼀个单例类,这样的话,我们就可以在任意位置来通过管理器单例获取到指定的⽇志器来进⾏⽇志输出了。
基于单例⽇志器管理器的设计思想,我们对于⽇志器建造者类进⾏继承,继承出⼀个全局⽇志器建造者类,实现⼀个⽇志器在创建完毕后,直接将其添加到单例的⽇志器管理器中,以便于能够在任何位置通过⽇志器名称能够获取到指定的⽇志器进⾏⽇志输出。
cpp
class LoggerManager
{
public:
static LoggerManager &getInstance()
{
static LoggerManager eton;
return eton;
}
void addLogger(Logger::ptr &Logger)
{
if(hasLogger(Logger->name())) return ;
std::unique_lock<std::mutex> lock(_mutex);
_loggers.insert(make_pair(Logger->name(),Logger));
}
bool hasLogger(std::string name)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it=_loggers.find(name);
if(it==_loggers.end()) return false;
else 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();
else return it->second;
}
Logger::ptr rootLogger()
{
return _root_logger;
}
private:
LoggerManager()
{
std::unique_ptr<dhylog::LoggerBuilder> builder(new dhylog::LocalLoggerBuilder());
builder->buildLoggerName("root");
_root_logger=builder->build();
_loggers.insert(make_pair("root",_root_logger));
}
private:
std::mutex _mutex;
Logger::ptr _root_logger;//默认日志器
std::unordered_map<std::string,Logger::ptr> _loggers;
};
8.10 ⽇志宏&全局接⼝设计(代理模式)
提供全局的⽇志器获取接⼝。
使⽤代理模式通过全局函数或宏函数来代理Logger类的log、debug、info、warn、error、fatal等接⼝,以便于控制源码⽂件名称和⾏号的输出控制,简化⽤⼾操作。
当仅需标准输出⽇志的时候可以通过主⽇志器来打印⽇志。 且操作时只需要通过宏函数直接进⾏输出
即可。
cpp
#ifndef _MY_LOG_
#define _MY_LOG_
#include"logger.hpp"
namespace dhylog
{
//1.提供获取指定日志器的全局接口(避免用户自己操作单例对象)
Logger::ptr getLogger(const std::string &name)
{
return dhylog::LoggerManager::getInstance().getLogger(name);
}
Logger::ptr rootLogger()
{
return dhylog::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,...) dhylog::rootLogger()->debug(fmt, ##__VA_ARGS__)
#define INFO(fmt,...) dhylog::rootLogger()->info(fmt, ##__VA_ARGS__)
#define WARN(fmt,...) dhylog::rootLogger()->warn(fmt, ##__VA_ARGS__)
#define ERROR(fmt,...) dhylog::rootLogger()->error(fmt, ##__VA_ARGS__)
#define FATAL(fmt,...) dhylog::rootLogger()->fatal(fmt, ##__VA_ARGS__)
}
#endif
9. 功能⽤例
cpp
#include "../logs/dhylog.hpp"
#include"unistd.h"
void test_log(const std::string &name)
{
INFO("%s", "测试开始");
dhylog::Logger::ptr logger=dhylog::LoggerManager::getInstance().getLogger(name);
logger->debug( "%s", "测试日志");
logger->info( "%s", "测试日志");
logger->error("%s", "测试日志");
logger->warn( "%s", "测试日志");
logger->fatal("%s", "测试日志");
INFO( "%s", "测试结束");
}
int main()
{
std::unique_ptr<dhylog::LoggerBuilder> builder(new dhylog::GlobalLoggerBuilder());
builder->buildLoggerName("async_logger");
builder->buildLoggerLevel(dhylog::loglevel::value::WARN);
builder->buildFormatter("[%c][%f:%l]%m%n");
builder->buildLoggerType(dhylog::LoggerType::LOGGER_ASYNC);
//builder->buildEnableUnSafeAsync();
builder->buildSink<dhylog::FileSink>("./logfile/async.log");
builder->buildSink<dhylog::StdoutSink>();
builder->buildSink<dhylog::RollSink>("./logfile/roll-async-by-size",1024*1024);
builder->build();
test_log("async_logger");
return 0;
}
10.性能测试
下⾯对⽇志系统做⼀个性能测试,测试⼀下平均每秒能打印多少条⽇志消息到⽂件。
主要的测试⽅法是:每秒能打印⽇志数 = 打印⽇志条数 / 总的打印⽇志消耗时间
主要测试要素:同步/异步 & 单线程/多线程
• 100w+条指定⻓度的⽇志输出所耗时间
• 每秒可以输出多少条⽇志
• 每秒可以输出多少MB⽇志
测试环境
cpp
#include "../logs/dhylog.hpp"
#include<vector>
#include<thread>
#include<chrono>
void bench(const std::string &logger_name,size_t thr_count,size_t msg_count,size_t msg_len)
{
dhylog::Logger::ptr logger=dhylog::getLogger(logger_name);
if(logger.get()==nullptr)
{
return;
}
std::cout<<"测试日志:"<<msg_count<<"条,总大小:"<<(msg_count*msg_len)/1024<<"kb\n";
std::string msg(msg_len-1,'A');
std::vector<std::thread> threads;
std::vector<double> cost_arry(thr_count);
size_t msg_per_thr=msg_count/thr_count;
for(int i=0;i<thr_count;i++)
{
threads.emplace_back([&,i]()
{
auto start=std::chrono::high_resolution_clock::now();
for(int j=0;j<msg_per_thr;j++)
{
logger->fatal("%s",msg.c_str());
}
auto end=std::chrono::high_resolution_clock::now();
std::chrono::duration<double> cost=end-start;
cost_arry[i]=cost.count();
std::cout<<"线程"<<i<<":"<<"\t输出数量"<<msg_per_thr<<",耗时:"<<cost.count()<<"s"<<std::endl;
});
}
for(int i=0;i<thr_count;i++)
{
threads[i].join();
}
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);
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<dhylog::LoggerBuilder> builder(new dhylog::GlobalLoggerBuilder());
builder->buildLoggerName("sync_logger");
builder->buildFormatter("%m%n");
builder->buildLoggerType(dhylog::LoggerType::LOGGER_SYNC);
builder->buildSink<dhylog::FileSink>("./logfile/sync.log");
builder->build();
bench("sync_logger",1,1000000,100);
}
void async_bench()
{
std::unique_ptr<dhylog::LoggerBuilder> builder(new dhylog::GlobalLoggerBuilder());
builder->buildLoggerName("async_logger");
builder->buildFormatter("%m%n");
builder->buildLoggerType(dhylog::LoggerType::LOGGER_ASYNC);
builder->buildEnableUnSafeAsync();
builder->buildSink<dhylog::FileSink>("./logfile/async.log");
builder->build();
bench("async_logger",3,1000000,100);
}
int main()
{
sync_bench();
//async_bench();
return 0;
}
11.参考文献
参考资料
https://www.imangodoc.com/174918.html
https://blog.csdn.net/w1014074794/article/details/125074038
https://zhuanlan.zhihu.com/p/472569975
https://zhuanlan.zhihu.com/p/460476053
https://gitee.com/davidditao/DDlog
https://www.cnblogs.com/ailumiyana/p/9519614.html
https://gitee.com/lqk1949/plog/