解析muduo源码之 Buffer.h & Buffer.cc

目录

[一、 Buffer.h](#一、 Buffer.h)

[1. Buffer 类的整体定位与核心设计目标](#1. Buffer 类的整体定位与核心设计目标)

[2. 核心常量与成员变量解析](#2. 核心常量与成员变量解析)

[3. 核心接口逐模块拆解](#3. 核心接口逐模块拆解)

[1. 构造函数与基础查询接口](#1. 构造函数与基础查询接口)

[2. 数据读取 / 回收接口(retrieve 系列)](#2. 数据读取 / 回收接口(retrieve 系列))

[3. 数据写入 / 追加接口(append 系列)](#3. 数据写入 / 追加接口(append 系列))

[4. 核心内存管理:makeSpace(扩容 / 数据移动)](#4. 核心内存管理:makeSpace(扩容 / 数据移动))

[5. 预追加接口(prepend 系列)](#5. 预追加接口(prepend 系列))

[6. 协议解析辅助接口(查找 CRLF/EOL)](#6. 协议解析辅助接口(查找 CRLF/EOL))

[7. 网络字节序解析接口(peek/read 系列)](#7. 网络字节序解析接口(peek/read 系列))

[8. 内存优化接口](#8. 内存优化接口)

[9. fd 读取接口(核心网络操作)](#9. fd 读取接口(核心网络操作))

[4. 设计亮点总结](#4. 设计亮点总结)

总结

[二、 Buffer.cc](#二、 Buffer.cc)

[1. readFd 的核心设计目标](#1. readFd 的核心设计目标)

[2. 逐行拆解核心实现](#2. 逐行拆解核心实现)

[3. 关键细节深度解析](#3. 关键细节深度解析)

[1. readv(分散读)的核心作用](#1. readv(分散读)的核心作用)

[2. extrabuf 的设计巧思](#2. extrabuf 的设计巧思)

[3. iovcnt 的动态判断逻辑](#3. iovcnt 的动态判断逻辑)

[4. 不同读取结果的处理逻辑](#4. 不同读取结果的处理逻辑)

[5. 错误码保存机制](#5. 错误码保存机制)

[4. 核心设计亮点总结](#4. 核心设计亮点总结)

[5. 典型执行流程(Buffer 空间不足场景)](#5. 典型执行流程(Buffer 空间不足场景))

总结


一、 Buffer.h

先贴出完整代码,再逐部分解释:

cpp 复制代码
// Copyright 2010, Shuo Chen.  All rights reserved.
// http://code.google.com/p/muduo/
//
// 本源代码的使用受 BSD 风格许可证约束
// 该许可证可在 License 文件中查阅。

// 作者:陈硕 (chenshuo at chenshuo dot com)
//
// 这是一个公共头文件,仅允许包含公共头文件。

#ifndef MUDUO_NET_BUFFER_H
#define MUDUO_NET_BUFFER_H

#include "muduo/base/copyable.h"       // 可拷贝基类(空类,标记该类支持拷贝)
#include "muduo/base/StringPiece.h"    // 轻量级字符串视图(避免拷贝)
#include "muduo/base/Types.h"          // 基础类型定义(如 string)

#include "muduo/net/Endian.h"          // 字节序转换(主机序 <-> 网络序)

#include <algorithm>                   // 算法库(std::copy/std::search 等)
#include <vector>                      // 动态数组:存储缓冲区数据

#include <assert.h>                    // 断言(调试期检查逻辑正确性)
#include <string.h>                    // 内存操作(memchr/memcpy 等)
//#include <unistd.h>  // ssize_t(系统调用返回值类型,已在其他头文件中定义)

namespace muduo
{
namespace net
{

/// 缓冲区类:模仿 org.jboss.netty.buffer.ChannelBuffer 设计
///
/// 内存布局说明:
/// @code
/// +-------------------+------------------+------------------+
/// | 可前置字节区域    |  可读字节区域     |  可写字节区域     |
/// | (prependable)     |  (readable/CONTENT)| (writable)     |
/// +-------------------+------------------+------------------+
/// |                   |                  |                  |
/// 0      <=      readerIndex   <=   writerIndex    <=     size
/// @endcode
/// - prependable:可在可读区域前追加数据(默认 8 字节,用于存储长度/协议头)
/// - readable:已写入数据的区域,通过 readerIndex 读取,读完后移动索引
/// - writable:空闲区域,通过 writerIndex 写入,写完后移动索引
class Buffer : public muduo::copyable
{
 public:
  static const size_t kCheapPrepend = 8;   // 默认前置区域大小(8 字节,适配 64 位整数)
  static const size_t kInitialSize = 1024; // 默认初始可写区域大小(1024 字节)

  /// 构造函数:初始化缓冲区
  /// @param initialSize 初始可写区域大小(默认 1024 字节)
  explicit Buffer(size_t initialSize = kInitialSize)
    : buffer_(kCheapPrepend + initialSize),  // 缓冲区总大小 = 前置区 + 初始可写区
      readerIndex_(kCheapPrepend),           // 读索引初始指向前置区末尾
      writerIndex_(kCheapPrepend)            // 写索引初始指向前置区末尾
  {
    // 初始化断言:可读字节为 0,可写字节为初始大小,可前置字节为默认值
    assert(readableBytes() == 0);
    assert(writableBytes() == initialSize);
    assert(prependableBytes() == kCheapPrepend);
  }

  // 隐式拷贝构造、移动构造、析构和赋值运算符均可用
  // 注:隐式移动构造在 g++ 4.6 及以上版本支持

  /// 交换两个缓冲区的数据(浅交换,仅交换内部容器和索引)
  void swap(Buffer& rhs)
  {
    buffer_.swap(rhs.buffer_);                // 交换底层 vector 数据
    std::swap(readerIndex_, rhs.readerIndex_); // 交换读索引
    std::swap(writerIndex_, rhs.writerIndex_); // 交换写索引
  }

  /// 获取可读字节数(已写入未读取的数据长度)
  size_t readableBytes() const
  { return writerIndex_ - readerIndex_; }

  /// 获取可写字节数(空闲区域长度)
  size_t writableBytes() const
  { return buffer_.size() - writerIndex_; }

  /// 获取可前置字节数(读索引前的空闲区域长度)
  size_t prependableBytes() const
  { return readerIndex_; }

  /// 获取可读区域的起始地址(仅读)
  const char* peek() const
  { return begin() + readerIndex_; }

  /// 在可读区域中查找 CRLF(\r\n)
  /// @return 找到则返回指向 '\r' 的指针,未找到返回 NULL
  const char* findCRLF() const
  {
    // FIXME:考虑替换为 memmem() 提升查找效率
    const char* crlf = std::search(peek(), beginWrite(), kCRLF, kCRLF+2);
    return crlf == beginWrite() ? NULL : crlf;
  }

  /// 从指定位置开始查找 CRLF(\r\n)
  /// @param start 查找起始位置(必须在可读区域内)
  /// @return 找到则返回指向 '\r' 的指针,未找到返回 NULL
  const char* findCRLF(const char* start) const
  {
    assert(peek() <= start);          // 断言:起始位置 >= 可读区域起始
    assert(start <= beginWrite());    // 断言:起始位置 <= 可写区域起始
    // FIXME:考虑替换为 memmem() 提升查找效率
    const char* crlf = std::search(start, beginWrite(), kCRLF, kCRLF+2);
    return crlf == beginWrite() ? NULL : crlf;
  }

  /// 在可读区域中查找 EOL(\n)
  /// @return 找到则返回指向 '\n' 的指针,未找到返回 NULL
  const char* findEOL() const
  {
    const void* eol = memchr(peek(), '\n', readableBytes());
    return static_cast<const char*>(eol);
  }

  /// 从指定位置开始查找 EOL(\n)
  /// @param start 查找起始位置(必须在可读区域内)
  /// @return 找到则返回指向 '\n' 的指针,未找到返回 NULL
  const char* findEOL(const char* start) const
  {
    assert(peek() <= start);          // 断言:起始位置 >= 可读区域起始
    assert(start <= beginWrite());    // 断言:起始位置 <= 可写区域起始
    const void* eol = memchr(start, '\n', beginWrite() - start);
    return static_cast<const char*>(eol);
  }

  /// 取回(消费)指定长度的可读数据(仅移动读索引,不清理数据)
  /// 设计为返回 void:避免以下未定义行为:
  /// string str(retrieve(readableBytes()), readableBytes());
  /// 两个函数的执行顺序未指定,可能导致错误
  /// @param len 要取回的字节数(必须 <= 可读字节数)
  void retrieve(size_t len)
  {
    assert(len <= readableBytes());   // 断言:取回长度不超过可读字节数
    if (len < readableBytes())        // 取回部分数据:仅移动读索引
    {
      readerIndex_ += len;
    }
    else                              // 取回全部数据:重置读写索引
    {
      retrieveAll();
    }
  }

  /// 取回数据直到指定位置(移动读索引到 end 位置)
  /// @param end 结束位置(必须在可读区域内)
  void retrieveUntil(const char* end)
  {
    assert(peek() <= end);            // 断言:结束位置 >= 可读区域起始
    assert(end <= beginWrite());      // 断言:结束位置 <= 可写区域起始
    retrieve(end - peek());           // 取回 [peek(), end) 区间的数据
  }

  /// 取回 int64_t 类型数据(8 字节)
  void retrieveInt64()
  {
    retrieve(sizeof(int64_t));
  }

  /// 取回 int32_t 类型数据(4 字节)
  void retrieveInt32()
  {
    retrieve(sizeof(int32_t));
  }

  /// 取回 int16_t 类型数据(2 字节)
  void retrieveInt16()
  {
    retrieve(sizeof(int16_t));
  }

  /// 取回 int8_t 类型数据(1 字节)
  void retrieveInt8()
  {
    retrieve(sizeof(int8_t));
  }

  /// 取回全部可读数据(重置读写索引到初始位置)
  void retrieveAll()
  {
    readerIndex_ = kCheapPrepend;
    writerIndex_ = kCheapPrepend;
  }

  /// 取回全部可读数据并转换为 string(消费数据)
  /// @return 包含所有可读数据的字符串
  string retrieveAllAsString()
  {
    return retrieveAsString(readableBytes());
  }

  /// 取回指定长度的可读数据并转换为 string(消费数据)
  /// @param len 要取回的字节数(必须 <= 可读字节数)
  /// @return 包含指定长度数据的字符串
  string retrieveAsString(size_t len)
  {
    assert(len <= readableBytes());   // 断言:取回长度不超过可读字节数
    string result(peek(), len);       // 拷贝数据到 string
    retrieve(len);                    // 移动读索引(消费数据)
    return result;                    // 返回字符串(RVO 优化,无拷贝)
  }

  /// 将可读区域转换为 StringPiece(不消费数据,轻量级视图)
  /// @return 可读区域的 StringPiece 视图
  StringPiece toStringPiece() const
  {
    return StringPiece(peek(), static_cast<int>(readableBytes()));
  }

  /// 追加字符串到可写区域
  /// @param str 要追加的字符串(StringPiece 视图,避免拷贝)
  void append(const StringPiece& str)
  {
    append(str.data(), str.size());
  }

  /// 追加指定长度的字符数据到可写区域
  /// @param data 数据起始地址(restrict 标记:指针无别名,编译器可优化)
  /// @param len 数据长度
  void append(const char* /*restrict*/ data, size_t len)
  {
    ensureWritableBytes(len);         // 确保可写区域足够容纳 len 字节
    std::copy(data, data+len, beginWrite()); // 拷贝数据到可写区域
    hasWritten(len);                  // 移动写索引
  }

  /// 追加指定长度的通用数据到可写区域
  /// @param data 数据起始地址(restrict 标记:指针无别名)
  /// @param len 数据长度
  void append(const void* /*restrict*/ data, size_t len)
  {
    append(static_cast<const char*>(data), len);
  }

  /// 确保可写区域至少有 len 字节空间(不足则扩容)
  /// @param len 需要的可写字节数
  void ensureWritableBytes(size_t len)
  {
    if (writableBytes() < len)        // 可写空间不足
    {
      makeSpace(len);                 // 扩容/调整内存布局
    }
    assert(writableBytes() >= len);   // 断言:扩容后空间足够
  }

  /// 获取可写区域的起始地址(可写)
  char* beginWrite()
  { return begin() + writerIndex_; }

  /// 获取可写区域的起始地址(仅读)
  const char* beginWrite() const
  { return begin() + writerIndex_; }

  /// 标记已写入指定长度的数据(移动写索引)
  /// @param len 已写入的字节数(必须 <= 可写字节数)
  void hasWritten(size_t len)
  {
    assert(len <= writableBytes());   // 断言:写入长度不超过可写字节数
    writerIndex_ += len;
  }

  /// 回退已写入的数据(移动写索引,仅用于撤销写入)
  /// @param len 要回退的字节数(必须 <= 已写入的可读字节数)
  void unwrite(size_t len)
  {
    assert(len <= readableBytes());   // 断言:回退长度不超过可读字节数
    writerIndex_ -= len;
  }

  ///
  /// 追加 int64_t 类型数据(网络字节序)
  /// @param x 要追加的主机序 64 位整数
  ///
  void appendInt64(int64_t x)
  {
    int64_t be64 = sockets::hostToNetwork64(x); // 转换为网络字节序
    append(&be64, sizeof be64);                // 追加到可写区域
  }

  ///
  /// 追加 int32_t 类型数据(网络字节序)
  /// @param x 要追加的主机序 32 位整数
  ///
  void appendInt32(int32_t x)
  {
    int32_t be32 = sockets::hostToNetwork32(x); // 转换为网络字节序
    append(&be32, sizeof be32);                // 追加到可写区域
  }

  /// 追加 int16_t 类型数据(网络字节序)
  /// @param x 要追加的主机序 16 位整数
  void appendInt16(int16_t x)
  {
    int16_t be16 = sockets::hostToNetwork16(x); // 转换为网络字节序
    append(&be16, sizeof be16);                // 追加到可写区域
  }

  /// 追加 int8_t 类型数据(无需字节序转换)
  /// @param x 要追加的 8 位整数
  void appendInt8(int8_t x)
  {
    append(&x, sizeof x);
  }

  ///
  /// 读取 int64_t 类型数据(网络字节序转主机序,消费数据)
  /// 要求:可读字节数 >= sizeof(int64_t)
  /// @return 主机序 64 位整数
  int64_t readInt64()
  {
    int64_t result = peekInt64();    // 读取数据(不消费)
    retrieveInt64();                 // 消费数据(移动读索引)
    return result;
  }

  ///
  /// 读取 int32_t 类型数据(网络字节序转主机序,消费数据)
  /// 要求:可读字节数 >= sizeof(int32_t)
  /// @return 主机序 32 位整数
  int32_t readInt32()
  {
    int32_t result = peekInt32();    // 读取数据(不消费)
    retrieveInt32();                 // 消费数据(移动读索引)
    return result;
  }

  /// 读取 int16_t 类型数据(网络字节序转主机序,消费数据)
  /// 要求:可读字节数 >= sizeof(int16_t)
  /// @return 主机序 16 位整数
  int16_t readInt16()
  {
    int16_t result = peekInt16();    // 读取数据(不消费)
    retrieveInt16();                 // 消费数据(移动读索引)
    return result;
  }

  /// 读取 int8_t 类型数据(消费数据)
  /// 要求:可读字节数 >= sizeof(int8_t)
  /// @return 8 位整数
  int8_t readInt8()
  {
    int8_t result = peekInt8();      // 读取数据(不消费)
    retrieveInt8();                  // 消费数据(移动读索引)
    return result;
  }

  ///
  /// 查看 int64_t 类型数据(网络字节序转主机序,不消费数据)
  /// 要求:可读字节数 >= sizeof(int64_t)
  /// @return 主机序 64 位整数
  int64_t peekInt64() const
  {
    assert(readableBytes() >= sizeof(int64_t)); // 断言:数据足够
    int64_t be64 = 0;
    ::memcpy(&be64, peek(), sizeof be64);       // 拷贝数据到临时变量
    return sockets::networkToHost64(be64);      // 转换为主机序
  }

  ///
  /// 查看 int32_t 类型数据(网络字节序转主机序,不消费数据)
  /// 要求:可读字节数 >= sizeof(int32_t)
  /// @return 主机序 32 位整数
  int32_t peekInt32() const
  {
    assert(readableBytes() >= sizeof(int32_t)); // 断言:数据足够
    int32_t be32 = 0;
    ::memcpy(&be32, peek(), sizeof be32);       // 拷贝数据到临时变量
    return sockets::networkToHost32(be32);      // 转换为主机序
  }

  /// 查看 int16_t 类型数据(网络字节序转主机序,不消费数据)
  /// 要求:可读字节数 >= sizeof(int16_t)
  /// @return 主机序 16 位整数
  int16_t peekInt16() const
  {
    assert(readableBytes() >= sizeof(int16_t)); // 断言:数据足够
    int16_t be16 = 0;
    ::memcpy(&be16, peek(), sizeof be16);       // 拷贝数据到临时变量
    return sockets::networkToHost16(be16);      // 转换为主机序
  }

  /// 查看 int8_t 类型数据(不消费数据)
  /// 要求:可读字节数 >= sizeof(int8_t)
  /// @return 8 位整数
  int8_t peekInt8() const
  {
    assert(readableBytes() >= sizeof(int8_t));  // 断言:数据足够
    int8_t x = *peek();                         // 直接读取第一个字节
    return x;
  }

  ///
  /// 前置 int64_t 类型数据(网络字节序,追加到可读区域前)
  /// @param x 要前置的主机序 64 位整数
  ///
  void prependInt64(int64_t x)
  {
    int64_t be64 = sockets::hostToNetwork64(x); // 转换为网络字节序
    prepend(&be64, sizeof be64);                // 前置到可读区域前
  }

  ///
  /// 前置 int32_t 类型数据(网络字节序,追加到可读区域前)
  /// @param x 要前置的主机序 32 位整数
  ///
  void prependInt32(int32_t x)
  {
    int32_t be32 = sockets::hostToNetwork32(x); // 转换为网络字节序
    prepend(&be32, sizeof be32);                // 前置到可读区域前
  }

  /// 前置 int16_t 类型数据(网络字节序,追加到可读区域前)
  /// @param x 要前置的主机序 16 位整数
  void prependInt16(int16_t x)
  {
    int16_t be16 = sockets::hostToNetwork16(x); // 转换为网络字节序
    prepend(&be16, sizeof be16);                // 前置到可读区域前
  }

  /// 前置 int8_t 类型数据(追加到可读区域前)
  /// @param x 要前置的 8 位整数
  void prependInt8(int8_t x)
  {
    prepend(&x, sizeof x);
  }

  /// 前置指定长度的数据(追加到可读区域前)
  /// @param data 数据起始地址(restrict 标记:指针无别名)
  /// @param len 数据长度(必须 <= 可前置字节数)
  void prepend(const void* /*restrict*/ data, size_t len)
  {
    assert(len <= prependableBytes());          // 断言:前置长度不超过可前置字节数
    readerIndex_ -= len;                        // 移动读索引,腾出前置空间
    const char* d = static_cast<const char*>(data);
    std::copy(d, d+len, begin()+readerIndex_);  // 拷贝数据到前置区域
  }

  /// 收缩缓冲区内存(保留指定预留空间,释放多余内存)
  /// @param reserve 收缩后保留的可写预留空间
  void shrink(size_t reserve)
  {
    // FIXME:C++11 及以上可使用 vector::shrink_to_fit() 优化
    Buffer other;                               // 创建临时缓冲区
    // 确保临时缓冲区有足够空间:当前可读数据 + 预留空间
    other.ensureWritableBytes(readableBytes()+reserve);
    other.append(toStringPiece());              // 拷贝当前可读数据到临时缓冲区
    swap(other);                                // 交换临时缓冲区和当前缓冲区(浅交换)
  }

  /// 获取缓冲区内部容量(vector 的容量,非已使用大小)
  size_t internalCapacity() const
  {
    return buffer_.capacity();
  }

  /// 直接从文件描述符读取数据到缓冲区
  /// 可通过 readv(2) 实现分散读(减少拷贝)
  /// @param fd 要读取的文件描述符(如 socket fd)
  /// @param savedErrno 用于保存 errno(避免被其他调用覆盖)
  /// @return read(2) 的返回值:成功返回读取的字节数,失败返回 -1(errno 存入 savedErrno)
  ssize_t readFd(int fd, int* savedErrno);

 private:

  /// 获取缓冲区起始地址(可写)
  char* begin()
  { return &*buffer_.begin(); }

  /// 获取缓冲区起始地址(仅读)
  const char* begin() const
  { return &*buffer_.begin(); }

  /// 扩容/调整内存布局,确保可写区域有 len 字节空间
  /// 策略:
  /// 1. 可写空间 + 可前置空间 < len + 前置区 → 直接扩容 vector
  /// 2. 否则 → 将可读数据移动到缓冲区头部,腾出可写空间
  /// @param len 需要的可写字节数
  void makeSpace(size_t len)
  {
    // 情况1:总空闲空间(可写+可前置)不足 → 扩容
    if (writableBytes() + prependableBytes() < len + kCheapPrepend)
    {
      // FIXME:可优化为移动可读数据后再扩容,减少内存分配
      buffer_.resize(writerIndex_+len); // 直接扩容到 写索引 + 需要的长度
    }
    else // 情况2:总空闲空间足够 → 移动可读数据到头部,腾出可写空间
    {
      assert(kCheapPrepend < readerIndex_); // 断言:读索引 > 前置区大小
      size_t readable = readableBytes();   // 可读数据长度
      // 将可读数据移动到缓冲区头部(前置区末尾)
      std::copy(begin()+readerIndex_,
                begin()+writerIndex_,
                begin()+kCheapPrepend);
      readerIndex_ = kCheapPrepend;        // 重置读索引到前置区末尾
      writerIndex_ = readerIndex_ + readable; // 重置写索引到可读数据末尾
      assert(readable == readableBytes()); // 断言:移动后可读数据长度不变
    }
  }

 private:
  std::vector<char> buffer_;  // 底层存储:动态字符数组
  size_t readerIndex_;        // 读索引:指向可读区域起始
  size_t writerIndex_;        // 写索引:指向可写区域起始

  static const char kCRLF[];  // CRLF 常量(\r\n)
};

}  // namespace net
}  // namespace muduo

#endif  // MUDUO_NET_BUFFER_H

1. Buffer 类的整体定位与核心设计目标

net::Buffer 是 Muduo 网络编程的核心数据缓冲区,解决了网络编程中以下痛点:

  • 避免频繁 malloc/free:用 std::vector<char> 作为底层存储,自动扩容且内存连续,减少内存碎片;
  • 读写分离:通过 readerIndex_/writerIndex_ 区分可读 / 可写区域,"读取数据" 仅移动索引(懒清理),无需频繁拷贝数据;
  • 适配网络协议:支持预追加(prepend) (在数据头部插入长度 / 协议字段)、文本协议解析 (查找 CRLF/EOL)、网络字节序转换(主机 <-> 网络字节序);
  • 高效读写 fd:通过 readFd 直接从 socket 读取数据到缓冲区,结合 readv 优化分散读,减少系统调用。

其核心内存布局如下(Muduo 注释中的经典示意图):

cpp 复制代码
+-------------------+------------------+------------------+
| prependable bytes |  readable bytes  |  writable bytes  |
|                   |     (CONTENT)    |                  |
+-------------------+------------------+------------------+
|                   |                  |                  |
0      <=      readerIndex_   <=   writerIndex_    <=     buffer_.size()
  • Prependable 区域[0, readerIndex_),用于在数据头部插入内容(如协议长度字段),默认预留 8 字节(kCheapPrepend);
  • Readable 区域[readerIndex_, writerIndex_),存储已写入、待读取的数据(业务层可读取);
  • Writable 区域[writerIndex_, buffer_.size()),存储待写入的数据(从 socket 读取 / 业务层写入)。

2. 核心常量与成员变量解析

cpp 复制代码
// 预追加区域默认预留8字节(可存放int64_t,适配大部分协议的长度字段)
static const size_t kCheapPrepend = 8;
// 缓冲区初始大小(1024字节,可通过构造函数自定义)
static const size_t kInitialSize = 1024;

// 底层存储:vector<char> 保证内存连续、自动扩容,且支持高效的首尾操作
std::vector<char> buffer_;
// 读索引:指向可读区域的起始位置
size_t readerIndex_;
// 写索引:指向可写区域的起始位置
size_t writerIndex_;

// 静态常量:CRLF("\r\n"),用于文本协议(如HTTP)解析
static const char kCRLF[];

关键设计点

  • vector<char> 作为底层:相比 char* 手动管理内存,vector 自动处理扩容 / 释放,且内存连续(满足 readv/writev 等系统调用的要求);
  • 双索引分离:读写操作仅修改索引,不实际删除 / 移动数据(懒清理),大幅减少数据拷贝开销;
  • 预追加区域默认 8 字节:刚好容纳 int64_t 类型,适配大部分网络协议的 "长度前缀" 设计(如先传 4/8 字节长度,再传数据)。

3. 核心接口逐模块拆解

1. 构造函数与基础查询接口
cpp 复制代码
// 构造函数:初始化缓冲区大小=预追加区域+初始大小,读写索引都指向预追加区域末尾
explicit Buffer(size_t initialSize = kInitialSize)
  : buffer_(kCheapPrepend + initialSize),
    readerIndex_(kCheapPrepend),
    writerIndex_(kCheapPrepend)
{
  // 断言验证初始状态:可读字节=0,可写字节=初始大小,预追加字节=8
  assert(readableBytes() == 0);
  assert(writableBytes() == initialSize);
  assert(prependableBytes() == kCheapPrepend);
}

// 可读字节数 = 写索引 - 读索引
size_t readableBytes() const { return writerIndex_ - readerIndex_; }
// 可写字节数 = 缓冲区总大小 - 写索引
size_t writableBytes() const { return buffer_.size() - writerIndex_; }
// 可预追加字节数 = 读索引(读索引左边都是可预追加区域)
size_t prependableBytes() const { return readerIndex_; }
// 获取可读区域起始地址(peek=窥视,仅读不移动索引)
const char* peek() const { return begin() + readerIndex_; }

核心价值:基础查询接口是所有读写操作的基础,计算逻辑简洁且无锁(Buffer 本身非线程安全,需上层加锁),保证高效。

2. 数据读取 / 回收接口(retrieve 系列)

"回收" 并非实际删除数据,而是移动读索引 (懒清理),只有当缓冲区空间不足时,才会通过 makeSpace 移动数据到前端,大幅减少拷贝开销。

cpp 复制代码
// 回收len字节(移动读索引)
void retrieve(size_t len)
{
  assert(len <= readableBytes()); // 断言:回收长度不超过可读字节
  if (len < readableBytes())
  {
    readerIndex_ += len; // 只回收部分:读索引后移,剩余数据仍可读
  }
  else
  {
    retrieveAll(); // 回收全部:重置读写索引到初始位置
  }
}

// 回收全部数据(重置索引,不清理底层内存)
void retrieveAll()
{
  readerIndex_ = kCheapPrepend;
  writerIndex_ = kCheapPrepend;
}

// 按类型回收(int64/32/16/8),适配网络协议的固定长度字段
void retrieveInt64() { retrieve(sizeof(int64_t)); }
void retrieveInt32() { retrieve(sizeof(int32_t)); }
// ... 其他int类型同理

// 回收直到指定位置(用于协议解析,如读取到CRLF为止)
void retrieveUntil(const char* end)
{
  assert(peek() <= end && end <= beginWrite());
  retrieve(end - peek());
}

// 回收并返回字符串(常用:读取所有可读数据为string)
string retrieveAllAsString() { return retrieveAsString(readableBytes()); }
string retrieveAsString(size_t len)
{
  assert(len <= readableBytes());
  string result(peek(), len); // 拷贝可读区域数据到string
  retrieve(len); // 回收已读取数据
  return result;
}

设计亮点

  • 懒清理:仅移动索引,不修改底层 vector 数据,避免频繁拷贝;
  • 类型化回收:直接适配网络协议的固定长度字段(如 4 字节长度、2 字节端口),无需手动计算长度。
3. 数据写入 / 追加接口(append 系列)

写入前先保证可写空间,空间不足时通过 makeSpace 扩容或移动数据,保证写入安全。

cpp 复制代码
// 追加字符串/字符数组/任意数据
void append(const StringPiece& str) { append(str.data(), str.size()); }
void append(const char* data, size_t len)
{
  ensureWritableBytes(len); // 确保有足够可写空间
  std::copy(data, data+len, beginWrite()); // 拷贝数据到可写区域
  hasWritten(len); // 移动写索引
}

// 追加数值(自动转换为网络字节序),适配网络传输
void appendInt64(int64_t x)
{
  int64_t be64 = sockets::hostToNetwork64(x); // 主机序→网络序
  append(&be64, sizeof be64);
}
void appendInt32(int32_t x)
{
  int32_t be32 = sockets::hostToNetwork32(x);
  append(&be32, sizeof be32);
}
// ... 其他int类型同理

// 确保可写空间≥len,不足则调用makeSpace扩容/移动数据
void ensureWritableBytes(size_t len)
{
  if (writableBytes() < len) { makeSpace(len); }
  assert(writableBytes() >= len);
}

// 获取可写区域起始地址
char* beginWrite() { return begin() + writerIndex_; }
const char* beginWrite() const { return begin() + writerIndex_; }

// 移动写索引(写入数据后调用)
void hasWritten(size_t len)
{
  assert(len <= writableBytes());
  writerIndex_ += len;
}

// 回退写索引(写入错误时撤销,如部分写入)
void unwrite(size_t len)
{
  assert(len <= readableBytes());
  writerIndex_ -= len;
}
4. 核心内存管理:makeSpace(扩容 / 数据移动)

这是 Buffer 内存优化的核心,分两种情况处理空间不足:

cpp 复制代码
void makeSpace(size_t len)
{
  // 情况1:可写空间 + 预追加空间 < 所需空间 + 最小预追加空间 → 直接扩容vector
  if (writableBytes() + prependableBytes() < len + kCheapPrepend)
  {
    buffer_.resize(writerIndex_ + len); // 扩容到"当前写索引+所需长度"
  }
  // 情况2:空间足够但分散 → 移动可读数据到前端,整合可写空间
  else
  {
    assert(kCheapPrepend < readerIndex_); // 确保有预追加空间可利用
    size_t readable = readableBytes();
    // 把可读数据从[readerIndex_, writerIndex_)移动到[kCheapPrepend, kCheapPrepend+readable)
    std::copy(begin()+readerIndex_,
              begin()+writerIndex_,
              begin()+kCheapPrepend);
    readerIndex_ = kCheapPrepend; // 重置读索引
    writerIndex_ = readerIndex_ + readable; // 重置写索引
    assert(readable == readableBytes()); // 验证数据未丢失
  }
}

设计逻辑

  • 优先移动数据:避免频繁扩容(vector 扩容会拷贝全部数据),仅当空间真的不足时才扩容;
  • 数据移动后,可写空间被整合到缓冲区末尾,最大化连续可写空间。
5. 预追加接口(prepend 系列)

网络编程中常用(如在已读取的数据头部插入长度 / 协议类型字段),利用预追加区域实现 "头部插入":

cpp 复制代码
// 预追加任意数据(插入到可读区域头部)
void prepend(const void* data, size_t len)
{
  assert(len <= prependableBytes()); // 确保预追加空间足够
  readerIndex_ -= len; // 读索引左移,腾出空间
  const char* d = static_cast<const char*>(data);
  std::copy(d, d+len, begin()+readerIndex_); // 拷贝数据到预追加区域
}

// 预追加数值(自动转换为网络字节序)
void prependInt64(int64_t x)
{
  int64_t be64 = sockets::hostToNetwork64(x);
  prepend(&be64, sizeof be64);
}
// ... 其他int类型同理

应用场景:比如发送数据时,先写入数据内容,再在头部预追加 4 字节的长度字段(协议要求先传长度再传数据)。

6. 协议解析辅助接口(查找 CRLF/EOL)

专为文本协议(如 HTTP、SMTP)设计,快速查找换行符:

cpp 复制代码
// 查找CRLF("\r\n"),返回NULL表示未找到
const char* findCRLF() const
{
  const char* crlf = std::search(peek(), beginWrite(), kCRLF, kCRLF+2);
  return crlf == beginWrite() ? NULL : crlf;
}

// 从指定位置开始查找CRLF
const char* findCRLF(const char* start) const
{
  assert(peek() <= start && start <= beginWrite());
  const char* crlf = std::search(start, beginWrite(), kCRLF, kCRLF+2);
  return crlf == beginWrite() ? NULL : crlf;
}

// 查找EOL("\n"),更快(用memchr)
const char* findEOL() const
{
  const void* eol = memchr(peek(), '\n', readableBytes());
  return static_cast<const char*>(eol);
}

核心价值 :无需遍历每个字节,利用标准库 std::search/memchr 高效查找,适配 HTTP 行解析(每行以 CRLF 结尾)。

7. 网络字节序解析接口(peek/read 系列)

读取可读区域的数值,自动转换为主机字节序,无需手动处理字节序:

cpp 复制代码
// 窥视int64(仅读不回收,peek=窥视)
int64_t peekInt64() const
{
  assert(readableBytes() >= sizeof(int64_t));
  int64_t be64 = 0;
  ::memcpy(&be64, peek(), sizeof be64); // 拷贝数据到变量
  return sockets::networkToHost64(be64); // 网络序→主机序
}

// 读取int64(窥视+回收)
int64_t readInt64()
{
  int64_t result = peekInt64();
  retrieveInt64();
  return result;
}
// ... 其他int类型同理
8. 内存优化接口
cpp 复制代码
// 收缩缓冲区,仅保留可读数据+预留空间,减少内存占用
void shrink(size_t reserve)
{
  Buffer other; // 创建临时Buffer
  // 临时Buffer预留足够空间:可读数据+reserve
  other.ensureWritableBytes(readableBytes()+reserve);
  other.append(toStringPiece()); // 拷贝当前可读数据到临时Buffer
  swap(other); // 交换底层vector,当前Buffer变为优化后的大小
}

// 获取底层vector的容量(用于调试/内存监控)
size_t internalCapacity() const { return buffer_.capacity(); }
9. fd 读取接口(核心网络操作)
cpp 复制代码
// 从fd(如socket)读取数据到缓冲区,返回读取字节数,保存errno
ssize_t readFd(int fd, int* savedErrno);

设计意图 :这是网络编程的核心接口,底层会用 readv 实现 "分散读":

  • 第一块缓冲区:当前 Buffer 的可写区域;
  • 第二块缓冲区:临时栈上缓冲区(避免 Buffer 频繁扩容);
  • 读取完成后,将临时缓冲区的数据追加到 Buffer,减少系统调用次数。

4. 设计亮点总结

设计点 核心价值 网络编程应用
读写索引分离(懒清理) 仅移动索引,不频繁拷贝 / 删除数据,效率极高 大量小数据读写场景(如 TCP 粘包拆包)
预追加区域(kCheapPrepend=8) 方便在数据头部插入长度 / 协议字段,无需移动已有数据 自定义协议(如 "长度 + 内容" 格式)
vector<char> 底层存储 内存连续、自动扩容、无需手动管理内存 适配 readv/writev 等系统调用
自动网络字节序转换 封装 host<->network 字节序,避免手动转换出错 跨主机数据传输(如端口、长度字段)
makeSpace 内存优化 优先移动数据而非扩容,减少内存拷贝 高并发场景下降低 CPU 开销
文本协议解析接口(findCRLF/EOL) 快速解析 HTTP/SMTP 等文本协议 应用层协议解析

总结

  1. 核心布局 :Buffer 分为预追加、可读、可写三个区域,通过 readerIndex_/writerIndex_ 实现读写分离,懒清理策略减少数据拷贝;
  2. 内存管理 :底层用 vector<char> 自动扩容,makeSpace 优先移动数据整合空间,仅在必要时扩容;
  3. 网络适配:支持预追加(协议头)、网络字节序转换、fd 高效读取,完美适配 TCP 数据收发 / 协议解析;
  4. 接口设计:类型化读写(int64/32 等)、文本协议查找(CRLF/EOL),降低网络编程复杂度。

二、 Buffer.cc

先贴出完整代码,再逐部分解释:

cpp 复制代码
// Copyright 2010, Shuo Chen.  All rights reserved.
// http://code.google.com/p/muduo/
//
// 本源代码的使用受 BSD 风格许可证约束
// 该许可证可在 License 文件中查阅。

// 作者:陈硕 (chenshuo at chenshuo dot com)
//

#include "muduo/net/Buffer.h"

#include "muduo/net/SocketsOps.h"  // 封装 socket 系统调用(如 readv)

#include <errno.h>   // 错误码定义(errno)
#include <sys/uio.h> // readv/writev 相关结构体(iovec)

using namespace muduo;
using namespace muduo::net;

// 静态常量初始化:CRLF 分隔符(\r\n)
const char Buffer::kCRLF[] = "\r\n";

// 静态常量初始化:默认前置区域大小(8 字节)
const size_t Buffer::kCheapPrepend;
// 静态常量初始化:默认初始可写区域大小(1024 字节)
const size_t Buffer::kInitialSize;

/// 从文件描述符读取数据到缓冲区(核心实现)
/// 采用 readv 分散读:优先读入缓冲区可写区域,不足时读入额外缓冲区,减少系统调用次数
/// @param fd 要读取的文件描述符(如 socket fd)
/// @param savedErrno 用于保存 errno(避免被其他系统调用覆盖)
/// @return 成功:读取的字节数;失败:-1(错误码存入 savedErrno)
ssize_t Buffer::readFd(int fd, int* savedErrno)
{
  // 额外缓冲区:64KB(避免频繁扩容,也避免单次读取过小导致多次系统调用)
  // 作用:当内置缓冲区可写空间不足时,临时存储超出部分的数据
  char extrabuf[65536];
  // iovec 数组:分散读的内存块列表(最多两个:内置缓冲区 + 额外缓冲区)
  struct iovec vec[2];
  const size_t writable = writableBytes(); // 获取内置缓冲区的可写字节数
  
  // 第一个内存块:内置缓冲区的可写区域
  vec[0].iov_base = begin() + writerIndex_; // 可写区域起始地址
  vec[0].iov_len = writable;                // 可写区域长度
  
  // 第二个内存块:额外缓冲区(仅当内置可写空间不足时使用)
  vec[1].iov_base = extrabuf;
  vec[1].iov_len = sizeof extrabuf;
  
  // 确定分散读的内存块数量:
  // - 内置可写空间 >= 额外缓冲区大小 → 仅用内置缓冲区(iovcnt=1)
  // - 否则 → 同时使用内置+额外缓冲区(iovcnt=2)
  // 注:这样设计的目的是单次最多读取 128KB-1 字节(writable + 64KB),减少系统调用
  const int iovcnt = (writable < sizeof extrabuf) ? 2 : 1;
  
  // 调用 readv 分散读:将数据读入 vec 指向的多个内存块
  // sockets::readv 是对系统调用 readv 的封装,处理了 EINTR 等中断情况
  const ssize_t n = sockets::readv(fd, vec, iovcnt);
  
  if (n < 0) // 读取失败
  {
    *savedErrno = errno; // 保存错误码(避免后续调用覆盖 errno)
  }
  else if (implicit_cast<size_t>(n) <= writable) // 读取的数据全部存入内置缓冲区
  {
    writerIndex_ += n; // 仅移动内置缓冲区的写索引
  }
  else // 读取的数据超出内置缓冲区可写空间,部分存入额外缓冲区
  {
    writerIndex_ = buffer_.size(); // 内置缓冲区写索引移到末尾(已写满)
    append(extrabuf, n - writable); // 将额外缓冲区中的数据追加到内置缓冲区(会触发扩容)
  }
  
  // 以下是注释掉的调试代码:若读取长度等于两个缓冲区总大小,说明可能还有数据未读取
  // if (n == writable + sizeof extrabuf)
  // {
  //   goto line_30; // 跳转到再次读取的逻辑(原代码未实现,仅预留)
  // }
  
  return n; // 返回实际读取的字节数
}

1. readFd 的核心设计目标

在网络编程中,从 socket fd 读取数据的核心痛点是:无法提前知道要读取的字节数 ------ 如果只准备小缓冲区,可能需要多次 read 系统调用(开销大);如果直接扩容 Buffer 到超大尺寸,又会浪费内存。

readFd 的设计目标就是用最小的系统调用次数 + 最少的内存拷贝,高效读取 socket 数据:

  • readv(分散读):一次系统调用可将数据读到多个缓冲区,避免多次 read
  • 双缓冲区策略:优先用 Buffer 内部可写区域,空间不足时用栈上 extrabuf(64KB),避免 Buffer 频繁扩容;
  • 错误码保存:将 errno 保存到参数中,避免覆盖全局 errno(多线程安全)。

2. 逐行拆解核心实现

cpp 复制代码
// 静态常量初始化(头文件中声明,源文件中定义)
const char Buffer::kCRLF[] = "\r\n";
const size_t Buffer::kCheapPrepend;
const size_t Buffer::kInitialSize;

ssize_t Buffer::readFd(int fd, int* savedErrno)
{
  // 栈上临时缓冲区:65536字节(64KB),无需malloc/free,自动释放,且大小适配大部分TCP数据包
  char extrabuf[65536];
  // iovec:分散读/写的核心结构体,存储"缓冲区地址+长度"
  struct iovec vec[2];
  // 获取Buffer当前的可写空间大小
  const size_t writable = writableBytes();

  // 第一个缓冲区:指向Buffer内部的可写区域(优先使用)
  vec[0].iov_base = begin()+writerIndex_;
  vec[0].iov_len = writable;
  // 第二个缓冲区:指向栈上的extrabuf(Buffer空间不足时使用)
  vec[1].iov_base = extrabuf;
  vec[1].iov_len = sizeof extrabuf;

  // 动态判断使用几个缓冲区:
  // - 如果Buffer可写空间 < 64KB → 用2个缓冲区(避免Buffer频繁扩容)
  // - 如果Buffer可写空间 ≥64KB → 只用1个缓冲区(无需extrabuf,减少后续拷贝)
  const int iovcnt = (writable < sizeof extrabuf) ? 2 : 1;

  // 调用封装的readv:从fd读取数据到vec指定的缓冲区,返回读取的字节数
  const ssize_t n = sockets::readv(fd, vec, iovcnt);

  if (n < 0)
  {
    // 读取失败:保存errno(避免覆盖全局errno,多线程安全)
    *savedErrno = errno;
  }
  else if (implicit_cast<size_t>(n) <= writable)
  {
    // 情况1:读取的字节数 ≤ Buffer可写空间 → 仅移动写索引,无需拷贝extrabuf
    writerIndex_ += n;
  }
  else
  {
    // 情况2:读取的字节数 > Buffer可写空间 → Buffer写满,剩余数据在extrabuf中
    writerIndex_ = buffer_.size(); // Buffer写索引移到末尾(写满)
    append(extrabuf, n - writable); // 追加extrabuf中的数据到Buffer
  }

  // 注释掉的代码是调试用:如果读取的字节数刚好等于两个缓冲区总和,可能需要再次读取(避免数据未读完)
  // if (n == writable + sizeof extrabuf) { goto line_30; }

  return n; // 返回实际读取的字节数(n<0表示失败)
}

3. 关键细节深度解析

1. readv(分散读)的核心作用

readv 是 POSIX 系统调用,原型为:

cpp 复制代码
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
  • 功能:一次系统调用,将 fd 中的数据分散读取到 iov 指向的多个缓冲区中;
  • 填充规则:先填第一个缓冲区(vec [0]),填满后再填第二个(vec [1]),依此类推;
  • 优势:相比多次调用 readreadv 只需一次系统调用,大幅降低内核态 / 用户态切换的开销(系统调用是高性能网络编程的核心优化点)。
2. extrabuf 的设计巧思
  • 大小选择:65536 字节(64KB)------ 这是 TCP 最大报文段长度(MSS)的常见上限,也是大部分场景下单次读取的最大数据量,既不会浪费栈空间,又能避免频繁扩容 Buffer;
  • 存储位置:栈上分配(而非堆)------ 栈内存分配 / 释放无开销,且自动销毁,无需手动管理;
  • 使用时机:仅当 Buffer 可写空间不足时才用,避免不必要的内存拷贝。
3. iovcnt 的动态判断逻辑
场景 iovcnt 说明
Buffer 可写空间 ≥ 64KB 1 只用 Buffer 内部缓冲区,读取的数据直接写入 Buffer,无需后续拷贝;
Buffer 可写空间 < 64KB 2 先用 Buffer 内部缓冲区,剩余数据写入 extrabuf,后续通过 append 拷贝到 Buffer;

这个判断的核心是:优先使用 Buffer 自身空间,减少拷贝;空间不足时用栈缓冲区兜底,避免频繁扩容

4. 不同读取结果的处理逻辑
读取结果 处理方式 核心目的
n < 0 保存 errno 到 savedErrno 多线程安全:全局 errno 可能被其他线程覆盖,保存到参数中供上层处理;
0 ≤ n ≤ writable 移动 writerIndex_ += n 无需拷贝:数据全部在 Buffer 内部,仅修改索引即可;
n > writable 先写满 Buffer(writerIndex_ = buffer_.size()),再 append extrabuf 中的数据 完整接收数据:避免数据丢失,append 会自动调用 ensureWritableBytes 扩容 Buffer;
5. 错误码保存机制

*savedErrno = errno:全局 errno 是线程局部存储(TLS),但直接使用仍可能被后续系统调用覆盖,将错误码保存到参数中,让上层能精准获取本次 readv 的错误原因(如 EAGAIN、ECONNRESET 等)。

4. 核心设计亮点总结

设计点 解决的问题 性能 / 易用性提升
双缓冲区(Buffer + extrabuf) Buffer 空间不足时,避免频繁扩容 / 多次 read 减少内存分配开销,降低系统调用次数;
readv 分散读 多次 read 导致的内核态 / 用户态切换开销 单次系统调用读取所有数据,提升吞吐量;
栈上 extrabuf 堆内存 malloc/free 的开销 栈内存无分配 / 释放开销,自动销毁;
动态 iovcnt 不必要的 extrabuf 拷贝 空间足够时直接写入 Buffer,减少一次 memcpy;
错误码保存 全局 errno 被覆盖的问题 多线程安全,上层能精准处理错误;

5. 典型执行流程(Buffer 空间不足场景)

假设 Buffer 可写空间为 1024 字节,从 socket 读取 8192 字节数据:

  1. iovcnt = 2(1024 < 65536);
  2. readv 读取 8192 字节:前 1024 字节写入 Buffer 可写区域,剩余 7168 字节写入 extrabuf;
  3. 处理结果:writerIndex_ = buffer_.size()(Buffer 写满),调用 append(extrabuf, 7168) 将剩余数据追加到 Buffer;
  4. 返回 n=8192,上层可通过 readableBytes() 获取 8192 字节可读数据。

总结

  1. 核心优化 :用 readv 分散读减少系统调用次数,双缓冲区(Buffer + 栈上 extrabuf)避免频繁扩容;
  2. 内存策略:优先使用 Buffer 自身空间,空间不足时用栈缓冲区兜底,减少拷贝开销;
  3. 错误处理:保存 errno 到参数中,保证多线程下错误码的准确性;
  4. 性能关键:栈上 extrabuf(64KB)适配 TCP 常见数据量,无内存管理开销,是高性能读取的核心。
相关推荐
阿猿收手吧!2 小时前
【C++】异常处理:catch块执行后程序如何继续
服务器·网络·c++
代码游侠2 小时前
C语言核心概念复习(一)
c语言·开发语言·c++·笔记·学习
Once_day2 小时前
C++之《Effective C++》读书总结(3)
c语言·c++
蜕变的土豆2 小时前
grpc-通关速成
开发语言·c++
今儿敲了吗2 小时前
10| 扫雷
c++·笔记·学习
代码游侠3 小时前
学习笔记——Linux内核与嵌入式开发3
开发语言·arm开发·c++·学习
怎么没有名字注册了啊3 小时前
C++ 进制转换
开发语言·c++
金枪不摆鳍3 小时前
C++常用关键字考察
c++
茉莉玫瑰花茶4 小时前
C++ 17 详细特性解析(4)
开发语言·c++·算法