目录
[一、 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. 设计亮点总结)
[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 等文本协议 | 应用层协议解析 |
总结
- 核心布局 :Buffer 分为预追加、可读、可写三个区域,通过
readerIndex_/writerIndex_实现读写分离,懒清理策略减少数据拷贝; - 内存管理 :底层用
vector<char>自动扩容,makeSpace优先移动数据整合空间,仅在必要时扩容; - 网络适配:支持预追加(协议头)、网络字节序转换、fd 高效读取,完美适配 TCP 数据收发 / 协议解析;
- 接口设计:类型化读写(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]),依此类推;
- 优势:相比多次调用
read,readv只需一次系统调用,大幅降低内核态 / 用户态切换的开销(系统调用是高性能网络编程的核心优化点)。
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 字节数据:
iovcnt = 2(1024 < 65536);readv读取 8192 字节:前 1024 字节写入 Buffer 可写区域,剩余 7168 字节写入 extrabuf;- 处理结果:
writerIndex_ = buffer_.size()(Buffer 写满),调用append(extrabuf, 7168)将剩余数据追加到 Buffer; - 返回 n=8192,上层可通过
readableBytes()获取 8192 字节可读数据。
总结
- 核心优化 :用
readv分散读减少系统调用次数,双缓冲区(Buffer + 栈上 extrabuf)避免频繁扩容; - 内存策略:优先使用 Buffer 自身空间,空间不足时用栈缓冲区兜底,减少拷贝开销;
- 错误处理:保存 errno 到参数中,保证多线程下错误码的准确性;
- 性能关键:栈上 extrabuf(64KB)适配 TCP 常见数据量,无内存管理开销,是高性能读取的核心。