云计算百科
云计算领域专业知识百科平台

C++ 仿 muduo 高并发服务器:用户态缓冲区 Buffer 模块实现

C++ 仿 muduo 高并发服务器:用户态缓冲区 Buffer 模块实现


1. 为什么需要 Buffer?

在非阻塞 I/O 的网络编程中,read 和 write 不保证一次就能读完或写完所有数据:

  • 读半包:一次 read 可能只收到请求的一部分,需要暂存起来,等下次数据到达再拼接。
  • 写半包:一次 write 可能只发送了部分数据,剩余数据需要缓冲,等 socket 可写时继续发送。
  • 粘包:多次发送的数据可能被合并成一个 TCP 包到达,需要按协议格式拆分。

因此,服务器需要一个用户态缓冲区,在应用层管理数据的接收和发送。
muduo 中的 Buffer 就是这样一个组件,本项目仿照它实现了一个简化版。


2. 代码总览

#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 成员变量

    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 获取位置

    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 空闲空间计算

    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 移动偏移

    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 确保可写空间:整理与扩容

    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 写入数据

    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 读取数据

    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 按行读取

    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 清空缓冲区

    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. 使用示例

    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;
    }


    赞(0)
    未经允许不得转载:网硕互联帮助中心 » C++ 仿 muduo 高并发服务器:用户态缓冲区 Buffer 模块实现
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!