当前位置: 首页 > news >正文

《Muduo网络库:实现Logger日志类》

从现在开始,就要根据Muduo源码抄写Muduo网络库的代码了,Muduo源码中实现的逻辑很多,有的用不到,我们抽丝剥茧,目的在于学习其精髓与设计细节,并总结知识。

实现noncopyable类

noncopyable.h

#pragma once/*** noncopyable被继承以后,派生类对象可以正常的拷贝和析构,* 但是派生类对象无法进行拷贝构造和赋值操作*/
class noncopyable
{
public:noncopyable(const noncopyable &) = delete;void operator=(const noncopyable &) = delete;protected:noncopyable() = default;~noncopyable() = default;
};

如果很多类都想禁止拷贝构造和赋值操作,只需要继承noncopyable类即可,非常方便。

《C++继承:深究C++三大特性之继承》-CSDN博客

实现Timestamp类

获取时间信息,为实现日志做准备。

Timestamp.h

#pragma once#include <iostream>
#include <string>class Timestamp
{
public:Timestamp();explicit Timestamp(int64_t SecondsSinceEpoch);// 获取表示当前时间的Timestamp实例,当前时间的时间戳static Timestamp now();// 时间戳转换为方便读的字符串 year/month/day :hour/min/secstd::string tostring() const;private:int64_t SecondsSinceEpoch_;  // 存储从epoch时间(1970-01-01 00:00:00 UTC)到当前时间的秒数
};

Timestamp.cc

#include "Timestamp.h"#include <time.h>Timestamp::Timestamp(): SecondsSinceEpoch_(0)
{
}
Timestamp::Timestamp(int64_t SecondsSinceEpoch): SecondsSinceEpoch_(SecondsSinceEpoch)
{
}
// 获取表示当前时间的Timestamp实例,当前时间的时间戳
Timestamp Timestamp::now()
{return Timestamp(time(NULL));
}
// 时间戳转换为人类可读的本地时间字符串 year/month/day :hour/min/sec
std::string Timestamp::tostring() const
{char buf[128] = {0};// 将秒级时间戳转换本地时间tm结构体(包含年月日时分秒)struct tm *tm_time = localtime(&SecondsSinceEpoch_);// 格式化字符串:年/月/日/ 时:分:秒snprintf(buf, 128, "%4d/%02d/%02d %02d:%02d:%02d",tm_time->tm_year + 1900,  // 年份:tm_year是从1900开始的偏移量tm_time->tm_mon + 1,  // 月份:tm_year范围是0-11tm_time->tm_mday,tm_time->tm_hour,tm_time->tm_min,tm_time->tm_sec);return buf;
}// 可以测试时间代码编写是否有问题 g++ -o a./out Timestamp.cc -std=c++11
// int main()
// {
//     std::cout << Timestamp::now().tostring() << std::endl;
//     return 0;
// }

实现日志类,获取当前时间戳,将时间戳转化为人类可读的时间字符串。

一、

注意到Timestamp类的构造函数中使用了explicit关键字修饰,是为了防止编译器进行隐式类型转换,提高代码安全性和可读性,避免不必要的隐式类型转换带来的潜在错误

eg:
Timestamp t = 16200;  // 隐式类型转换 Timestamp tmp(16200) -> Timestamp t = tmp
// 看似方便,实则可能会存在潜在问题,比如16200可能想表示其他整数但在某函数中被当作时间戳
// 使用explicit,只能通过显示方式构造对象
Timestamp t = Timestamp(16200)

二、

注意到Timestamp now()方法被声明成了static(静态成员函数),主要原因在于它的功能与特定对象实例无关,而是用于创建一个表示“当前时间”的新Timestamp对象。static成员函数只属于类本身,而非某个类的实例。

eg:
// 直接通过类名调用,无需创建实例
Timestamp current = Timestamp::now();// 不需要这样(也不应该这样)
Timestamp temp;
Timestamp current = temp.now();  // 非静态函数才需要这样调用

三、

注意到tostring()方法被声明为const,表示该方法不会修改对象的任何成员变量,是一个已读操作。tostring()方法仅仅用于读取SecondsSinceEpoch_,将该时间戳转换为字符串,如果意外修改了SecondsSinceEpoch_,编译会报错。

在 C++ 中,所有仅读取成员变量、不修改对象状态的成员函数,都应该声明为const,这是良好的编程实践。

《C++ const关键字》-CSDN博客

四、

localtime是 C/C++ 标准库中用于将秒级时间戳转换为本地时区时间的函数,它的核心作用是把一个抽象的 “从 epoch 时间(1970-01-01 00:00:00 UTC)开始的秒数” 转换为人类可读的 “本地时区的年、月、日、时、分、秒” 等信息,存储在struct tm结构体中。

struct tm* localtime(const time_t* timer);

struct tm结构体,存储“拆解后时间信息”的核心结构体:

实现Logger类

实现日志系统。

Logger.h

#pragma once#include <string>#include "noncopyable.h"// 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);#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);                                  \} 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);#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// 定义日志级别
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() {}
};

Logger.cc

#include "Logger.h"
#include "Timestamp.h"#include <iostream>// 获取日志唯一的实例对象
Logger &Logger::instance()
{static Logger logger;return logger;
}
// 设置日志级别
void Logger::setlogLevel(int level)
{logLevel_ = level;
}
// 写日志 [日志级别] 时间 : 日志msg
void Logger::log(std::string msg)
{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;}// 打印时间和msgstd::cout << Timestamp::now().tostring() << " : " << msg << std::endl;
}

一、

在Logger类中,使用静态instance()方法实现单例模式(确保全局只有一个Logger实例)。日志系统的核心功能是统一处理和输出日志,它作为一个“全局服务”。

  • 如果存在多个Logger实例,可能导致日志格式不一样(例如不同实例设置了不同日志级别、输出方式)
  • 多实例也可能导致资源竞争(例如同时写入一个日志文件时的冲突)
  • 全局需要一个“单一入口”来管理日志配置,避免混乱

所以,日志类天生适合设计成单例。全局只需要一个实例来协调所有日志操作。

  1. 静态instance()方法属于类本身,不依赖对象即可调用。获取单例“入口”。
  2. 内部通过静态局部变量(第一次调用instance时初始化,之后调用不再创建)实现唯一实例。
  3. 通过继承noncopyable类禁止拷贝和赋值,构造函数私有化,外部无法通过new Logger()或Logger obj创建新实例

《C++特殊类的设计 + 单例模式》-CSDN博客

二、

日志系统中通常使用宏定义(而非普通函数)来实现LOG_INFO、LOG_ERROR等日志输出接口。核心作用是简化日志调用流程,更具灵活性和实用性。

1、封装重复的日志操作、简化调用

// 不使用宏的繁琐写法
Logger::instance().setlogLevel(INFO);
char buf[1024];
snprintf(buf, 1024, "用户 %s 登录成功", username);
Logger::instance().log(buf);
// 有了宏之后,只需要一行
LOG_INFO("用户 %s 登录成功", username);  // 简洁明了

宏自动帮我们完成了获取单例、设置级别、格式化消息、调用日志方法这一系列重复操作,减少了代码冗余。

2、支持可变参数与格式化输出

日志打印通常需要支持类似printf的格式化字符串(如LOG_INFO("用户 %s 年零 %d", username,age)),这要求接口能接收数量不确定的参数。

宏定义通过__VA_ARGS__天然支持可变参数,它能将多个参数传递给snprintf等函数可直接实现格式化功能。即使普通函数也可实现,但是存在额外开销。

《宏定义》-CSDN博客

3、条件编译实现日志开关

LOG_DEBUG需要在调试环境开启、发布环境关闭,宏定义通过条件编译指令轻松实现(预处理阶段就被替换,不会产生任何运行时开销)。若用普通函数,即使通过条件判断跳过日志逻辑,函数调用的开销扔可能存在,且代码无法被完全移除,占用二进制体积。


我们先来进行CMake编译一下。

编译成功,并把libmymuduo.so动态库放在了lib目录下。


主要实现的就是一个日志类。更多的是融合之前学过的C++知识以及在一个具体项目中的应用。比如涉及到的继承、explicit关键字、static关键字、const关键字、单例模式、宏定义等知识。

http://www.dtcms.com/a/407317.html

相关文章:

  • 开发避坑指南(58):Java Stream 按List元素属性分组实战指南
  • 郑州专门做网站的公司wordpress主题移植
  • Pinia 核心概念详解:Store, State, Getter, Action
  • Redis 64字节分界线与跳表实现原理
  • 网站租用价格wordpress后台打开太慢
  • Kanass入门到实战(3) - 如何进行需求管理
  • Java Web项目开发实战实战指南与实战技巧
  • 基于SiC的60kW LLC变换器采用新型变压器设计
  • CSP-J初赛试题之一
  • pip下载失败-python的pip镜像源修改为国内镜像源
  • 网站开发列表名人朋友圈网页版qq登录入口
  • Jenkins Pipeline 的 `sh` 步骤里使用 ‘‘‘ ... ‘‘‘和 “““ ... “““ 的区别,一篇文章搞定
  • 金融分析师职场学习技能提升方法分享
  • 网站打包app网站备案是需要去哪里做
  • YOLOv8深度解析:从架构革新到应用实践
  • CICD流程建设之持续测试实践指南
  • 津做网站嘉兴建设企业网站
  • 广州做购物网站平面设计培训班要学多久
  • 【复习】计网每日一题--ALOHA
  • 状态机模式:用Python Enum和字典.get()构建健壮的状态管理系统
  • 悬线法,dp 求解 P4147 玉蟾宫
  • 网站建设 北京wordpress关闭站点
  • 云南建投第十建设有限公司网站商城网站开发哪家好
  • 移动固态硬盘插入电脑后提示“需要格式化”或“文件系统损坏”如何修复?
  • ErrorProne 详解
  • 理解 Elasticsearch 中的分块策略
  • 政务服务网站建设整改报告想建个购物网站
  • 网站建设中如何设置外链接网站开发在哪里接活
  • SpringCloud与微服务
  • 织梦网站统计代码app编写软件