webserver服务器从零搭建到上线(五)|noncopyable类和Logger类

webserver服务器从零搭建到上线(五)|noncopyable类和Logger类

码农世界 2024-05-30 前端 81 次浏览 0个评论

文章目录

  • noncopyable类
    • delete掉了拷贝构造和析构
    • protected成员
      • 1. 允许派生
      • 2.防止直接实例化
      • 主要使用场景
      • Logger类
        • 定义日志级别
        • 输出一个日志类
        • 实现对应的成员函数
        • 实现宏函数来调用日志类
        • 知识拓展

          noncopyable类

          我们首先进入/muduo/net中查看TcpServer.h、EventLoop.h等等核心代码的类,都继承子一个基类noncopyable,那么我们不得不去看一下noncopyable到底是啥了

          class noncopyable
          {
           public:
            noncopyable(const noncopyable&) = delete;
            void operator=(const noncopyable&) = delete;
           protected:
            noncopyable() = default;
            ~noncopyable() = default;
          };
          

          这个类他把它的拷贝构造函数和拷贝复制运算符都delete掉了;另外他写了一个默认default的构造函数和析构函数,相当于就是给直接套了一个空的大括号,因为这两个函数编译器本来就可以自动生成。

          delete掉了拷贝构造和析构

          我们在使用该基类的时候,去对TcpServer的对象进行拷贝构造和赋值的时候,由于派生类的拷贝和赋值要先去调用基类的拷贝和赋值,然后才是派生类特有部分的拷贝和赋值。

          但是由于基类的拷贝和赋值都被delete掉了,所以它的好处就在于我们的其他类如果不想让他被拷贝或者复制,就不用额外写这段代码了,这是一个大型程序所必须的。

          总结

          • 防止派生类的拷贝构造

            protected成员

            1. 允许派生

            protected 访问修饰符使得只有 noncopyable 类及其派生类能够访问这些构造和析构函数。这意味着不能直接在 noncopyable 类的外部创建该类的实例(即不能直接实例化 noncopyable),但您可以创建派生自 noncopyable 的类的实例。

            2.防止直接实例化

            如果构造函数和析构函数是 public 的,那么理论上可以直接创建和销毁 noncopyable 类的对象,这违背了类的设计初衷(作为一个基类来阻止复制)。通过将构造和析构函数设为 protected,noncopyable 类的实例化只能通过其子类进行,而 noncopyable 本身不能被直接实例化。

            主要使用场景

            有一个管理资源的类,且这些资源不应该被复制时(比如说,单例模式、文件处理类、数据库连接管理类),您可以让这个类继承自 noncopyable,从而禁止编译时对这些资源进行复制操作。

            Logger类

            日志对于一个软件来说是非常重要的,如果软件上线,用gdb调试有很多不便,所以我们应该使用日志来记录问题。

            定义日志级别

            定义日志级别一般都是使用枚举类。

            enum LogLevel {
            	INFO,
            	ERROR,
            	FATAL,
            	DEBUG,
            };
            

            输出一个日志类

            日志类应该输出单例模式:

            class Logger: noncopyable {
            public:
            	//获取日志唯一的实例对象
            	static Logger& instance();
            	//设置日志级别
            	void setLogLevel(int level);
            	//写日志的接口
            	void log(std::string msg);
            private:
            	int logLevel_;
            	Logger() { }
            }
            

            为什么要使用单例模式呢?

            1. 全局访问点:单例模式确保在整个应用程序中只有一个日志类实例,方便统一管理和访问。
            2. 资源共享:避免多个日志实例导致的资源浪费,特别是文件句柄和网络连接等有限资源。
            3. 一致性:确保日志配置和行为一致,避免不同模块使用不同的日志实例导致的日志风格不一致。

            实现对应的成员函数

            Logger& Logger::instance() { //获取日志唯一的实例对象
            	static Logger logger;
            	return logger;
            }
            void Logger::setLogLevel(int level) {// 设置日志级别
            	logLevel_ = level;
            }
            void Logger::log(std::string msg) {//写日志的接口  【级别信息】time : xxx
                switch (logLevel_)
                {
                case INFO:
                    std::cout << "[INFO]";
                    break;
                case ERROR:
                    std::cout << "[ERROR]";
                    break;
                case FATAL:
                    std::cout << "[FATAL]";
                    break;
                case DEBUG:
                    std::cout << "[DEBUG]";
                    break;
                default:
                    break;
                }
            	//打印时间和msg
            	std::cout << Timestamp::now().toString() 
            			  << ": " << msg << std::endl;
            }
            

            这里的Timestamp类在「webserver服务器从零搭建到上线(六)」会进行介绍。

            实现宏函数来调用日志类

            用的时候我们不需要让用户来操作这些复杂的步骤:

            • 获取日志实例
            • 设置日志级别
            • 在写日志

              我们难道还让用户调用这么多接口吗?

              用户真正关心的就是写日志,打印出来,所以我们应该把这些内容全部包装到宏函数里面。

              这里定义四种宏,对应4个日志级别

              //LOG_INFO("%s %d", arg1, arg2)
              //参数 ... 表示可变参
              #define LOG_INFO(logmsgFormat, ...) \
              	do { \
              		Logger &logger = Logger::instance(); \
              		logger.setLogLevel(INFO); \
              		char buf[1024] = { 0 }; \
              		snprintf(buf, 1024, logmsgFormat, ##__VA_ARGS__); \
              		logger.log(buf); \
              	} while(0)
              

              在宏定义中,##__VA_ARGS__是一种预处理器语法,用于处理可变参数宏。


              #define LOG_ERROR(logmsgFormat, ...) \
                  do { \
                      Logger &logger = Logger::instance(); \
                      logger.setLogLevel(ERROR); \
                      char buf[1024] = { 0 }; \
                      snprintf(buf, 1024, logmsgFormat, ##__VA_ARGS__); \
                      logger.log(buf); \
                      exit(-1); \
                  } while(0)
              #define LOG_FATAL(logmsgFormat, ...) \
                  do { \
                      Logger &logger = Logger::instance(); \
                      logger.setLogLevel(FATAL); \
                      char buf[1024] = { 0 }; \
                      snprintf(buf, 1024, logmsgFormat, ##__VA_ARGS__); \
                      logger.log(buf); \
                  } while(0)
              

              在ERROR级别应该直接停止程序运行。

              最后我们重点谈一下 LOG_DEBUG

              #ifdef MUDEBUG
              #define LOG_DEBUG(logmsgFormat, ...) \
                  do { \
                      Logger &logger = Logger::instance(); \
                      logger.setLogLevel(DEBUG); \
                      char buf[1024] = { 0 }; \
                      snprintf(buf, 1024, logmsgFormat, ##__VA_ARGS__); \
                      logger.log(buf); \
                  } while(0)
              #else
                  #define LOG_DEBUG(logmsgFormat, ...)
              #endif
              

              其中的 #ifdef 可以保证我们在release版本中,不定义宏 MUDEBUG,就可以不打印调试信息,毕竟日志属于IO操作,对想能影响较大。

              知识拓展

              在实现宏函数来调用日志类中我们使用了 snprintf ,它是 C 标准库中的一个函数,用于将格式化输出写入字符串,限制写入的最大字符数以避免缓冲区溢出。它是 sprintf 的安全版本。

              int snprintf(char *str, size_t size, const char *format, ...);
              

              参数:

              str:指向目标缓冲区的指针。

              size:要写入的最大字符数,包括终止空字符。

              format:格式字符串,类似于 printf 的格式说明。

              …:可变参数,根据格式字符串进行格式化。

              使用日志类是通过使用宏函数:

              LOG_INFO("func=%s => fd total count:%lu \n"
                 , __FUNCTION__
                 , channels_.size());
              

              其中的__FUNCTION__表示当前函数的名称。这个宏是由编译器提供的,帮助开发者在调试和记录日志时自动插入当前函数的名称,便于追踪和排查问题。

转载请注明来自码农世界,本文标题:《webserver服务器从零搭建到上线(五)|noncopyable类和Logger类》

百度分享代码,如果开启HTTPS请参考李洋个人博客
每一天,每一秒,你所做的决定都会改变你的人生!

发表评论

快捷回复:

评论列表 (暂无评论,81人围观)参与讨论

还没有评论,来说两句吧...

Top