《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实例,可能导致日志格式不一样(例如不同实例设置了不同日志级别、输出方式)
- 多实例也可能导致资源竞争(例如同时写入一个日志文件时的冲突)
- 全局需要一个“单一入口”来管理日志配置,避免混乱
所以,日志类天生适合设计成单例。全局只需要一个实例来协调所有日志操作。
- 静态instance()方法属于类本身,不依赖对象即可调用。获取单例“入口”。
- 内部通过静态局部变量(第一次调用instance时初始化,之后调用不再创建)实现唯一实例。
- 通过继承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关键字、单例模式、宏定义等知识。