解析muduo源码之 Buffer.h & Buffer.cc
目录
一、 Buffer.h
1. Buffer 类的整体定位与核心设计目标
2. 核心常量与成员变量解析
3. 核心接口逐模块拆解
1. 构造函数与基础查询接口
2. 数据读取 / 回收接口(retrieve 系列)
3. 数据写入 / 追加接口(append 系列)
4. 核心内存管理:makeSpace(扩容 / 数据移动)
5. 预追加接口(prepend 系列)
6. 协议解析辅助接口(查找 CRLF/EOL)
7. 网络字节序解析接口(peek/read 系列)
8. 内存优化接口
9. fd 读取接口(核心网络操作)
4. 设计亮点总结
总结
二、 Buffer.cc
1. readFd 的核心设计目标
2. 逐行拆解核心实现
3. 关键细节深度解析
1. readv(分散读)的核心作用
2. extrabuf 的设计巧思
3. iovcnt 的动态判断逻辑
4. 不同读取结果的处理逻辑
5. 错误码保存机制
4. 核心设计亮点总结
5. 典型执行流程(Buffer 空间不足场景)
总结
一、 Buffer.h
先贴出完整代码,再逐部分解释:
// 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 // 算法库(std::copy/std::search 等)
#include // 动态数组:存储缓冲区数据
#include // 断言(调试期检查逻辑正确性)
#include // 内存操作(memchr/memcpy 等)
//#include // 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(
)
/// @return 找到则返回指向 '
' 的指针,未找到返回 NULL
const char* findCRLF() const
{
// FIXME:考虑替换为 memmem() 提升查找效率
const char* crlf = std::search(peek(), beginWrite(), kCRLF, kCRLF+2);
return crlf == beginWrite() ? NULL : crlf;
}
/// 从指定位置开始查找 CRLF(
)
/// @param start 查找起始位置(必须在可读区域内)
/// @return 找到则返回指向 '
' 的指针,未找到返回 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(
)
/// @return 找到则返回指向 '
' 的指针,未找到返回 NULL
const char* findEOL() const
{
const void* eol = memchr(peek(), '
', readableBytes());
return static_cast(eol);
}
/// 从指定位置开始查找 EOL(
)
/// @param start 查找起始位置(必须在可读区域内)
/// @return 找到则返回指向 '
' 的指针,未找到返回 NULL
const char* findEOL(const char* start) const
{
assert(peek() <= start); // 断言:起始位置 >= 可读区域起始
assert(start <= beginWrite()); // 断言:起始位置 <= 可写区域起始
const void* eol = memchr(start, '
', beginWrite() - start);
return static_cast(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(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(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(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 buffer_; // 底层存储:动态字符数组
size_t readerIndex_; // 读索引:指向可读区域起始
size_t writerIndex_; // 写索引:指向可写区域起始
static const char kCRLF[]; // CRLF 常量(
)
};
} // namespace net
} // namespace muduo
#endif // MUDUO_NET_BUFFER_H
1. Buffer 类的整体定位与核心设计目标
net::Buffer 是 Muduo 网络编程的核心数据缓冲区,解决了网络编程中以下痛点:
- 避免频繁
malloc/free:用std::vector作为底层存储,自动扩容且内存连续,减少内存碎片; - 读写分离:通过
readerIndex_/writerIndex_区分可读 / 可写区域,“读取数据” 仅移动索引(懒清理),无需频繁拷贝数据; - 适配网络协议:支持预追加(prepend)(在数据头部插入长度 / 协议字段)、文本协议解析(查找 CRLF/EOL)、网络字节序转换(主机 <-> 网络字节序);
- 高效读写 fd:通过
readFd直接从 socket 读取数据到缓冲区,结合readv优化分散读,减少系统调用。
其核心内存布局如下(Muduo 注释中的经典示意图):
+-------------------+------------------+------------------+
| 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. 核心常量与成员变量解析
// 预追加区域默认预留8字节(可存放int64_t,适配大部分协议的长度字段)
static const size_t kCheapPrepend = 8;
// 缓冲区初始大小(1024字节,可通过构造函数自定义)
static const size_t kInitialSize = 1024;
// 底层存储:vector 保证内存连续、自动扩容,且支持高效的首尾操作
std::vector buffer_;
// 读索引:指向可读区域的起始位置
size_t readerIndex_;
// 写索引:指向可写区域的起始位置
size_t writerIndex_;
// 静态常量:CRLF("
"),用于文本协议(如HTTP)解析
static const char kCRLF[];
关键设计点:
vector作为底层:相比char*手动管理内存,vector自动处理扩容 / 释放,且内存连续(满足readv/writev等系统调用的要求);- 双索引分离:读写操作仅修改索引,不实际删除 / 移动数据(懒清理),大幅减少数据拷贝开销;
- 预追加区域默认 8 字节:刚好容纳
int64_t类型,适配大部分网络协议的 “长度前缀” 设计(如先传 4/8 字节长度,再传数据)。
3. 核心接口逐模块拆解
1. 构造函数与基础查询接口
// 构造函数:初始化缓冲区大小=预追加区域+初始大小,读写索引都指向预追加区域末尾
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 移动数据到前端,大幅减少拷贝开销。
// 回收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 扩容或移动数据,保证写入安全。
// 追加字符串/字符数组/任意数据
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 内存优化的核心,分两种情况处理空间不足:
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 系列)
网络编程中常用(如在已读取的数据头部插入长度 / 协议类型字段),利用预追加区域实现 “头部插入”:
// 预追加任意数据(插入到可读区域头部)
void prepend(const void* data, size_t len)
{
assert(len <= prependableBytes()); // 确保预追加空间足够
readerIndex_ -= len; // 读索引左移,腾出空间
const char* d = static_cast(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)设计,快速查找换行符:
// 查找CRLF("
"),返回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("
"),更快(用memchr)
const char* findEOL() const
{
const void* eol = memchr(peek(), '
', readableBytes());
return static_cast(eol);
}
核心价值:无需遍历每个字节,利用标准库 std::search/memchr 高效查找,适配 HTTP 行解析(每行以 CRLF 结尾)。
7. 网络字节序解析接口(peek/read 系列)
读取可读区域的数值,自动转换为主机字节序,无需手动处理字节序:
// 窥视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. 内存优化接口
// 收缩缓冲区,仅保留可读数据+预留空间,减少内存占用
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 读取接口(核心网络操作)
// 从fd(如socket)读取数据到缓冲区,返回读取字节数,保存errno
ssize_t readFd(int fd, int* savedErrno);
设计意图:这是网络编程的核心接口,底层会用 readv 实现 “分散读”:
- 第一块缓冲区:当前 Buffer 的可写区域;
- 第二块缓冲区:临时栈上缓冲区(避免 Buffer 频繁扩容);
- 读取完成后,将临时缓冲区的数据追加到 Buffer,减少系统调用次数。
4. 设计亮点总结
| 设计点 | 核心价值 | 网络编程应用 |
|---|---|---|
| 读写索引分离(懒清理) | 仅移动索引,不频繁拷贝 / 删除数据,效率极高 | 大量小数据读写场景(如 TCP 粘包拆包) |
| 预追加区域(kCheapPrepend=8) | 方便在数据头部插入长度 / 协议字段,无需移动已有数据 | 自定义协议(如 “长度 + 内容” 格式) |
| vector | 内存连续、自动扩容、无需手动管理内存 | 适配 readv/writev 等系统调用 |
| 自动网络字节序转换 | 封装 host<->network 字节序,避免手动转换出错 | 跨主机数据传输(如端口、长度字段) |
| makeSpace 内存优化 | 优先移动数据而非扩容,减少内存拷贝 | 高并发场景下降低 CPU 开销 |
| 文本协议解析接口(findCRLF/EOL) | 快速解析 HTTP/SMTP 等文本协议 | 应用层协议解析 |
总结
- 核心布局:Buffer 分为预追加、可读、可写三个区域,通过
readerIndex_/writerIndex_实现读写分离,懒清理策略减少数据拷贝; - 内存管理:底层用
vector自动扩容,makeSpace优先移动数据整合空间,仅在必要时扩容; - 网络适配:支持预追加(协议头)、网络字节序转换、fd 高效读取,完美适配 TCP 数据收发 / 协议解析;
- 接口设计:类型化读写(int64/32 等)、文本协议查找(CRLF/EOL),降低网络编程复杂度。
二、 Buffer.cc
先贴出完整代码,再逐部分解释:
// 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)
#include // readv/writev 相关结构体(iovec)
using namespace muduo;
using namespace muduo::net;
// 静态常量初始化:CRLF 分隔符(
)
const char Buffer::kCRLF[] = "
";
// 静态常量初始化:默认前置区域大小(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(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. 逐行拆解核心实现
// 静态常量初始化(头文件中声明,源文件中定义)
const char Buffer::kCRLF[] = "
";
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(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 系统调用,原型为:
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 常见数据量,无内存管理开销,是高性能读取的核心。






