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

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


1. 为什么需要 Buffer?

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

  • 读半包 :一次 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 的设计围绕三个核心:

  1. 读写偏移分离
    _reader_idx_writer_idx 标记读位置和写位置,而不是每次读走数据就删除 vector 头部。
    这样避免频繁内存移动,提高效率。
  2. 空间整理与扩容
    当尾部空间不足时,先尝试把可读数据移动到头部,利用头部空闲空间;
    如果总空间仍不足,再对 vector 扩容。
  3. 按行读取支持
    提供 FindCRLFGetLineGetLineAndPop,方便 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 最核心的函数之一:

  1. 尾部空间足够:直接返回。
  2. 尾部不够,但头部+尾部够 :把可读数据整体移动到头部。
    • 保存可读数据大小 rsz
    • std::copy 把数据搬到 Begin()
    • _reader_idx = 0_writer_idx = rsz
    • 这样尾部就腾出了空间。
  3. 总空间不够 :直接扩容到 _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 写入流程

  1. 调用 WriteWriteAndPush
  2. EnsureWriteSpace(len) 确保空间足够:
    • 尾部够 → 直接写。
    • 尾部不够但总空闲够 → 数据移到头部,再写。
    • 总空间不够 → 扩容,再写。
  3. std::copy 拷贝数据到 WritePosition()
  4. 如果是 WriteAndPushMoveWriteOffset(len)

5.2 读取流程

  1. 调用 ReadReadAndPop
  2. assert 检查可读数据足够。
  3. std::copyReadPosition() 拷贝到目标缓冲区。
  4. 如果是 ReadAndPopMoveReadOffset(len)

5.3 按行解析流程

  1. FindCRLF()memchr'\n'
  2. GetLine() 计算长度并读取。
  3. GetLineAndPop() 读取后移动读偏移。
  4. 如果没找到 '\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;
}

相关推荐
Java红桃峰峰日拱一卒3 小时前
在jdk8的centos上安装jdk17
linux·jdk17·jdk8
晴天的雨.9924 小时前
【C++算法】和为s的两个数
开发语言·数据结构·c++·算法
M78佐菲9 小时前
ARM学习笔记(1)
linux·arm开发·笔记·嵌入式硬件·学习
益达丫10 小时前
Socket编程揭秘:端口、IP与进程通信的底层逻辑
linux·笔记
chicheese12 小时前
Linux 面试速查表:从初级到高级,附高频场景答题模板
linux·运维
Linux-lucky13 小时前
32-38-Linux学习之旅之MySQL综合
linux·运维·学习·mysql·ubuntu
vortex514 小时前
常用终端模拟器全面对比:Kitty、WezTerm、Ghostty 与 Konsole
linux
知识分享小能手14 小时前
C++ 学习教程,从入门到精通,C++ 入门知识 — 完整知识点(1)
开发语言·c++·学习
qq_1998868714 小时前
第8板块·第2节:统一内存的高级特性与性能调优
c++·人工智能·gpu算力·cuda