本项目主要实现一个日志系统, 其主要支持以下功能:
本项目不依赖其他任何第三方库, 只需要安装好CentOS/Ubuntu + vscode/vim环境即可开发。
生产环境的产品为了保证其稳定性及安全性是不允许开发人员附加调试器去排查问题, 可以借助日志系统来打印一些日志帮助开发人员解决问题
上线客户端的产品出现bug无法复现并解决, 可以借助日志系统打印日志并上传到服务端帮助开发人员进行分析
对于一些高频操作(如定时器、心跳包)在少量调试次数下可能无法触发我们想要的行为,通过断点的暂停方式,我们不得不重复操作几十次、上百次甚至更多,导致排查问题效率是非常低下, 可以借助打印日志的方式查问题
在分布式、多线程/多进程代码中, 出现bug比较难以定位, 可以借助日志系统打印log帮助定位bug
帮助首次接触项目代码的新开发人员理解代码的运行流程
日志系统的技术实现主要包括三种类型:
利用printf、std::cout等输出函数将日志信息打印到控制台
对于大型商业化项目, 为了方便排查问题,我们一般会将日志输出到文件或者是数据库系统方便查询和分析日志, 主要分为同步日志和异步日志方式
同步日志是指当输出日志时,必须等待日志输出语句执行完毕后,才能执行后面的业务逻辑语句,日志输出语句与程序的业务逻辑语句将在同一个线程运行。每次调用一次打印日志API就对应一次系统调用write写日志文件。
在高并发场景下,随着日志数量不断增加,同步日志系统容易产生系统瓶颈:
异步日志是指在进行日志输出时,日志输出语句与业务逻辑语句并不是在同一个线程中运行,而是有专门的线程用于进行日志输出操作。业务线程只需要将日志放到一个内存缓冲区中不用等待即可继续执行后续业务逻辑(作为日志的生产者),而日志的落地操作交给单独的日志线程去完成(作为日志的消费者), 这是一个典型的生产-消费模型。
这样做的好处是即使日志没有真的地完成输出也不会影响程序的主业务,可以提高程序的性能:
在初学C语言的时候,我们都用过printf函数进行打印。其中printf函数就是一个不定参函数,在函数内部可以根据格式化字符串中格式化字符分别获取不同的参数进行数据的格式化。
而这种不定参函数在实际的使用中也非常多见,在这里简单做一介绍:
#include
// __FILE__: 文件名 __LINE__: 行号
// ##: 一旦不定参数为空,取消前面的 ,
#define LOG(fmt,...) printf("[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
int main()
{
LOG("%s-%s","hello","比特");
LOG("比特");
}
#include
#include
#include
// C语言中不定参函数的使用, 不定参数据的访问
void PrintNum(int count,...)
{
va_list ap;
va_start(ap,count); // 获取指定参数的起始地址, 这里是让ap指向count参数之后的第一个参数的起始地址
for(int i=0;i<count;++i)
{
int num=va_arg(ap,int);
printf("param[%d]:%d\n",i,num);
}
va_end(ap); // 将ap指针置空
}
int main()
{
PrintNum(3,1,2,4);
PrintNum(4,1,2,3,88);
}
模拟实现printf
#include
#include
#include
void myprintf(const char*fmt,...) // 模拟实现printf
{
// int vasprintf(char **strp, const char *fmt, va_list ap);
va_list ap;
va_start(ap,fmt);
char*res;
int len=vasprintf(&res,fmt,ap); // 通过ap指针根据fmt组织好数据后放入res中
if(len!=-1)
{
printf(res);
free(res);
}
va_end(ap);
}
int main()
{
myprintf("%s-%d\n","比特", 666);
myprintf("%s-%f\n","比特", 6.0);
return 0;
}
// C++风格的不定参数使用
void xprintf()
{
std::cout<<std::endl;
}
template<class T, class ...Args>
void xprintf(const T&val, Args &&...args) // 万能引用 + 完美转发
{
std::cout<<val<<" ";
if((sizeof...(args)) > 0)
xprintf(std::forward<Args>(args)...);
else
xprintf();
}
int main()
{
xprintf();
xprintf("比特");
xprintf("比特", 666);
return 0;
}
设计模式是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。
单一职责原则(Single Responsibility Principle);
开闭原则(Open Closed Principle);
里氏替换原则(Liskov Substitution Principle);
依赖倒置原则(Dependence Inversion Principle)。
迪米特法则(Law of Demeter),又叫“最少知道法则”;
接口隔离原则(Interface Segregation Principle);
从整体上来理解六大设计原则,可以简要的概括为一句话,用抽象构建框架,用实现扩展细节,具体到每一条设计原则,则对应一条注意事项:
一个类只能创建一个对象,即单例模式,该设计模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式有两种实现模式:饿汉模式和懒汉模式
// 饿汉模式: 一上来就实例化对象
// 版本1:
class Singleton
{
public:
static Singleton& getInsance()
{
return _ins;
}
private:
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
Singleton()
{
std::cout<<"单例对象构造完成"<<std::endl;
}
~Singleton()
{}
static Singleton _ins;
};
Singleton Singleton::_ins;
使用饿汉模式时,无论是否使用单例对象都会创建实例对象,即程序启动就会创建一个唯一实例对象。此时如果单例对象过大,程序启动速度会变慢
懒汉模式: 第一次使用要使用单例对象的时候创建实例对象。如果单例对象构造特别耗时或者耗费济源(加载插件、加载网络资源等), 可以选择懒汉模式, 在第一次使用的时候才创建对象。
// 懒汉模式: 懒加载---延迟加载的思想---一个对象在用的时候再进行实例化
class Singleton
{
public:
static Singleton& getInstance()
{
static Singleton ins; // C++11, 静态变量是线程安全的
return ins;
}
private:
int _data;
public:
int getData()
{
return _data;
}
private:
Singleton()
{
std::cout<<"单例对象构造完成"<<std::endl;
}
~Singleton(){}
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
};
C++11之前,懒汉模式实现方法:双检查加锁
class Singleton
{
public:
static Singleton* getInstance()
{
if( _ins == nullptr) // 提高效率, 不需要每次获取单例都加锁解锁
{
_mtx.lock();
if(_ins==nullptr) // 保证线程安全和只new一次
{
_ins=new Singleton;
}
_mtx.unlock();
}
return _ins;
}
private:
Singleton()
{
std::cout<<"单例对象构造完成"<<std::endl;
}
~Singleton(){}
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
private:
static Singleton* _ins;
static std::mutex _mtx;
};
Singleton* Singleton::_ins=nullptr;
std::mutex Singleton::_mtx;
工厂模式是一种创建型设计模式, 它提供了一种创建对象的最佳方式。在工厂模式中,我们创建对象时不会对上层暴露创建逻辑,而是通过使用一个共同结构来指向新创建的对象,以此实现创建-使用的分离。
工厂模式可以分为: 简单工厂模式,工厂方法模式,抽象工厂模式
简单工厂模式: 简单工厂模式实现由一个工厂对象通过类型决定创建出来指定产品类的实例。假设有个工厂能生产出水果,当客户需要产品的时候明确告知工厂生产哪类水果,工厂需要接收用户提供的类别信息,当新增产品的时候,工厂内部去添加新产品的生产方式。
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 FruitFactory1
{
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>();
}
};
int main()
{
std::shared_ptr<Fruit> fruit=FruitFactory1::create("苹果");
fruit->name();
fruit=FruitFactory1::create("香蕉");
fruit->name();
}
简单工厂模式: 通过参数控制可以生产任何产品
优点: 简单粗暴,直观易懂。使用同一个工厂生产同一等级结构下的任意产品
缺点:
- 所有东西生产在一起, 产品太多会导致代码量庞大
- 开闭原则遵循(开放扩展, 关闭修改)的不是很好, 要新增产品就必须修改工厂方法
工厂方法模式: 在简单工厂模式下新增多个工厂,多个产品,每个产品对应一个工厂。假设现在有A、B 两种产品,则开两个工厂,工厂 A 负责生产产品 A,工厂 B 负责生产产品 B,用户只知道产品的工厂名,而不知道具体的产品信息,工厂不需要再接收客户的产品类别,而只负责生产产品。
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 FruitFactory2
{
public:
virtual std::shared_ptr<Fruit> create()=0;
};
class AppleFactory:public FruitFactory2
{
public:
virtual std::shared_ptr<Fruit> create() override
{
return std::make_shared<Apple>();
}
};
class BananaFactory:public FruitFactory2
{
public:
virtual std::shared_ptr<Fruit> create() override
{
return std::make_shared<Banana>();
}
};
int main()
{
std::shared_ptr<FruitFactory2> ff(new AppleFactory());
std::shared_ptr<Fruit> fruit=ff->create();
fruit->name();
ff.reset(new BananaFactory());
fruit=ff->create();
fruit->name();
return 0;
}
工厂方法: 定义一个创建对象的接口, 但是由子类来决定创建哪种对象, 使用多个工厂分别生产指定的固定产品
优点:
- 减轻了工厂类的负担, 将某类产品的生产交给指定的工厂来进行
- 开闭原则遵循较好, 添加新产品只需要新增产品的工厂即可, 不需要修改原先的工厂类
缺点: 对于某种可以形成一组产品族的情况处理较为复杂, 需要创建大量的工厂类
抽象工厂模式: 工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时,我们可以考虑将一些相关的产品组成一个产品族(位于不同产品等级结构中功能相关联的产品组成的家族),由同一个工厂来统一生产,这就是抽象工厂模式的基本思想。
class Animal
{
public:
Animal(){}
virtual void name()=0;
};
class Cat:public Animal
{
public:
virtual void name()
{
std::cout<<"我是一只小猫"<<std::endl;
}
};
class Dog:public Animal
{
public:
virtual void name()
{
std::cout<<"我是一只小狗"<<std::endl;
}
};
class Factory
{
public:
virtual std::shared_ptr<Fruit> getFruit(const std::string&name)=0;
virtual std::shared_ptr<Animal> getAnimal(const std::string&name)=0;
};
class FruitFactory:public Factory
{
public:
virtual std::shared_ptr<Animal> getAnimal(const std::string&name) override
{
return std::shared_ptr<Animal>();
}
virtual 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>();
}
};
class AnimalFactory:public Factory
{
public:
virtual std::shared_ptr<Fruit> getFruit(const std::string&name) override
{
return std::shared_ptr<Fruit>();
}
virtual 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>();
}
};
class FactoryProducer
{
public:
static std::shared_ptr<Factory> create(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> ff=FactoryProducer::create("水果");
std::shared_ptr<Fruit> fruit=ff->getFruit("苹果");
fruit->name();
fruit=ff->getFruit("香蕉");
fruit->name();
std::shared_ptr<Factory> fa=FactoryProducer::create("动物");
std::shared_ptr<Animal> animal=fa->getAnimal("小猫");
animal->name();
animal=fa->getAnimal("小狗");
animal->name();
}
抽象工厂:围绕一个超级工厂创建其他工厂。每个生成的工厂按照工厂模式提供对象。
思想:将工厂抽象成两层,抽象工厂 & 具体简单工厂子类, 在工厂子类种生产不同类型的子产品
抽象工厂模式适用于生产多个工厂系列产品衍生的设计模式,增加新的产品等级结构复杂,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,违背了“开闭原则”。
建造者模式是一种创建型设计模式, 使用多个简单的对象一步一步构建成一个复杂的对象,能够将一个复杂的对象的构建与它的表示分离,提供一种创建对象的最佳方式。主要用于解决对象的构建过于复杂的问题。
建造者模式主要基于四个核心类实现:
#include
#include
#include
// 抽象电脑类
class Computer
{
public:
Computer(){}
void SetBoard(const std::string & board)
{
_board=board;
}
void SetDisplay(const std::string & display)
{
_display=display;
}
virtual void SetOS()=0;
void ShowParmaters()
{
std::string param="Computer Paramaters:\n";
param+="\tBoard: " + _board + "\n";
param+="\tDisplay: " + _display + "\n";
param+="\tOs: " + _os + "\n";
std::cout<<param<<std::endl;
}
protected:
std::string _board;
std::string _display;
std::string _os;
};
// 具体产品类
class MacBook:public Computer
{
public:
MacBook(){}
virtual void SetOS()
{
_os="Mac Os X12";
}
};
// 抽象建造者类
class Builder
{
public:
virtual void buildBoard(const std::string & board)=0;
virtual void buildDisplay(const std::string & display)=0;
virtual void buildOS()=0;
virtual std::shared_ptr<Computer> build()=0;
};
// 具体产品的建造者类: 实现抽象接口, 构建和组装各个部件
class MacBookBuilder: public Builder
{
public:
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();
}
std::shared_ptr<Computer> build()
{
return _computer;
}
private:
std::shared_ptr<Computer> _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:
std::shared_ptr<Builder> _builder;
};
int main()
{
Builder*builder=new MacBookBuilder();
std::unique_ptr<Director> director(new Director(builder));
director->construct("华硕主板", "三星显示器");
std::shared_ptr<Computer> computer=builder->build();
computer->ShowParmaters();
return 0;
}
代理模式指代理控制对其他对象的访问, 也就是代理对象控制对原对象的引用。在某些情况下,一个对象不适合或者不能直接被引用访问,而代理对象可以在客户端和目标对象之间起到中介的作用。
代理模式的结构包括一个是真正的你要访问的对象(目标类)、一个是代理对象。目标对象与代理对象实现同一个接口,先访问代理类再通过代理类访问目标对象。代理模式分为静态代理、动态代理:
以租房为例,房东将房子租出去,但是要租房子出去,需要发布招租启示, 带人看房,负责维修,这些工作中有些操作并非房东能完成,因此房东为了图省事,将房子委托给中介进行租赁。 代理模式实现:
#include
// 房东要把一个房子通过中介租出去理解代理模式
class RentHouse
{
public:
virtual void rentHouse()=0;
};
// 房东类:将房子租出去
class Landlord: public RentHouse
{
public:
virtual void rentHouse()
{
std::cout<<"将房子租出去"<<std::endl;
}
};
// 中介代理类:对租房子进行功能加强,实现租房以外的其他功能
class Intermediary: public RentHouse
{
public:
virtual void rentHouse()
{
std::cout<<"发布招租启示"<<std::endl;
std::cout<<"带人看房"<<std::endl;
_landlord.rentHouse();
std::cout<<"负责租后维修"<<std::endl;
}
private:
Landlord _landlord;
};
int main()
{
Intermediary intermediary;
intermediary.rentHouse();
return 0;
}