C++ 仿 muduo 高并发服务器:用户态缓冲区 Buffer 模块实现
1. 为什么需要 Buffer?
在非阻塞 I/O 的网络编程中,read 和 write 不保证一次就能读完或写完所有数据:
- 读半包 :一次
read可能只收到请求的一部分,需要暂存起来,等下次数据到达再拼接。 - 写半包 :一次
write可能只发送了部分数据,剩余数据需要缓冲,等 socket 可写时继续发送。 - 粘包:多次发送的数据可能被合并成一个 TCP 包到达,需要按协议格式拆分。
因此,服务器需要一个用户态缓冲区 ,在应用层管理数据的接收和发送。
muduo 中的 Buffer 就是这样一个组件,本项目仿照它实现了一个简化版。
2. 代码总览
c++
#include <vector>
#include <stdint.h>
#include <assert.h>
#include <iostream>
#include <string>
#include <cstring>
#include <ctime>
#define BUFFER_DEFAULT_SIZE 1024
class Buffer {
private:
std::vector<char> _buffer; // 使用 vector 管理内存
uint64_t _reader_idx; // 读偏移
uint64_t _writer_idx; // 写偏移
public:
Buffer() : _reader_idx(0), _writer_idx(0), _buffer(BUFFER_DEFAULT_SIZE) {}
char *Begin() { return &*_buffer.begin(); }
char *WritePosition() { return Begin() + _writer_idx; }
char *ReadPosition() { return Begin() + _reader_idx; }
uint64_t TailIdleSize() { return _buffer.size() - _writer_idx; }
uint64_t HeadIdleSize() { return _reader_idx; }
uint64_t ReadAbleSize() { return _writer_idx - _reader_idx; }
void MoveReadOffset(uint64_t len) {
assert(len <= ReadAbleSize());
_reader_idx += len;
}
void MoveWriteOffset(uint64_t len) {
assert(len <= TailIdleSize());
_writer_idx += len;
}
void EnsureWriteSpace(uint64_t len) {
if (TailIdleSize() >= len) return;
if (len <= TailIdleSize() + HeadIdleSize()) {
uint64_t rsz = ReadAbleSize();
std::copy(ReadPosition(), ReadPosition() + rsz, Begin());
_reader_idx = 0;
_writer_idx = rsz;
} else {
_buffer.resize(_writer_idx + len);
}
}
void Write(const void *data, uint64_t len) {
EnsureWriteSpace(len);
const char *d = (const char *)data;
std::copy(d, d + len, WritePosition());
}
void WriteAndPush(const void *data, uint64_t len) {
Write(data, len);
MoveWriteOffset(len);
}
void WriteString(const std::string &data) {
return Write(data.c_str(), data.size());
}
void WriteStringAndPush(const std::string &data) {
WriteString(data);
MoveWriteOffset(data.size());
}
void WriteBuffer(Buffer &data) {
return Write(data.ReadPosition(), data.ReadAbleSize());
}
void WriteBufferAndPush(Buffer &data) {
WriteBuffer(data);
MoveWriteOffset(data.ReadAbleSize());
}
void Read(void *buf, uint64_t len) {
assert(len <= ReadAbleSize());
std::copy(ReadPosition(), ReadPosition() + len, (char *)buf);
}
void ReadAndPop(void *buf, int len) {
Read(buf, len);
MoveReadOffset(len);
}
std::string ReadAsString(uint64_t len) {
assert(len <= ReadAbleSize());
std::string str;
str.resize(len);
Read(&str[0], len);
return str;
}
std::string ReadAsStringAndPop(uint64_t len) {
assert(len <= ReadAbleSize());
std::string str = ReadAsString(len);
MoveReadOffset(len);
return str;
}
char *FindCRLF() {
char *res = (char *)memchr(ReadPosition(), '\n', ReadAbleSize());
return res;
}
std::string GetLine() {
char *pos = FindCRLF();
if (pos == NULL) return "";
return ReadAsString(pos - ReadPosition() + 1);
}
std::string GetLlineAndPop() { // 注意:拼写错误,应为 GetLineAndPop
std::string str = GetLine();
MoveReadOffset(str.size());
return str;
}
void Clear() {
_reader_idx = 0;
_writer_idx = 0;
}
};
3. 核心设计思想
Buffer 的设计围绕三个核心:
- 读写偏移分离
用_reader_idx和_writer_idx标记读位置和写位置,而不是每次读走数据就删除 vector 头部。
这样避免频繁内存移动,提高效率。 - 空间整理与扩容
当尾部空间不足时,先尝试把可读数据移动到头部,利用头部空闲空间;
如果总空间仍不足,再对 vector 扩容。 - 按行读取支持
提供FindCRLF、GetLine、GetLineAndPop,方便 HTTP 等文本协议按行解析。
4. 成员变量与接口拆解
4.1 成员变量
c++
std::vector<char> _buffer; // 底层内存
uint64_t _reader_idx; // 读偏移
uint64_t _writer_idx; // 写偏移
_buffer:用std::vector<char>管理内存,自动扩容。_reader_idx:下一个待读取数据的位置。_writer_idx:下一个待写入数据的位置。- 可读数据区间:
[_reader_idx, _writer_idx)。 - 空闲空间:
- 头部空闲:
[0, _reader_idx) - 尾部空闲:
[_writer_idx, _buffer.size())
- 头部空闲:
4.2 获取位置
c++
char *Begin() { return &*_buffer.begin(); }
char *WritePosition() { return Begin() + _writer_idx; }
char *ReadPosition() { return Begin() + _reader_idx; }
Begin():返回 vector 首元素地址。&*_buffer.begin()是获取首地址的惯用写法。WritePosition():写起始地址。ReadPosition():读起始地址。
4.3 空闲空间计算
c++
uint64_t TailIdleSize() { return _buffer.size() - _writer_idx; }
uint64_t HeadIdleSize() { return _reader_idx; }
uint64_t ReadAbleSize() { return _writer_idx - _reader_idx; }
TailIdleSize():写偏移之后的空闲空间。HeadIdleSize():读偏移之前的空闲空间。ReadAbleSize():可读数据大小。
4.4 移动偏移
c++
void MoveReadOffset(uint64_t len) {
assert(len <= ReadAbleSize());
_reader_idx += len;
}
void MoveWriteOffset(uint64_t len) {
assert(len <= TailIdleSize());
_writer_idx += len;
}
- 移动前用
assert检查越界,Debug 下可及时发现问题。 - 读偏移后移表示数据已被消费。
- 写偏移后移表示数据已写入。
4.5 确保可写空间:整理与扩容
c++
void EnsureWriteSpace(uint64_t len) {
if (TailIdleSize() >= len) return;
if (len <= TailIdleSize() + HeadIdleSize()) {
uint64_t rsz = ReadAbleSize();
std::copy(ReadPosition(), ReadPosition() + rsz, Begin());
_reader_idx = 0;
_writer_idx = rsz;
} else {
_buffer.resize(_writer_idx + len);
}
}
这是 Buffer 最核心的函数之一:
- 尾部空间足够:直接返回。
- 尾部不够,但头部+尾部够 :把可读数据整体移动到头部。
- 保存可读数据大小
rsz。 - 用
std::copy把数据搬到Begin()。 _reader_idx = 0,_writer_idx = rsz。- 这样尾部就腾出了空间。
- 保存可读数据大小
- 总空间不够 :直接扩容到
_writer_idx + len,保证能写下新数据。- 不移动数据,因为移动也没用,总空间不够。
为什么先移动再扩容?
移动可以复用头部空闲空间,减少内存分配次数,提高性能。
4.6 写入数据
c++
void Write(const void *data, uint64_t len) {
EnsureWriteSpace(len);
const char *d = (const char *)data;
std::copy(d, d + len, WritePosition());
}
void WriteAndPush(const void *data, uint64_t len) {
Write(data, len);
MoveWriteOffset(len);
}
Write:只拷贝数据,不移动写偏移。WriteAndPush:拷贝后移动写偏移,表示数据已写入。- 其他写入接口都是对这两个的封装:
WriteString/WriteStringAndPush:写入std::string。WriteBuffer/WriteBufferAndPush:把另一个 Buffer 的可读数据写入当前 Buffer。
4.7 读取数据
c++
void Read(void *buf, uint64_t len) {
assert(len <= ReadAbleSize());
std::copy(ReadPosition(), ReadPosition() + len, (char *)buf);
}
void ReadAndPop(void *buf, int len) {
Read(buf, len);
MoveReadOffset(len);
}
Read:只拷贝数据,不移动读偏移。ReadAndPop:读取后移动读偏移,表示数据已消费。ReadAsString/ReadAsStringAndPop:读取为std::string。
注意:ReadAndPop 的参数是 int len,而其他接口是 uint64_t,类型不一致,建议统一为 uint64_t。
4.8 按行读取
c++
char *FindCRLF() {
char *res = (char *)memchr(ReadPosition(), '\n', ReadAbleSize());
return res;
}
std::string GetLine() {
char *pos = FindCRLF();
if (pos == NULL) return "";
return ReadAsString(pos - ReadPosition() + 1);
}
std::string GetLlineAndPop() { // 拼写错误,应为 GetLineAndPop
std::string str = GetLine();
MoveReadOffset(str.size());
return str;
}
FindCRLF():在可读数据中查找'\n',返回指针。GetLine():从读位置到'\n'(包含'\n')读取为字符串。如果没找到换行,返回空串。GetLineAndPop():读取一行并移动读偏移。
为什么找 '\n' 而不是 "\r\n"?
HTTP 协议中行结束可能是 \r\n 或 \n,查找 \n 更通用,解析时再处理 \r。
注意函数名拼写错误:GetLlineAndPop 应为 GetLineAndPop。
4.9 清空缓冲区
c++
void Clear() {
_reader_idx = 0;
_writer_idx = 0;
}
只重置偏移量,不释放内存,下次写入直接覆盖。
5. 关键流程
5.1 写入流程
- 调用
Write或WriteAndPush。 EnsureWriteSpace(len)确保空间足够:- 尾部够 → 直接写。
- 尾部不够但总空闲够 → 数据移到头部,再写。
- 总空间不够 → 扩容,再写。
std::copy拷贝数据到WritePosition()。- 如果是
WriteAndPush,MoveWriteOffset(len)。
5.2 读取流程
- 调用
Read或ReadAndPop。 assert检查可读数据足够。std::copy从ReadPosition()拷贝到目标缓冲区。- 如果是
ReadAndPop,MoveReadOffset(len)。
5.3 按行解析流程
FindCRLF()用memchr找'\n'。GetLine()计算长度并读取。GetLineAndPop()读取后移动读偏移。- 如果没找到
'\n',返回空串,等待更多数据。
6. 使用示例
c++
Buffer buf;
std::string data = "GET /hello HTTP/1.1\r\nHost: localhost\r\n\r\n";
buf.WriteStringAndPush(data);
std::string line = buf.GetLineAndPop(); // "GET /hello HTTP/1.1\r\n"
std::cout << line;
while (true) {
std::string l = buf.GetLineAndPop();
if (l.empty()) break;
std::cout << l;
}