2026-05-28 日志库完整实现与详解

文件清单

Mutex.hpp    — 互斥锁 + RAII 锁守卫
Logger.hpp   — 日志系统(策略模式 + LogMessage 临时对象自动刷出)

一、Mutex.hpp

完整代码

#pragma once                                    // 防止头文件被重复包含
#include <pthread.h>                            // POSIX 线程库:pthread_mutex_t 及相关函数

// ═══════════════════════════════════════════════
// Mutex — 互斥锁的 C++ 封装
// 把 C 风格的 pthread_mutex_t 包装成 C++ 对象
// 构造时 init,析构时 destroy,自动管理锁的生命周期
// ═══════════════════════════════════════════════
class Mutex
{
private:
    pthread_mutex_t _lock;                      // 内核中真正的互斥锁变量

public:
    // 构造:初始化锁
    // pthread_mutex_init 第一个参数是锁的地址,第二个 nullptr = 默认属性(普通锁)
    Mutex()
    {
        pthread_mutex_init(&_lock, nullptr);
    }

    // 析构:销毁锁
    // 保证 Mutex 对象销毁时,内核中的锁资源也被回收
    ~Mutex()
    {
        pthread_mutex_destroy(&_lock);
    }

    // 加锁
    // 如果锁空闲 → 立刻拿到锁,继续执行
    // 如果锁被别人持有 → 阻塞等待,直到对方解锁,内核把自己唤醒
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }

    // 解锁
    // 释放锁。如果有其他线程在 lock() 里等着,内核会挑一个唤醒
    void Unlock()
    {
        pthread_mutex_unlock(&_lock);
    }

    // 获取原始 pthread_mutex_t 指针
    // 用途:某些 API(如 pthread_cond_wait)需要 pthread_mutex_t*,通过这个方法暴露出去
    pthread_mutex_t *Get() { return &_lock; }
};

// ═══════════════════════════════════════════════
// LockGuard — RAII 锁守卫
// 构造时自动加锁,析构时自动解锁
// 保证"不管函数怎么退出,锁一定会被释放"
// ═══════════════════════════════════════════════
class LockGuard
{
private:
    Mutex *_mutex;                              // 指向要管理的锁(不拥有所有权,只是借用)

public:
    // 构造:保存锁的指针,然后立刻加锁
    // 初始化列表先存指针,函数体再 Lock,保证顺序正确
    LockGuard(Mutex *mutex) : _mutex(mutex)
    {
        _mutex->Lock();
    }

    // 析构:自动释放锁
    // 不管函数是正常 return 还是抛异常,C++ 保证局部对象必定析构 → 锁必定释放
    ~LockGuard()
    {
        _mutex->Unlock();
    }

    // 禁止拷贝
    // 如果允许拷贝:两个 LockGuard 指向同一把锁 → 第一个析构 Unlock → 第二个析构再 Unlock
    // → 重复解锁,未定义行为
    LockGuard(const LockGuard&) = delete;
    LockGuard& operator=(const LockGuard&) = delete;
};

设计要点

为什么要封装 pthread_mutex_t?

C 风格的原生用法容易出错:

// C 风格 — 容易忘记 init/destroy/lock/unlock 中的一个
pthread_mutex_t lock;
pthread_mutex_init(&lock, nullptr);   // 容易忘
pthread_mutex_lock(&lock);
// ... 临界区 ...
pthread_mutex_unlock(&lock);          // 容易忘
pthread_mutex_destroy(&lock);         // 容易忘

封装后用 C++ 对象生命周期自动管理:

Mutex lock;                          // 构造 → init
{
    LockGuard guard(&lock);          // 构造 → lock
    // ... 临界区 ...
}                                    // guard 析构 → unlock
                                     // lock 析构 → destroy

为什么有两个类(Mutex + LockGuard)?

这两个类职责不同:

Mutex LockGuard
角色 锁本身(资源) 锁的临时持有者(管家)
生命周期 和拥有它的对象一样长 一次临界区的作用域
负责 init / destroy lock / unlock

类比:

  • Mutex = 你家的大门锁,一直装在门上
  • LockGuard = 拿钥匙开门进去、出来锁门的那个人

RAII 的本质

构造 → 获取资源(init / lock / new)
析构 → 释放资源(destroy / unlock / delete)

C++ 保证:局部对象离开作用域必定析构
→ 资源必定释放,不会泄露

二、Logger.hpp

完整代码

#pragma once

// ====== 头文件 ======
#include <string>                   // std::string
#include <iostream>                 // std::cout, std::cerr, std::endl
#include <filesystem>               // C++17 文件系统:exists(), create_directories()
#include <fstream>                  // std::ofstream 文件输出流
#include <memory>                   // std::unique_ptr, std::make_unique
#include <unistd.h>                 // getpid() 获取进程ID
#include <ctime>                    // time_t, localtime_r 时间处理
#include <sstream>                  // std::stringstream 字符串拼接
#include "Mutex.hpp"                // 我们自己的锁封装

// ═══════════════════════════════════════════════
// LogLevel — 日志等级(强类型枚举)
// ═══════════════════════════════════════════════
enum class LogLevel
{
    DEBUG,      // 调试信息:开发阶段用的详细日志
    INFO,       // 正常运行信息:服务启动、新连接等
    WARNING,    // 警告:非致命但需关注的情况
    ERROR,      // 错误:功能受影响但程序还能跑
    FATAL       // 致命错误:程序即将崩溃
};

// ═══════════════════════════════════════════════
// Level2String — 把枚举等级转成人类可读的字符串
// ═══════════════════════════════════════════════
std::string Level2String(LogLevel level)
{
    switch (level)
    {
    case LogLevel::DEBUG:   return "Debug";
    case LogLevel::INFO:    return "Info";
    case LogLevel::WARNING: return "Warning";
    case LogLevel::ERROR:   return "Error";
    case LogLevel::FATAL:   return "Fatal";
    default:                return "Unknown";
    }
}

// ═══════════════════════════════════════════════
// GetCurrentTime — 获取当前时间,格式化成字符串
// 输出格式:"2026-05-28 19:45:30"
// ═══════════════════════════════════════════════
std::string GetCurrentTime()
{
    // 1. 拿到 Unix 时间戳(从 1970-01-01 到现在的秒数)
    time_t currtime = time(nullptr);

    // 2. 把时间戳拆成年月日时分秒
    //    localtime_r 是线程安全版本(_r = reentrant 可重入)
    struct tm currtm;
    localtime_r(&currtime, &currtm);

    // 3. 格式化成字符串
    //    %4d:占 4 位整数(年份)
    //    %02d:占 2 位,不足前面补 0(月份、日期、时、分、秒)
    //    tm_year 从 1900 起算 → +1900
    //    tm_mon  从 0 起算(0=一月)→ +1
    char timebuffer[64];
    snprintf(timebuffer, sizeof(timebuffer),
             "%4d-%02d-%02d %02d:%02d:%02d",
             currtm.tm_year + 1900,
             currtm.tm_mon + 1,
             currtm.tm_mday,
             currtm.tm_hour,
             currtm.tm_min,
             currtm.tm_sec);
    return timebuffer;
}

// ═══════════════════════════════════════════════
// LogStrategy — 抽象基类(策略模式)
// 定义"日志刷到哪去"的接口
// 纯虚函数 SyncLog 强制子类必须实现
// ═══════════════════════════════════════════════
class LogStrategy
{
public:
    // 虚析构:父类指针 delete 子类对象时,能正确调用子类的析构函数
    virtual ~LogStrategy() = default;

    // 纯虚函数:子类必须重写。参数是已经拼好的完整日志字符串
    virtual void SyncLog(const std::string &logmessage) = 0;
};


// ═══════════════════════════════════════════════
// ConsoleLogStrategy — 控制台输出策略
// 把日志刷到标准输出(显示器)
// ═══════════════════════════════════════════════
class ConsoleLogStrategy : public LogStrategy
{
private:
    Mutex _lock;                                // 多线程同时写控制台时需要加锁
                                                // 否则两条日志会交错在一起,变成乱码

public:
    void SyncLog(const std::string &logmessage) override
    {
        // 构造加锁,出作用域自动解锁
        LockGuard lockguard(&_lock);
        std::cout << logmessage << std::endl;
    }
};


// ═══════════════════════════════════════════════
// FileLogStrategy — 文件输出策略
// 把日志追加写入磁盘文件
// ═══════════════════════════════════════════════
class FileLogStrategy : public LogStrategy
{
private:
    Mutex _lock;                                // 保护文件操作(多线程同时写同一文件)
    std::string _dir_path_name;                 // 日志目录路径,如 "/var/log/"
    std::string _filename;                      // 日志文件名,如 "test.log"

public:
    // 构造:指定日志目录和文件名(有默认值)
    FileLogStrategy(const std::string &dir = "/var/log/",
                    const std::string &filename = "test.log")
        : _dir_path_name(dir), _filename(filename)
    {
        // 创建目录时需要加锁,防止多线程同时 mkdir 导致竞态
        LockGuard lockguard(&_lock);

        // 如果目录已存在,什么都不做
        if (std::filesystem::exists(_dir_path_name))
        {
            return;
        }

        // 递归创建目录(等价于 mkdir -p)
        // 用 try-catch 包住,创建失败不会崩,只是文件日志功能不可用
        try
        {
            std::filesystem::create_directories(_dir_path_name);
        }
        catch (const std::filesystem::filesystem_error &e)
        {
            std::cerr << "创建日志目录失败: " << e.what() << std::endl;
        }
    }

    // 线程安全的文件写入
    void SyncLog(const std::string &logmessage) override
    {
        // 加锁保护:防止多线程同时打开/写入/关闭同一文件
        LockGuard lockguard(&_lock);

        // 拼出完整路径:目录 + "/" + 文件名
        std::string target = _dir_path_name + "/" + _filename;

        // std::ios::app = append 模式:每次写入追加到文件末尾,不覆盖已有内容
        std::ofstream out(target, std::ios::app);
        if (!out.is_open())                     // 打开失败就返回,别崩
        {
            return;
        }
        out << logmessage << std::endl;         // 写入 + 换行
        out.close();                            // 显式关闭(不写也会在析构时关)
    }
};


// ═══════════════════════════════════════════════
// Logger — 日志主类(策略模式的上下文)
// 持有输出策略,通过 operator() 创建临时 LogMessage
// ═══════════════════════════════════════════════
class Logger
{
public:
    Logger() {}

    // 切换到控制台输出
    void EnableConsoleLogStrategy()
    {
        // make_unique 在堆上分配 ConsoleLogStrategy,所有权交给 _strategy
        _strategy = std::make_unique<ConsoleLogStrategy>();
    }

    // 切换到文件输出
    void EnableFileLogStrategy()
    {
        _strategy = std::make_unique<FileLogStrategy>();
    }

    // ═══════════════════════════════════════════════
    // LogMessage — 日志消息临时对象
    // 生命周期 = 一行日志的拼接过程
    // 构造时拼日志头 → operator<< 拼内容 → 析构时刷出
    // ═══════════════════════════════════════════════
    class LogMessage
    {
    public:
        // 构造:生成日志头并存入 _loginfo
        LogMessage(LogLevel level, std::string filename, int line, Logger &logger)
            : _curr_time(GetCurrentTime()),     // 拿到当前时间
              _level(level),                    // 日志等级
              _pid(getpid()),                   // 当前进程 PID
              _filename(filename),              // 源文件名(通过 __FILE__ 传入)
              _line(line),                      // 行号(通过 __LINE__ 传入)
              _logger(logger)                   // 记住属于哪个 Logger(引用,必须初始化列表绑定)
        {
            // 拼出日志头:
            // [2026-05-28 19:45:30][Info][12345][server.cc][42] - 
            // 时间           等级   进程号 文件       行号   分隔符
            std::stringstream ss;
            ss << "[" << _curr_time << "]"
               << "[" << Level2String(_level) << "]"
               << "[" << _pid << "]"
               << "[" << _filename << "]"
               << "[" << _line << "] - ";
            _loginfo = ss.str();
        }

        // operator<< 模板:接受任意类型,转成字符串追加到日志后面
        // 返回 LogMessage&(自己的引用)以支持链式调用
        template <typename T>
        LogMessage &operator<<(const T &info)
        {
            std::stringstream ss;
            ss << info;                          // int/double/string 等自动转字符串
            _loginfo += ss.str();               // 追加到日志尾部
            return *this;                        // 返回自己,让下一个 << 继续拼接
        }

        // 析构:整条日志拼完了,刷出去!
        // RAII 的应用:不管怎么退出,日志一定被刷出
        ~LogMessage()
        {
            // _strategy 可能为空(没调 EnableXxx()),检查一下
            if (_logger._strategy)
            {
                _logger._strategy->SyncLog(_loginfo);
            }
            // 如果 _strategy 是 nullptr,这条日志就丢了(静默丢弃,不崩)
        }

    private:
        std::string _curr_time;     // 日志产生的时间
        LogLevel _level;            // 日志等级
        pid_t _pid;                 // 进程 PID(区分多进程)
        std::string _filename;      // 源文件名
        int _line;                  // 行号
        std::string _loginfo;       // 已拼好的完整日志字符串(头 + 体)
        Logger &_logger;            // 指向 Logger 的引用(析构时用它刷出)
    };

    // operator() — 仿函数
    // 让 Logger 对象可以被"调用",返回一个临时 LogMessage
    // 用法:logger(LogLevel::INFO, "server.cc", 42)
    LogMessage operator()(LogLevel level, std::string filename, int line)
    {
        return LogMessage(level, filename, line, *this);   // *this = Logger 自己
    }

private:
    // 输出策略:指向 ConsoleLogStrategy 或 FileLogStrategy 或 nullptr(未设置)
    // unique_ptr 保证 Logger 析构时自动 delete 策略对象
    std::unique_ptr<LogStrategy> _strategy;
};


// ═══════════════════════════════════════════════
// 全局 Logger 声明 + 便捷宏
// ═══════════════════════════════════════════════

// extern 声明:告诉编译器"logger 存在,但定义在某个 .cpp 文件中"
// 这样多个 .cpp include 此头文件时不会重复定义
extern Logger logger;

// LOG(等级) << "内容";
// 展开后:logger(LogLevel::INFO, __FILE__, __LINE__) << "内容";
// __FILE__:编译器替换为当前源文件名
// __LINE__:编译器替换为当前行号
#define LOG(level) logger(level, __FILE__, __LINE__)

// 快捷设置策略的宏
#define EnableConsoleLogStrategy() logger.EnableConsoleLogStrategy()
#define EnableFileLogStrategy()    logger.EnableFileLogStrategy()

三、完整执行流程(一行日志的生命周期)

以这一行为例:

EnableConsoleLogStrategy();
LOG(LogLevel::INFO) << "服务端启动, 端口: " << 8080;

时序分解

步骤0:初始化(程序启动时,只做一次)
─────────────────────────────────────────
EnableConsoleLogStrategy();
  └→ logger.EnableConsoleLogStrategy()
       └→ _strategy = make_unique<ConsoleLogStrategy>()
            ConsoleLogStrategy 在堆上创建,_strategy 持有它


步骤1:LOG 宏展开
─────────────────────────────────────────
LOG(LogLevel::INFO) << "服务端启动, 端口: " << 8080;
  │
  └→ logger(LogLevel::INFO, "server.cc", 42) << "服务端启动, 端口: " << 8080;


步骤2:operator() 调用 → 创建临时 LogMessage
─────────────────────────────────────────
logger(LogLevel::INFO, "server.cc", 42)
  └→ return LogMessage(INFO, "server.cc", 42, *this);

      LogMessage 构造:
        _curr_time = GetCurrentTime()              // "2026-05-28 19:45:30"
        _level     = LogLevel::INFO
        _pid       = getpid()                      // 12345
        _filename  = "server.cc"
        _line      = 42
        _logger    = *this                         // Logger 的引用

        拼日志头 _loginfo =
          "[2026-05-28 19:45:30][Info][12345][server.cc][42] - "


步骤3:operator<< 链式拼接内容
─────────────────────────────────────────
<< "服务端启动, 端口: "
  └→ operator<<(const char*)
       _loginfo += "服务端启动, 端口: "
       return *this;                               // 返回自己引用

<< 8080
  └→ operator<<(const int&)
       _loginfo += "8080"
       return *this;                               // 返回自己引用

此时 _loginfo =
  "[2026-05-28 19:45:30][Info][12345][server.cc][42] - 服务端启动, 端口: 8080"


步骤4:分号 → LogMessage 析构 → 刷出
─────────────────────────────────────────
";" 到了 → 临时 LogMessage 对象析构

~LogMessage():
  if (_logger._strategy)                           // 不是 nullptr ✓
    _logger._strategy->SyncLog(_loginfo)
      │
      └→ ConsoleLogStrategy::SyncLog()
           LockGuard lockguard(&_lock);            // 加锁
           std::cout << _loginfo << std::endl;     // 输出到控制台
           // lockguard 析构 → 解锁


控制台输出:
[2026-05-28 19:45:30][Info][12345][server.cc][42] - 服务端启动, 端口: 8080

四、为什么要加锁

不加锁会发生什么

假设两个线程同时写日志:

线程A: LOG(INFO) << "用户登录: " << user_id;
线程B: LOG(INFO) << "连接断开: " << fd;

时间线(不加锁):
  A 写入 "[2026-05-28...][Info] 用户登录: "
  B 切入,写入 "[2026-05-28...][Info] 连接断开: "
  A 继续写入 "42"
  B 继续写入 "5"

结果(两条日志搅在一起,无法阅读):
  [2026-05-28][Info] 用户登录: [2026-05-28][Info] 连接断开: 425

加锁后

线程A: LockGuard(&_lock) → 拿到锁 → 写入完整日志 → 解锁
线程B: LockGuard(&_lock) → 阻塞等待 → A 解锁后拿到锁 → 写入 → 解锁

结果(两条完整日志,先后输出):
  [2026-05-28][Info][12345][s.cc][10] - 用户登录: 42
  [2026-05-28][Info][12345][s.cc][15] - 连接断开: 5

锁在哪里

ConsoleLogStrategy::SyncLog()  — 保护 std::cout,多线程不会交叉输出
FileLogStrategy::SyncLog()     — 保护文件写入,多线程不会交叉写入
FileLogStrategy 构造函数       — 保护 mkdir,多线程不会同时创建目录

三个锁是独立的(每个策略对象有自己的 Mutex),不会互相阻塞。


五、知识点清单

序号 知识点 出现的类/函数
1 enum class 强类型枚举 LogLevel
2 虚函数 + = 0 纯虚函数 LogStrategy
3 override 显式重写 ConsoleLogStrategy, FileLogStrategy
4 虚析构函数 LogStrategy
5 RAII(资源获取即初始化) LockGuard, LogMessage
6 unique_ptr + make_unique Logger::_strategy
7 策略模式(Strategy Pattern) LogStrategy 体系
8 template<typename T> 模板函数 LogMessage::operator<<
9 operator() 重载(仿函数) Logger::operator()
10 operator<< 重载(链式调用) LogMessage::operator<<
11 析构函数自动触发 LogMessage::~LogMessage
12 = default / = delete LogStrategy ~, LockGuard 拷贝
13 extern 全局变量 extern Logger logger
14 __FILE__ / __LINE__ 预定义宏 LOG 宏
15 stringstream 字符串拼接 LogMessage 构造 + operator<<
16 snprintf 安全格式化 GetCurrentTime
17 std::filesystem(C++17) FileLogStrategy 构造
18 std::ofstream 文件输出流 FileLogStrategy::SyncLog
19 类成员引用必须初始化列表绑定 LogMessage::_logger
20 pthread_mutex_t 互斥锁 Mutex
21 localtime_r 线程安全时间转换 GetCurrentTime
22 getpid() 获取进程 PID LogMessage 构造

六、内存生命周期图

程序启动
│
├─ Logger logger 构造(全局,程序全程存活)
│   _strategy = nullptr
│
├─ EnableConsoleLogStrategy()
│   _strategy → new ConsoleLogStrategy(堆上,随 Logger 一起死亡)
│
├─ LOG(INFO) << "hello";
│   │
│   ├─ LogMessage 临时对象(栈上,这一行结束析构)
│   │   _logger → 指回 Logger
│   │   _loginfo = "拼好的日志字符串"
│   │
│   └─ ; → 析构 → SyncLog → 控制台输出
│
├─ LOG(WARNING) << "world";
│   │   另一个临时 LogMessage(新的,和上面那个无关)
│   └─ ; → 析构 → SyncLog → 控制台输出
│
程序结束
│
└─ logger 析构
     └─ _strategy(unique_ptr)析构 → delete ConsoleLogStrategy

关键:每个 LOG(xxx) << ... 创建一个全新的临时 LogMessage 对象,拼完就析构刷出。Logger 是唯一的长命对象。


七、测试代码

#include "Logger.hpp"

// 全局 Logger 的定义(整个程序只能有一处!)
Logger logger;

int main()
{
    // 1. 设置输出策略(必须调,否则 _strategy 为空不会输出)
    EnableConsoleLogStrategy();

    // 2. 写日志
    LOG(LogLevel::INFO)    << "服务端启动";
    LOG(LogLevel::DEBUG)   << "调试信息: sockfd=" << 3;
    LOG(LogLevel::WARNING) << "内存使用率超过 80%";
    LOG(LogLevel::ERROR)   << "连接超时: " << 30 << "秒";
    LOG(LogLevel::FATAL)   << "无法分配内存";

    // 3. 切换到文件输出
    EnableFileLogStrategy();
    LOG(LogLevel::INFO)    << "这条会写到 /var/log/test.log";

    return 0;
}

编译:

g++ test.cc -o test -std=c++17 -lstdc++fs -lpthread

-lpthread 是因为 Mutex.hpp 用了 pthread 库。


八、与项目的关系

这个日志库会用在后续所有模块中,把 std::cout << ...printf(...) 替换成 LOG(LogLevel::INFO) << ...

好处:

  1. 统一格式:每条日志都有时间、等级、PID、文件、行号
  2. 可以切输出:调试时刷控制台,上线时刷文件
  3. 线程安全:多线程 Server 不会日志交叉
  4. 可以按等级过滤:后续可以加 SetLogLevel(LogLevel::WARNING) 只输出 WARNING 及以上

更多推荐