Muduo网络库解析---基础模块

前言

重写Muduo库实现核心模块的Git仓库

注:本文将重点剖析 Muduo 网络库的核心框架,深入探讨作者精妙的代码设计思路,并针对核心代码部分进行重写,将原本依赖 boost 的实现替换为原生的 C++11 语法。需要说明的是,本文并不打算对整个 Muduo 库进行完整的重写。Muduo库源码链接

在前文中,我们已经对 Muduo 的整体架构设计有了初步了解(如果还未阅读过的同学可先参考这篇文章进行回顾)。接下来,我们将深入探讨 Muduo 的各个基础模块,包括:

  • Logger
  • Timestamp
  • Buffer

其中,LoggerTimestamp 是其他核心模块经常依赖的基础组件,分别用于:

  • 记录日志信息
  • 封装时间戳

如果您的时间有限,可以先了解它们的功能,在掌握核心模块的原理后再回头深入理解这两个基础模块的实现也为时不晚。

至于 Buffer 模块,它是为每个客户端连接提供接收与发送数据的缓冲区空间。作者在实现该模块时进行了一些创新手法,务必对其设计思路有深入的理解。

Timestamp

在高性能网络服务器及其相关组件的运行过程中,精准的时间信息对故障定位、性能分析以及日志记录都至关重要。Muduo 中的 Timestamp 模块正是为了解决这一需求而设计的。

Timestamp 使用私有成员变量 microSecondsSinceEpoch_(类型为 time_t)来记录时间戳,并通过以下两项接口向外提供服务:

  • now():公有的静态方法,用于获取当前的高精度时间戳。
  • ToString():公有方法,将内部记录的时间戳转换为可读性更高的字符串格式,方便日志记录和调试。

凭借这一设计,Timestamp 模块为 Logger 提供了高效、简洁的时间戳获取接口,使日志记录能够更准确地体现事件发生的时间点。

cpp 复制代码
#include <unistd.h>
#include <iostream>
#include <string>
#include <time.h>

class Timestamp
{
public:
    Timestamp() : microSecondsSinceEpoch_(0) {}

    explicit Timestamp(long microSecondsSinceEpoch) : microSecondsSinceEpoch_(microSecondsSinceEpoch) {}

    ~Timestamp() = default;

    static Timestamp now(){
        time_t tm = time(NULL);
        return Timestamp(tm);
    }

    std::string ToString(){
        struct tm* localtm = localtime(&microSecondsSinceEpoch_);
        char buf[128] = {0};
        snprintf(buf, 128, "%04d/%02d/%02d %02d:%02d:%02d ", 
            localtm->tm_year + 1900, 
            localtm->tm_mon  + 1,
            localtm->tm_mday,
            localtm->tm_hour,
            localtm->tm_min,
            localtm->tm_sec);
        return std::string(buf);
    }
private:
    time_t microSecondsSinceEpoch_;
};

Logger

Logger类位于<muduo/base/Logging.h>文件内,muduo::net的命名空间内。它是整个框架的基础工具之一,主要负责在服务器运行过程中打印日志、提供运行时信息。通常如以下方式使用:

cpp 复制代码
LOG_INFO << "This is a info message" << some_variable;
LOG_ERROR << "Error occurred: " << error_code;

项目中,我们将原先的流式日志输出方式替换为类似 print 函数的格式化打印模式,例如:

cpp 复制代码
LOG_INFO("This is a info message %s", some_variable);
LOG_ERROR("Error occurred: %d", error_code)

为了确保 Logger 在程序运行期间始终只有单一实例,Muduo 对其进行了单例化设计。实现方式如下:

  • 通过继承 noncopyable 类,禁止拷贝与复制操作;
  • 将构造函数设为私有,以防止在类外部创建新的实例。

noncopyable

noncopyable类唯一的作用就是:

  • 不允许在类外构造和析构
  • 继承noncopyable的类,不允许构造拷贝和拷贝赋值
cpp 复制代码
class noncopyable
{
public:
    noncopyable(const noncopyable&) = delete;
    noncopyable& operator=(const noncopyable&) = delete;
protected:
    noncopyable() = default;
    ~noncopyable() = default;
};

解析:

  1. 为什么构造函数和析构函数被设为 protected
  • 通过将构造函数和析构函数设为 protected,派生类的构造函数可以直接调用基类构造函数,从而实现继承关系中更灵活的构造流程。
  • 同时,这种访问控制策略也确保了类外部无法直接实例化该类,既保证了类的可扩展性,又避免了不当使用带来的问题。

Logger代码

cpp 复制代码
enum LogLevel{
    DEBUG,
    INFO,
    ERROR,
    FATAL
};

class Logger : noncopyable
{
public:
    ~Logger() = default;

    void setLogLevel(LogLevel loglevel);

    void Log(const std::string& msg) const;

    static Logger& GetInstance();
private:
    LogLevel loglevel_;
    Logger() = default;
};

解析:

LogLevel 是用于定义日志级别的枚举类型,包括 DEBUGINFO 等等级。

Logger 中,loglevel_ 是一个私有成员变量,用于表示当前的日志等级设置。面向外部使用时,它提供了三个公有接口:

  • GetInstance():静态方法,用于获取单例模式下的唯一 Logger 实例。在 C++11 标准中,对 static 变量的初始化已实现线程安全。这意味着当多个线程同时访问一个带有 static 修饰的变量时,编译器会确保该变量仅在第一次使用时被正确构造一次,不会出现重复实例化的问题。
cpp 复制代码
Logger& Logger::GetInstance()
{
    static Logger logger;
    return logger;
}
  • setLogLevel(LogLevel loglevel):用于设置当前的日志级别。
cpp 复制代码
void Logger::setLogLevel(LogLevel loglevel)
{
    loglevel_ = loglevel;
}
  • Log(const std::string& msg) const:根据当前的日志级别 loglevel_ 来输出相应的日志信息。
    日志信息格式为:[日志等级] + 当前时间戳字符串格式化 + 要记录的日志信息\n
cpp 复制代码
void Logger::Log(const std::string& msg) const
{
    switch (loglevel_)
    {
    case(LogLevel::DEBUG):
        std::cout << "[DEBUG] ";
        break;
    case(LogLevel::INFO):
        std::cout << "[INFO] ";
        break;
    case(LogLevel::ERROR):
        std::cout << "[ERROR] ";
        break;
    case(LogLevel::FATAL):
        std::cout << "[FATAL] ";
        break;
    }

    std::cout << "time : " << Timestamp::now().ToString() << msg << std::endl;
}

在使用 Logger 时,如果严格按照下述步骤进行会显得相对繁琐:

  1. 通过 GetInstance() 获取实例
  2. 调用 Log 方法记录日志
  3. 再次通过 GetInstance() 获取实例

为简化这一过程,Muduo 通过定义宏来直接记录日志,从而避免了上述重复步骤。

cpp 复制代码
// ##__VA_ARGS__表示可变参数
// do..while(0)

#define LOG_INFO(LogmsgFormat, ...) \
    do  \
    {   \
        Logger& logger = Logger::GetInstance(); \
        logger.setLogLevel(LogLevel::INFO); \
        char buf[512] = {0};    \
        snprintf(buf, 512, LogmsgFormat, ##__VA_ARGS__);\
        logger.Log(buf);\
    }   \
    while(0)

#define LOG_FATAL(LogmsgFormat, ...) \
    do  \
    {   \
        Logger& logger = Logger::GetInstance(); \
        logger.setLogLevel(LogLevel::FATAL); \
        char buf[512] = {0};    \
        snprintf(buf, 512, LogmsgFormat, ##__VA_ARGS__);\
        logger.Log(buf);\
        exit(-1);    \
    }   \
    while(0)


#define LOG_DEBUG(LogmsgFormat, ...) \
    do  \
    {   \
        Logger& logger = Logger::GetInstance(); \
        logger.setLogLevel(LogLevel::DEBUG); \
        char buf[512] = {0};    \
        snprintf(buf, 512, LogmsgFormat, ##__VA_ARGS__);\
        logger.Log(buf);\
    }   \
    while(0)


#define LOG_ERROR(LogmsgFormat, ...) \
    do  \
    {   \
        Logger& logger = Logger::GetInstance(); \
        logger.setLogLevel(LogLevel::ERROR); \
        char buf[512] = {0};    \
        snprintf(buf, 512, LogmsgFormat, ##__VA_ARGS__);\
        logger.Log(buf);\
    }   \
    while(0)

解析

  1. ##__VA_ARGS__用来表示可变参数...
  2. 为什么在宏定义中使用do{...}while(0)这样的语法,大家可以参考这篇文章,讲的十分透彻。总的来说就是防止宏展开时出现语法问题。

其他单例模式写法

在这里列出,我经常使用的单例模式写法:

cpp 复制代码
#include <memory>
#include <mutex>
 
using namespace std;                                                                                                                                                          
template <typename T>
class Singleton {
protected:
	Singleton() = default;
	Singleton(const Singleton<T>&) = delete;
	Singleton& operator=(const Singleton<T>& st) = delete;
  
	static std::shared_ptr<T> _instance;
    
public:
	static std::shared_ptr<T> GetInstance() {
		static std::once_flag s_flag; // 定义一个静态的 once_flag,用于保证只执行一次初始化
		/*
		std::call_once 接受一个 once_flag 和一个可调用对象(本例中为 lambda 表达式),确保该可调用对象			在多线程环境中只会被调用一次。无论多少个线程同时调用 GetInstance() 函数,只有第一个调用成功执行该 	 		 lambda,其余线程在std::call_once 上都会等待直到初始化完成。
        */
        std::call_once(s_flag, [&]() {
			_instance = shared_ptr<T>(new T);
		});	
		return _instance;
	}
     
	~Singleton() = default;   
};
 
template <typename T>
std::shared_ptr<T> Singleton<T>::_instance = nullptr;

解析:

该类包含一个 静态成员变量 _instance(类型为 std::shared_ptr<T>),并通过下述策略实现单例模式:

  • 将构造函数设为 protected,以便派生类能够调用基类构造函数,同时避免在类外直接实例化基类;

  • 删除拷贝构造函数与拷贝赋值操作符,防止通过拷贝产生额外实例;

  • 提供一个静态公有方法 GetInstance() 用于对外获取唯一的类实例。

在使用此类时需特别注意以下几点:

  1. 继承时替换模板类型 :在定义派生类时,需要将模板参数 <T> 替换为当前的派生类类型。
  2. 私有构造函数:将派生类的构造函数设为私有,以确保无法在类外部直接实例化派生类对象,维持单例特性。
  3. 友元关系 :将 Singleton 类声明为派生类的友元类(friend class Singleton;),这样 Singleton 内部的代码才能合法调用派生类的私有构造函数,确保唯一实例的安全创建。

Buffer类

Muduo 框架中的 Buffer 类主要用于在网络通信过程中管理数据的接收与发送缓冲区。

  • 为什么要设计缓冲区?
  • Muduo缓冲区是如何设计的?
  • 其创新点在哪里?

大家可以阅读一下Muduo原作者陈硕老师所写的一篇博客地址

里面很详细的介绍了这几个问题,尤其是如何设计的?第一次接触网络框架的小伙伴们可能会有疑问(也包括我):

  • 以为只有接收缓冲区而没有发送缓冲区;
  • 为什么在Buffer::readFd函数里明明是从内核缓冲区读取数据,但是会从writeIndex开始写数据
  • Buffer里的writeIndexreadIndex是什么含义?以及计算公式...

读完陈硕老师的那篇博客,相信你会对Buffer的设计更加透彻。

我自己也单独写了一篇关于Muduo Buffer的设计原理并详细介绍了readFd成员成员函数,如果时间紧迫大家也可以去看一下地址

此项目的目标是实现 Muduo 库的核心功能,并不打算对 Muduo 的全部代码进行完整梳理和重写。换言之,这里只会呈现为完成核心模块所需的精简代码示例。

与原版 Muduo 相比,此项目中额外封装了一个 Buffer::writeFd 函数,用于将缓冲区的内容通过 write 系统调用写入内核发送缓冲区。在原版实现中,这一过程是直接调用 write 完成的,并没有独立的函数封装。为了使其与现有的 readFd 函数在功能和形式上保持对称性,我们增加了 writeFd 函数。

cpp 复制代码
/// A buffer class modeled after org.jboss.netty.buffer.ChannelBuffer
///
/// @code
/// +-------------------+------------------+------------------+
/// | prependable bytes |  readable bytes  |  writable bytes  |
/// |                   |     (CONTENT)    |                  |
/// +-------------------+------------------+------------------+
/// |                   |                  |                  |
/// 0      <=      readerIndex   <=   writerIndex    <=     size
/// @endcode
class Buffer
{
public:
    static const std::size_t kCheapPrepend = 8; // 存储额外的元数据
    static const std::size_t kInitialSize = 1024;

    explicit Buffer(std::size_t initialSize = kInitialSize) : 
        buffer_(kCheapPrepend + initialSize),
        readerIndex_(kCheapPrepend),
        writerIndex_(kCheapPrepend)
    {  }

    // 返回可读字节大小
    size_t readableBytes() const { return writerIndex_ - readerIndex_; }
    // 返回可写字节大小
    size_t writableBytes() const { return buffer_.size() - writerIndex_; }
    // 返回额外存储元数据大小
    size_t prependableBytes() const { return readerIndex_; }

    // 确认写缓冲区的大小满足写 len 字节的长度,不够需要扩容
    void ensureWritableBytes(std::size_t len)
    {
        if(writableBytes() < len)
        {
            makeSpace(len);
        }
    }

    // 把[data, data+len)内存上的数据添加到writable缓冲区中
    void append(const char* data, std::size_t len)
    {
        ensureWritableBytes(len);
        std::copy(data, data+len, beginWrite());
        writerIndex_ += len;
    }

    char* beginWrite()
    {   return begin() + writerIndex_; }

    const char* beginWrite() const
    { return begin() + writerIndex_; }

    // OnMessage string <- Buffer
    // 移动readerIndex,丢弃指定长度len的数据
    void retrieve(size_t len)
    {
        if(len < readableBytes())//丢弃长度小于可读字节长度
        {
            readerIndex_ += len;// 应用只读取了缓冲区的一部分
        }
        else
        {
            retrieveAll();
        }
    }
    
    // 把OnMessage函数上报的Buffer数据,转成string类型返回
    std::string retrieveAllAsString()
    {
        return retrieveAsString(readableBytes());        
    }

    // 读取指定字节
    std::string retrieveAsString(size_t len)
    {
        std::string result(peek(), len);
        retrieve(len);
        return result;
    }
    
    ssize_t readFd(int fd, int* saveErrno);
    ssize_t writeFd(int fd, int* saveErrno);

private:
    char* begin()
    { return &*buffer_.begin(); }

    const char* begin() const 
    { return &*buffer_.begin(); }

    // 返回可读数据的起始地址
    const char* peek() const
    { return begin() + readerIndex_; }

    // 相当于复位操作
    void retrieveAll()
    {
        readerIndex_ = writerIndex_ = kCheapPrepend;
    }

    // 缓冲区剩余空间不足时,重新分配或整理空间。
    void makeSpace(std::size_t len)
    {
        if(writableBytes() + prependableBytes() < len + kCheapPrepend)
        {
            buffer_.resize(writerIndex_ + len); 
        }
        else
        {
            size_t readable = readableBytes();
            std::copy(begin() + readerIndex_,
                    begin() + writerIndex_,
                    begin() + kCheapPrepend);
            readerIndex_ = kCheapPrepend;
            writerIndex_ = readerIndex_ + readable;
        }
    }

    std::vector<char> buffer_; // 存储实际数据的缓冲区
    std::size_t readerIndex_;   // 读指针,指向可读取数据的起始位置
    std::size_t writerIndex_;   // 写指针,指向可写入数据的起始位置
};
cpp 复制代码
// 从从fd上读取数据 Poller工作在LT模式
// 如果此次数据没有读完,下一次接着触发读就绪
ssize_t Buffer::readFd(int fd, int *savenoErrno)
{
    char extrabuf[65536] = {0};
    struct iovec vec[2];

    const std::size_t writable = writableBytes();// Buffer底层缓冲区可写的空间大小
    vec[0].iov_base = begin() + writerIndex_; 
    vec[0].iov_len = writable;

    vec[1].iov_base = extrabuf;
    vec[1].iov_len = sizeof extrabuf;

    const int iovcnt = (writable < sizeof extrabuf) ? 2 : 1;
    const ssize_t n = readv(fd, vec, iovcnt);
    if(n < 0)
    {
        *savenoErrno = errno;
    }
    else if(n <= writable)
    {
        writerIndex_ += n;
    }
    else
    {
        writerIndex_ = buffer_.size();
        append(extrabuf, n - writable);
    }
    return n;
}


ssize_t Buffer::writeFd(int Fd, int *saveErrno)
{
    ssize_t n = ::write(Fd, peek(), readableBytes());
    if(n > 0)
    {
        *saveErrno = errno;
    }
    return n;
}
相关推荐
人间打气筒(Ada)39 分钟前
MySQL主从架构
服务器·数据库·mysql
落笔画忧愁e2 小时前
FastGPT快速将消息发送至飞书
服务器·数据库·飞书
小冷爱学习!2 小时前
华为动态路由-OSPF-完全末梢区域
服务器·网络·华为
Dream it possible!2 小时前
LeetCode 热题 100_在排序数组中查找元素的第一个和最后一个位置(65_34_中等_C++)(二分查找)(一次二分查找+挨个搜索;两次二分查找)
c++·算法·leetcode
柠石榴2 小时前
【练习】【回溯No.1】力扣 77. 组合
c++·算法·leetcode·回溯
王老师青少年编程2 小时前
【GESP C++八级考试考点详细解读】
数据结构·c++·算法·gesp·csp·信奥赛
技术小齐2 小时前
网络运维学习笔记 016网工初级(HCIA-Datacom与CCNA-EI)PPP点对点协议和PPPoE以太网上的点对点协议(此处只讲华为)
运维·网络·学习
落幕3 小时前
C语言-进程
linux·运维·服务器
澄澈天空4 小时前
C++ MFC添加RichEditControl控件后,程序启动失败
c++·mfc
shimly1234564 小时前
tcpdump 用法示例
网络·测试工具·tcpdump