Appender抽象类的实现c++20

引言

本篇文章还是对于日志器的实现,我们的日志输出会输出到不同的地方,有时候是终端,有时候是网络,有时候是文件里面,所以我们需要规定其输出的地方,这也就是appender的作用。

appender.h

这个类的实现我们是要对外面公开的,但是里面的细节我们会隐藏。并且我们Appender是一个基类,而其派生类我们是要实现我们特定输出的位置。我们要实现这个方案其实和上一个format的架构思想基本一样,在类里面声明一个impl,类外定义,最后我们外部通过传入不同的impl,实现不同的方案。

还有一个是AppenderAccess,这个类是为了代理我们的内部权限,严格控制谁可以访问我们的Appender私有实现,也就是说凡是访问Appender的类,都需要在AppenderAccess里面存储。在这个代码的最后我们提供了所有对应的外部可以访问的接口,这个接口实际上调用的是AppenderAcess里面的接口。我们之所以加了这一层,就是为了确保只有特定的类可以访问这个Appender,这里面也涉及到了friend友元类。

可能这里还不是很清楚,我们往后面看。

cpp 复制代码
// 用来代理我们的内部权限,严格控制谁可以访问我们的Appender私有实现
namespace detail {
class AppenderAccess;
}

class PANS_API Appender final 
{
public:
    // 声明一个内部实现内,类外定义(我们的输出地方可能是网络,可能是终端。。。不同的输出地方需要不同的处理,所以全部在impl的派生类里面实现)
    class Impl;

    ~Appender() = default;
    Appender(const Appender&) = delete;
    Appender(Appender&& ) = delete;
    Appender& operator=(const Appender&) = delete;
    Appender& operator=(Appender&&) = delete;

    void setLevel(LogLevel::Level level) noexcept;
    [[nodiscard]] LogLevel::Level getLevel() const noexcept;

    // 把数据写入脏页,我们的std::endl就是换行加上flush
    void flush();
    // 把脏页的数据写入磁盘,达到数据同步的效果
    void sync();

private:
    // 而我们通过构造函数传进来对应的指针,就可以输出到对应的地方
    explicit Appender(std::unique_ptr<Impl> impl) noexcept; 
    std::unique_ptr<Impl> impl_;
    friend class detail::AppenderAccess;
};

using AppenderPtr = std::shared_ptr<Appender>;

[[nodiscard]] PANS_API AppenderPtr MakeStdoutAppender();
[[nodiscard]] PANS_API AppenderPtr MakeFileAppender(std::string file_name);

}

这个就是impl的实现,本质上是一个基类,也就是里面的函数都是虚函数,实现的方法都是交给派生类。

flush和sync:

这里我们补充一下操作系统的知识:

当我们要写入一些内容的时候,会先写入缓存之中,这个缓存并不会显示在终端上面,所以我们如果先printf("xxxx"),然后while()阻塞,那么我们的终端不会显示,因为并没有触发我们flush的条件,导致内容在缓存里面,而不在内存里面。但是如果我们加了std::endl,那么cout的内容就会立马出来,因为std::endl不仅仅是换行,还执行了flush的命令。当flush写入内存之后,我们称其为脏页,也就是内存里面的数据还没有同步到磁盘上面,只要在这一瞬间断电了,那么这一块内容就会消失,因为没有保存到磁盘里面。

所以我们需要进行sync强制同步的操作,把数据写入磁盘。

我们的对脏页的同步操作,系统也会执行,一般来说,5s系统会强制把脏页的内容写入磁盘,3s会对脏页添加上过期的标志,下一次同步sync的时候会先处理这一部分过期的内容,或者脏页的内容超过了内存的10%,会立刻sync。但是正式因为有时间的限制,操作系统不可能每时每刻都在读取脏页的内容,所以一旦不幸运的事情(系统崩溃)发生,我们的内容全部都没有了。

然后还有一个AppenderAccess类的声明,里面就是我们要定向输出的函数,这几个函数会返回对应的Append指针,最后一个append函数就是统一调用这些派生类appender。

cpp 复制代码
class Appender::Impl
{
public:
    virtual ~Impl() = default;
    void append(LogLevel::Level level, std::string_view formatted_record) noexcept;
    void setLevel(LogLevel::Level level) noexcept;
    [[nodiscard]] LogLevel::Level getLevel() const noexcept;
    void flush();
    void sync();

protected:
    virtual void writeUnlocked(std::string_view formatted_record) noexcept = 0;
    virtual void flushUnlocked() noexcept = 0;
    virtual void syncUnlocked() noexcept = 0;

    std::mutex mutex_;

private:
    std::atomic<LogLevel::Level> level_{LogLevel::Level::LOG_DEBUG};
};

namespace detail
{

class AppenderAccess final
{
public:
    [[nodiscard]] static AppenderPtr MakeStdoutAppender();
    [[nodiscard]] static AppenderPtr MakeFileAppender(std::string file_name);
    static void Append(const AppenderPtr& appender, LogLevel::Level level, std::string_view formatted_record);
};

从这部分代码就可以看出来,我们外部传入Appender的impl,然后Appender会调用impl各自的实现。

cpp 复制代码
Appender::Appender(std::unique_ptr<Impl> impl) noexcept
    : impl_(std::move(impl))
{}

void Appender::setLevel(LogLevel::Level level) noexcept
{
    impl_->setLevel(level);
}

LogLevel::Level Appender::getLevel() const noexcept
{
    return impl_->getLevel();
}

void Appender::flush() 
{
    impl_->flush();
}

void Appender::sync()
{
    impl_->sync();
}

我们只会输出等级比我们设定大的日志,虽然我们函数里面都标志了noexcept,但是因为lock_guard这里会抛出异常,有可能没有抢到锁的情况,所以我们需要捕捉这个异常,然后写入stderr

这里解释一下为什么我们要写入stderr,而不是stdout

stderr是立刻输出的,没有对应的缓冲区,但是stdout有对应的缓冲区,也就是说如果我们没有flush,那么输入进stdout的内容有可能不会打印出来,而这个是一个异常的报错,绝对不允许这种情况发生。

如果没有报错,那么我们正常的写入,如果是致命的错误就需要flush,如果不是就不着急,因为flush也需要消耗性能的。

cpp 复制代码
void Appender::Impl::append(LogLevel::Level level, std::string_view formatted_record) noexcept
{
    if (static_cast<u8>(level) < static_cast<u8>(this->getLevel())) 
    {
        return;   
    }
    // std::mutex::lock()函数,在极端情况下会抛出std::system_error异常, lock_guard的构造函数也不是noexcept的
    try
    {
        std::lock_guard<std::mutex> lock(mutex_);
        writeUnlocked(formatted_record);
        if (level == LogLevel::Level::LOG_FATAL)
        {
            flushUnlocked();
        }
    }
    catch(...)
    {
        constexpr std::string_view message = "pans logger: appender operation failed\n";
        std::fwrite(message.data(), 1, sizeof(message), stderr);
    }
}

这些是公共函数,本质上就是调用派生类重写的函数

cpp 复制代码
void Appender::Impl::setLevel(LogLevel::Level level) noexcept
{
    level_.store(level, std::memory_order_release);
}

LogLevel::Level Appender::Impl::getLevel() const noexcept
{
    return level_.load(std::memory_order_acquire);
}

// 把数据从程序的内存刷到操作系统缓冲区
void Appender::Impl::flush()
{
    std::lock_guard<std::mutex> lock(mutex_);
    flushUnlocked();
}

void Appender::Impl::sync()
{
    std::lock_guard<std::mutex> lock(mutex_);
    syncUnlocked();
}

这个是终端的虚函数实现,因为目的地是打印到终端上面,不需要写到磁盘上面,所以不需要sync

cpp 复制代码
class StdoutAppenderImpl final : public Appender::Impl
{
protected:
    void writeUnlocked(std::string_view formatted_record) noexcept override
    {
        std::cout.write(formatted_record.data(), static_cast<std::streamsize>(formatted_record.size()));
    }

    void flushUnlocked() noexcept override
    {
        std::cout.flush();
    }

    // 这个是输出到终端,不需要同步到磁盘,所以还是flush(也可以不写)
    void syncUnlocked() noexcept override
    {
        flushUnlocked();
    }
};

这个是输出到文件里面,我们这里用的是c提供的FILE,为什么不用c++20提供的std::ofstream呢,因为c++20底层没有提供得到文件描述符,这个在c++26里面才出现,但是因为我们使用c++20实现的,所以这里只能用FILE

cpp 复制代码
class FileAppenderImpl final : public Appender::Impl
{
public:
    explicit FileAppenderImpl(const std::string& file_name)
    {
        if (file_name.empty())
        {
            throw std::invalid_argument("logger file name cannot be empty");
        }
        /*
        a(append):追加写入,保留文件原来的内容,写入的数据放到文件末尾。文件不存在时会创建。
        b(binary):二进制模式,不进行文本转换,例如 Windows 下不会把 \n 自动转换成 \r\n。Linux 下文本模式和二进制模式通常没有区别。
        */
        file_ = std::fopen(file_name.c_str(), "ab");
        if (file_ == nullptr) 
        {
            // 通用错误类别,用来说明一个错误码应该按照什么规则解释,常用于解释 errno
            // 错误码只是数字,需要搭配错误类别才能确定含义
            throw std::system_error(errno, std::generic_category(), "failed to open logger file: " + file_name);
        }
    }

    ~FileAppenderImpl() override 
    {
        if (file_ != nullptr)
        {
            std::fflush(file_);
            std::fclose(file_);
        }
    }

protected:
    void writeUnlocked(std::string_view formatted_record) noexcept override
    {
        std::fwrite(formatted_record.data(), 1, formatted_record.size(), file_);
    }

    void flushUnlocked() noexcept override
    {
        std::fflush(file_);
    }

    void syncUnlocked() noexcept override
    {
        flushUnlocked();
        ::fdatasync(::fileno(file_));
    }

private:
    std::FILE* file_ = nullptr;
};

这个是最好帮助我们理解整个架构的代码,我们Access会返回每个派生类的指针,然后通过那这个指针传入append。最后我们要把指针返回到我们对外的接口里面。

cpp 复制代码
AppenderPtr detail::AppenderAccess::MakeStdoutAppender()
{
    return AppenderPtr(new Appender(std::make_unique<StdoutAppenderImpl>()));
}

AppenderPtr detail::AppenderAccess::MakeFileAppender(std::string file_name)
{
    return AppenderPtr(new Appender(std::make_unique<FileAppenderImpl>(file_name)));
}

void detail::AppenderAccess::Append(const AppenderPtr& appender, LogLevel::Level level, std::string_view formatted_record) 
{
    if (appender != nullptr) 
    {
        appender->impl_->append(level, formatted_record);
    }
}

AppenderPtr MakeStdoutAppender()
{
    return detail::AppenderAccess::MakeStdoutAppender();
}

AppenderPtr MakeFileAppender(std::string file_name) 
{
    return detail::AppenderAccess::MakeFileAppender(std::move(file_name));
}

总结

这个架构还是挺复杂的,需要多多体会~~~

相关推荐
小此方1 小时前
Linux网络(七):从 Socket 封装到应用层协议:手写网络版计算器,彻底搞懂 TCP 通信、序列化与反序列化
linux·网络·tcp/ip
captain3761 小时前
网络初 识
运维·服务器·网络
kyle~1 小时前
Linux --- epoll (I/O就绪事件通知机制)
linux·运维·服务器
AlfredZhao1 小时前
进程都杀了,为什么 `netstat` 还能看到端口?
linux
一条泥憨鱼1 小时前
【从0开始学习计算机网络】| 邮件协议入门:SMTP、POP3、IMAP
linux·运维·计算机网络·github
z落落4 小时前
C# WinForm Socket 转串口设备服务器+Modbus CRC16校验+Socket 客户端
服务器·开发语言·c#
神仙别闹11 小时前
基于 C++ 实现两个有序链表序列的交集
java·c++·链表
Johny_Zhao12 小时前
网络安全等级保护测评实施方案
linux·网络·人工智能·网络安全·信息安全·云计算·等保测评·系统运维·itsm
hansang_IR12 小时前
【题解】[APIO2023] 赛博乐园 / cyberland
c++·算法·图论