• 轻量级RPC分布式网络通信框架设计——日志模块解析


    01 背景

    (1)由于RPC服务器端采用了epoll+多线程技术 , 并发处理来自客户端的请求,所以有可能造成多线程同时写日志信息

    (2)因此设计了一个线程安全的消息队列(主要采用了互斥锁和条件变量),写日志的时候会先将日志信息放到消息队列中去,再有专门的写日志进程读取消息队列中的日志,写入文件中。

    (3)最后,设计了日志模块设计成为了单例模式。

    02 日志框架设计

    异步缓冲日志队列

    image-20221104194400460

    03 单例模式设计

    单例模式就是一个类只允许创建出一个实例对象。

    单例模式的好处主要有两个:

    (1) 可以用来解决资源冲突,比如日志模块, 假设两个对象同时写入日志文件或者对共享变量执行修改,就会出现相互覆盖的情况,而单例模式只会产生一个对象,这个对象统一访问共享资源或者竞争资源,可以避免相互覆盖的情况

    (2)第二表示全局唯一类,有些数据在系统中只应该保留一份,所以应该设计为单例模式,比如配置文件、全局ID生成器

    饿汉式单例模式

    一开始就加载了实例

    #include 
    
    using namespace std;
    //饿汉式单例模式
    class Singleton {
    public:
        static Singleton* Getinstance() {
            static Singleton instance;
            return &instance;
        }
    private:
        Singleton() {
            cout << "Singleton mode" << endl;
        }
    };
    
    int main() {
        Singleton* test1 = Singleton::Getinstance();
        Singleton* test2 = Singleton::Getinstance();
        system("pause");
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    image-20221104200936955

    这种写法在C++98中不是线程安全的,但是在C++11起是线程安全的,因为静态的局部变量在调用的时候分配到静态存储区,所以在编译的时候没有分配。

    静态局部对象:
    在程序执行到该对象的定义处时,创建对象并调用相应的构造函数!
    如果在定义对象时没有提供初始指,则会暗中调用默认构造函数,如果没有默认构造函数,则自动初始化为0。
    如果在定义对象时提供了初始值,则会暗中调用类型匹配的带参的构造函数(包括拷贝构造函数),如果没有定义这样的构造函数,编译器可能报错!
    直到main()结束后才会调用析构函数!

    懒汉式单例模式

    #include 
    
    using namespace std;
    
    //懒汉式单例模式
    class Singleton {
    public:
        static Singleton* Getinstance() {
            if (instance == nullptr) {
                instance = new Singleton;
            }
            return instance;
        }
    private:
        Singleton() {
            cout << "Singleton mode" << endl;
        }
        static Singleton* instance;
    };
    
    Singleton* Singleton::instance = nullptr;  // 这一步很重要!!!!
    
    int main() {
        Singleton* test1 = Singleton::Getinstance();
        Singleton* test2 = Singleton::Getinstance();
        system("pause");
        return 0;
    }
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    特点是延迟加载 当函数被首次访问的时候才加载

    线程安全的懒汉式单例模式

    #include 
    #include 
    
    using namespace std;
    
    //线程安全懒汉式单例模式
    class Singleton {
    public:
        static Singleton* Getinstance() {
            if (instance == nullptr) {
                lock_guard<mutex> lc(mtx);
                instance = new Singleton;
            }
            return instance;
        }
    private:
        Singleton() {
            cout << "Singleton mode" << endl;
        }
        static Singleton* instance;
        static mutex mtx;
    };
    
    mutex Singleton::mtx;
    Singleton* Singleton::instance;
    
    int main() {
        Singleton* test1 = Singleton::Getinstance();
        Singleton* test2 = Singleton::Getinstance();
        system("pause");
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    image-20221104203840091

    04 日志模块源码解析

    (1)线程安全的队列 借助mutex和cv实现了queue

    (2)日志模块本身设计为(饿汉式)单例模式

    (3)最后,主要实现了两部分功能 一个是rpc服务端worker线程向队列中写入数据 主要利用push接口 log函数实现 然后是 加入写日志线程 向日志文件中写队列的数据 主要利用pop接口(设置分离线程)

    lockqueue.h

    (1)首先需要封装一个日志队列的自定义的push和pop的api接口,通过mutex和conditional_variable来保证线程安全。

    (2)他的原理类似一个生产者消费者模型,队列push函数处理的是rpc服务器端的多个worker线程向队列里写数据,写之前加上一把互斥锁,然后push数据,结束以后notify阻塞等待写日志线程向磁盘写数据。

    (3)pop接口 首先会检测队列是否为空 为空代表没有数据 就会进入阻塞wait状态 然后释放锁 ,有数据来了返回数据。

    #pragma once
    #include 
    #include 
    #include     // pthread_mutex_t  线程互斥
    #include    // pthread_condition_t   线程通信
    
    
    // 模板代码  不能写在cc文件中
    // 异步写日志的日志队列
    template 
    class LockQueue {
    public:
        //muduo 提供的 多个worker线程都会写日志queue
        void Push(const T &data) {
            std::lock_guard lock(m_mutex);  // 获得互斥锁
            m_queue.push(data);
            m_condvariable.notify_one();  // 唤醒wait线程
        }
        // 出右括号释放锁
    
        // 一个线程 在读日志queue,写日志文件
        T Pop() {
            std::unique_lock lock(m_mutex);
            while (m_queue.empty()) {
                // 日志队列为空, 线程进入wait状态
                m_condvariable.wait(lock);  // 进入wait等待状态  释放锁
            }
    
            T data = m_queue.front();
            m_queue.pop();
            return data;
        }
    private:
        std::queue m_queue;
        std::mutex m_mutex;
        std::condition_variable m_condvariable;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    logger.h

    #pragma once
    #include "lockqueue.h"
    #include 
    
    enum LogLevel {
        INFO,   // 普通信息
        ERROR,  // 错误信息
    };
    
    //mprpc框架提供的日志系统
    
    class Logger {
    public:
        //获取日志的单例
        static Logger& GetInstance();
        //设置日志级别
        void SetLogLevel(LogLevel level);
        //写日志
        void Log(std::string msg);
    private:
        int m_loglevel;  // 记录日志级别
        LockQueue m_lckQue; // 日志缓冲队列
    
        Logger(); //  设置为单例模式  
        Logger(const Logger&) = delete;  // 防止通过拷贝构造生成新对象
        Logger(Logger&&) = delete;
    };
    
    //定义宏    可变参 LOG_INFO("xxx %d %s", 20, "xxx")
    #define LOG_INFO(logmsgformat, ...) \
        do \
        {  \
            Logger &logger = Logger::GetInstance(); \
            logger.SetLogLevel(INFO); \
            char c[1024] = {0}; \
            snprintf(c, 1024, logmsgformat, ##__VA_ARGS__); \
            logger.Log(c); \
        } while (0)
    
    
    #define LOG_ERR(logmsgformat, ...) \
        do \
        {  \
            Logger &logger = Logger::GetInstance(); \
            logger.SetLogLevel(ERROR); \
            char c[1024] = {0}; \
            snprintf(c, 1024, logmsgformat, ##__VA_ARGS__); \
            logger.Log(c); \
        } while (0)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    logger.cc

    #include "logger.h"
    #include "time.h"
    #include 
    
    Logger& Logger::GetInstance() {
        static Logger logger;
        return logger;
    }
    
    Logger::Logger() {
        // 启动专门的写日志进程
        std::thread writeLogTask([&]() {
            for (;;) {
                // 获取当天的日期, 然后取日志信息, 写入相应的日志文件当中 a+
                time_t now = time(nullptr);
                tm *nowtm = localtime(&now);
    
                char file_name[128];
                sprintf(file_name, "%d-%d-%d-log.txt", nowtm->tm_year + 1900, nowtm->tm_mon + 1, nowtm->tm_mday);
    
                FILE *pf = fopen(file_name, "a+");
    
                if (pf == nullptr) {
                    std::cout << "logger file : "<< file_name << "open error!" << std::endl;
                    exit(EXIT_FAILURE);
                }
    
                std::string msg = m_lckQue.Pop();  // 从异步日志队列中读数据
    
                char time_buf[128] = {0};
                sprintf(time_buf, "%d:%d:%d => [%s] ", nowtm->tm_hour, nowtm->tm_min, nowtm->tm_sec, (m_loglevel == INFO ? "info" : "error"));
                msg.insert(0, time_buf);
                msg.append("\n");
    
                fputs(msg.c_str(), pf);
                fclose(pf);
            }
        });
        // 设置分离线程,守护线程
        writeLogTask.detach();
    }
    
    //设置日志级别
    void Logger::SetLogLevel(LogLevel level) {
        m_loglevel = level;
    }
    
    
    // 写日志, 把日志信息写入lockqueue缓冲区中
    void Logger::Log(std::string msg) {
        //
        m_lckQue.Push(msg);   // 从worker线程中把数据写入queue
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    05 宏的写法

    //定义宏    可变参 LOG_INFO("xxx %d %s", 20, "xxx")
    #define LOG_INFO(logmsgformat, ...) \
        do \
        {  \
            Logger &logger = Logger::GetInstance(); \
            logger.SetLogLevel(INFO); \
            char c[1024] = {0}; \
            snprintf(c, 1024, logmsgformat, ##__VA_ARGS__); \
            logger.Log(c); \
        } while (0)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 反斜杠\表示 继续符 本行与下一行连接起来

    _VA_ARGS_的用法

    自定义打印时,用到可变参数,用...即可表示可变参数,如下:

    #include 
    
    #define LOG1(...)               printf(__VA_ARGS__)//...表示可变参数,__VA_ARGS__就是将...的值复制到这里
    int main(int argc, char** argv)
    {
        char *str = "test __VA_ARGS__";
        int num = 10086;
        LOG1("this is test __VA_ARGS__\r\n");
        LOG1("this is test __VA_ARGS__:%s, %d\r\n", str, num);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    打印结果如下:

    this is test __VA_ARGS__
    this is test __VA_ARGS__:test __VA_ARGS__, 10086
    
    • 1
    • 2

    VA_ARGS__就是将…的值复制到这里
    int main(int argc, char** argv)
    {
    char *str = “test VA_ARGS”;
    int num = 10086;
    LOG1(“this is test VA_ARGS\r\n”);
    LOG1(“this is test VA_ARGS:%s, %d\r\n”, str, num);

    return 0;
    
    • 1

    }

    
    打印结果如下:
    
    ```cpp
    this is test __VA_ARGS__
    this is test __VA_ARGS__:test __VA_ARGS__, 10086
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
  • 相关阅读:
    微信小程序框架---视图层&逻辑层&API&事件
    数仓工具—Hive集成篇之UDF写ES(04)
    chromium线程模型(1)-普通线程实现(ui和io线程)
    全栈自动化测试之 python基础语法介绍
    java计算机毕业设计燕理快递中转站系统设计与实现MyBatis+系统+LW文档+源码+调试部署
    3D激光slam:ALOAM---后端lasermapping最终篇地图更新及消息发布
    286节---------6月22日
    JSD-2204-异常处理-SpringJDBC事务管理-Day14
    python 学习积累
    携职教育:对于想进入财务工作的人来说,第一个证考CPA还是CMA?
  • 原文地址:https://blog.csdn.net/qq_41945053/article/details/127721900