引言
本篇文章使用c++20标准实现的一个自定义的日志缓冲区。打印日志对于每一个项目来说都是必不可少的,在muduo库里面我们也简单的实现了一个日志的打印,大家如果有兴趣可以看一看我之前的这个博客写文章-CSDN创作中心。不过那个是比较简易的,而本篇文章的上下篇将完成一个复杂,高性能的打印日志器。
日志
打印日志是为了可以更快的发现错误,而我们的错误是分等级的,有的错误无伤大雅,而有的错误一旦发生就会造成损失,所以我们需要给日志分几个级别、
然后对于日志所对外提供的函数,一共就两个,一个是把Level转化成string的类型,因为我们的enum底层还是uint8类型,我们为了可以更好的看出到底是什么类型的错误,需要转化成字符串。
还有一个是把一个字符串形式的日志级别,转换成 LogLevel::Level 枚举值,一般的用法如下:
auto level = LogLevel::FromString("debug");
cpp
class PANS_API LogLevel final
{
public:
enum class Level : std::uint8_t
{
LOG_DEBUG, // 调试细节
LOG_INFO, // 记录正常运行的信息
LOG_WARN, // 可能存在的问题
LOG_ERROR, // 操作失败,需要检查
LOG_FATAL, // 程序如果继续运行会造成损失
LOG_OFF, // 关闭日志
};
[[nodiscard]] static std::string_view ToString(Level level) noexcept;
[[nodiscard]] static LogLevel::Level FromString(std::string_view value) noexcept;
};
我们这里用宏来简化代码的书写,并且因为这个宏是定义在函数的内部,所以当我们用完这个宏之后,我们需要及时的释放掉。FromString这个函数的思路就是创建一个数组来存储大写的value,然后返回这个,所以主要的作用就是转化为大写,因为外部可能会传递小写的,类似于debug之类的。
cpp
std::string_view LogLevel::ToString(Level level) noexcept
{
switch (level)
{
#define XX(name) case Level::LOG_##name : return #name;
XX(DEBUG)
XX(INFO)
XX(WARN)
XX(ERROR)
XX(FATAL)
XX(OFF)
}
#undef XX
return "UNKNOWN";
}
// 把一个字符串形式的日志级别,转换成 LogLevel::Level 枚举值
LogLevel::Level LogLevel::FromString(std::string_view value) noexcept
{
std::array<char, 6> normalized{};
std::transform(value.begin(), value.end(), normalized.begin(), [](unsigned char character){
return static_cast<char>(std::toupper(character));
});
const std::string_view upper_value(normalized.data(), value.size());
#define XX(name) if(upper_value == #name) return Level::LOG_##name;
XX(DEBUG)
XX(INFO)
XX(WARN)
XX(FATAL)
XX(OFF)
XX(ERROR)
#undef XX
return Level::LOG_OFF;
}
缓冲区
接下来就是比较关键的缓冲区,其实大部分的项目里面打印的日志都是在堆上打印的,堆上写完即释放,里面比较典型的就是sylar的服务器架构,就是用std::stringstream,这个的优点就是比较的简单容易实现。但是缺点也比较明显,我们拿到数据的时候需要用std::string来接受,多了一次拷贝,而自定义的缓冲区我们可以开窗口,用string_view来直接拿数据。
因为一般的日志都是少于200字的,栈的空间完全足够,但是也不排除一些比较长的日志信息,这个时候我们就需要把日志的内容放在堆上,方便扩容处理。所以我们的目的就是如果日志信息比较少就在栈上,日志比较长就在堆上。那么我们很容易想到的架构就是array和vector
这里解释一下,我们在缓冲区对象里面的成员array和成员一起被放在了栈上,随着缓冲区一起被销毁,而vector的一些属性,比如size,type放在了栈上,其数据是放在了堆上,方便扩容。
我们这里还是用的类模板,因为我们的内联缓冲区的大小还是不确定的,其中对外提供的API主要就是append,其中的逻辑是如果堆上没有内容并且要写的内容没有超过限制,那么就写入inline
如果超出了,并且overflow是空的,那么我们先把之前在inline的数据通过insert拷贝到overflow里面,再把当前的数据insert到end()后面。
这里要说明一下子std::string_view,这个性能很高,因为其不需要拷贝到string,里面管理了一个指针ptr和string的大小size,所以我们
cpp
// 自定义一个日志缓冲区,针对高频,小数据量的日志场景,在栈上分配日志缓冲区,与超长日志,再在堆上分配内存
template <std::size_t INLINE_CAPACITY>
class InlineBuffer
{
public:
void append(const char* data, std::size_t size)
{
if (size == 0)
{
return;
}
if (overflow_.empty() && size + size_ <= INLINE_CAPACITY)
{
std::memcpy(inline_.data() + size_, data, size);
size_ += size;
return;
}
if (overflow_.empty())
{
const std::size_t required_capacity = size_ + size;
overflow_.reserve(std::max(required_capacity, 2 * INLINE_CAPACITY));
overflow_.insert(overflow_.end(), inline_.data(), inline_.data() + size_);
}
overflow_.insert(overflow_.end(), data, data + size);
size_ += size;
}
void append(std::string_view value)
{
append(value.data(), value.size());
}
void append(char value)
{
append(&value, 1);
}
[[nodiscard]] const char* data() const noexcept
{
return overflow_.empty() ? inline_.data() : overflow_.data();
}
[[nodiscard]] std::size_t size() const noexcept
{
return size_;
}
[[nodiscard]] std::string_view view() const noexcept
{
return {data(), size()};
}
private:
std::array<char, INLINE_CAPACITY> inline_{};
std::vector<char> overflow_;
std::size_t size_;
};
为了继续可以使用流式输入的接口,我们重新实现了一个streambuf,然后重写对应的接口。
这个是我们的目标,但是与系统提供put_area不同的是,我们有自己的缓冲区了,系统的put_area会维护两个指针pptr和epptr,而我们因为没有提供这个put_area所以不会进入if()里面,而是直接return,执行overflow函数(我们需要重写)

std::ostream 负责格式化输出,std::streambuf 负责把数据交给实际的存储对象。
streambuf 默认没有自己的输出缓冲区,pbase/pptr/epptr 都为空;
如果通过 setp() 设置缓冲区,sputc() 会先判断 pptr() < epptr(), 有空间就直接写入,没有空间才调用 overflow()。
多个字符通过 sputn() 进入虚函数 xsputn()。 本类没有调用 setp(),因此不使用 streambuf 自己的缓冲区:
单字符 -> sputc() -> 无可用空间 -> overflow() -> InlineBuffer
多字符 -> sputn() -> xsputn() -> InlineBuffer
所以 SmallStreamBuffer 的作用就是: 把 std::ostream 的输出接口直接转接到自己的 InlineBuffer。
cpp
// 继承std::streambuf是为了能够能够继续使用流式输入接口,兼容ostream生态
template <std::size_t INLINE_CAPACITY>
class SmallStreamBuffer : public std::streambuf
{
public:
explicit SmallStreamBuffer(InlineBuffer<INLINE_CAPACITY>& buffer) noexcept
: buffer_(buffer)
{}
protected:
std::streamsize xsputn(const char* data, std::streamsize size) override
{
if (size <= 0)
{
return 0;
}
buffer_.append(data, static_cast<std::size_t>(size));
return size;
}
int_type overflow(int_type character) override
{
// overflow,除了可以接受一般字符,还可能会接收流终止符EOF
// 如果我们遇到了EOF, 把它转成非EOF值向上报告写入成功,实际上不向Buffer里写入任何值。
if (traits_type::eq_int_type(character, traits_type::eof()))
{
return traits_type::not_eof(character);
}
buffer_.append(traits_type::to_char_type(character));
return character;
}
private:
InlineBuffer<INLINE_CAPACITY>& buffer_;
};
总结
本篇文章到这里就结束了!!!!希望可以帮助大家理解~~~