用户态网络缓冲区设计与实现

一、理论基础

用户态网络缓冲区设计的本质

用一层可控的用户态内存把“字节流 ↔ 消息边界”和“生产 ↔ 消费速率”解耦,同时通过 readv/writev 降低拷贝与系统调用成本。

1.1 用户态网络缓冲区是什么

  1. 定义

    • 用户态网络缓冲区(user-space network buffer)是运行在应用进程地址空间的内存区域,用于缓存从内核 socket/网络栈取出的接收数据,或缓存要写入内核/网卡但尚未发出的发送数据。
  2. 典型形式

    • 固定大小数组(Fixed buffer)
    • 环形缓冲区(Ring buffer / circular buffer)
    • 链式缓冲区(Chainbuffer,若干可变块或段组成的链表或双端队列)
  3. 放在哪里、由谁管理

    • 完全由应用分配和管理(malloc/new 或内存池),在用户态通过应用逻辑读写;在单线程 reactor 中通常无锁访问;在多线程场景需要同步。
  4. 为什么不是只用内核缓冲

    • TCP 是字节流,内核不负责消息边界;
    • 速率常常不匹配(应用处理慢/网络发送慢);
    • 减少系统调用与拷贝,做批量化处理(readv/writev

1.2 解决了什么问题(为什么需要)

  1. **粘包 / 半包问题

    • TCP 是字节流,应用每次 recv/read 可能获得半个包或多个包拼在一起。用户态缓冲区用于保存“未完整的报文片段”,直到可以拼成一个完整报文再交给上层解析器。
  2. 生产者速度 > 消费者速度

    • 内核把数据尽快交到用户态,但应用处理较慢。用户态缓冲区做“临时池”,防止数据被丢弃或影响 TCP 流控(TCP 层在内核层也有发送/接收缓冲)。
  3. 减少系统调用开销 / 批量处理

    • 把内核到用户的交互批量化(一次 readv 读多个片段,或把多个应用消息合并后一次处理),减少 syscall 次数,提高吞吐。
  4. 处理部分发送(发送缓冲)

    • 非阻塞 send 可能只写出部分数据,剩余部分需要放在用户态发送缓冲区等待后续发送或 retry(或由内核 TCP 发送缓冲承担,但有些应用协议在用户态也要保留消息队列)。

1.3 怎么解决(设计)

关键词:消息边界恢复 + 速率解耦 + 低拷贝 + 可伸缩

  • 数据结构选择

    • 定长 buffer:简单;伸缩差、容易 memmove。
    • ringbuffer:不回绕数据、只回绕索引;减少搬移,但数据可能分两段。
    • chainbuffer:按需分配小块,扩展灵活;适配 readv/writev;跨块搜索要做好。
  • 增长策略

    • 懒腾挪:仅当必须时,把未读数据 memmove 到首部(释放尾部空间)。
    • 渐进扩容:不足才扩;一般扩大 1/2~1 倍或“至少够这次 + α”。
    • 上限控制:每连接(或总量)设置硬上限,防 DoS/OOM;超限触发 backpressure:暂停读、丢弃或断开。
  • 解析接口

    • peek(n)read(n)retrieve(n)find(delim)peekUint32() 等,支持跨块读取与长度字段解析。
    • 状态机:长度前缀协议通常实现成“读长度 → 等待足够数据 → 取包”的简单状态机。
  • 零拷贝/少拷贝

    • 散/聚 I/Oreadv() 直接把数据散落写入多块;writev() 把多块一次发送。
    • 若需要连续内存(调用某些库/系统 API),提供 coalesce() 临时合并。
  • 并发模型

    • 单线程 reactor:缓冲区无锁即可。
    • 多线程:SPSC/MPSC 队列或细粒度锁;谨慎跨线程共享缓冲。

1.4 UDP 和 TCP 设计是否一样

UDP(报文导向、不可靠投递)

  • 内核交付天然有边界:一次 recvfrom 拿到一个报文,无粘包问题

  • 没有可靠性机制:不重传、不拥塞控制;最大报文约 64KB(实践上 < MTU 更稳妥)。

  • 对用户态缓冲的影响:通常无需为粘包特意设计;但仍可用缓冲来平滑速率或做重试/排序的应用级逻辑。

TCP(字节流、可靠传输)

  • 必须做“粘包/半包处理”:应用端负责边界恢复。

  • 内核会做:TCP 分段(MSS)、重传、拥塞控制;IP 层可能分片/重组。

  • 对用户态缓冲的影响:既要收方向解析,又要发方向排队/续发,缓冲与状态机更复杂。

UDP 在 Linux 里也有 socket 发送/接收队列和 SO_SNDBUF/SO_RCVBUF 限制,但不承担可靠性/重传的职责;和 TCP 的“可靠发送缓冲”的语义不同。

1.5 粘包处理

  1. 分隔符协议(如 \r\n0x00

    • 适合文本协议:HTTP(行)、SMTP、Redis(CRLF)。
    • 实现:从读缓冲中扫描分隔符,找到就切包。
    • 细节:
      • 分隔符跨块时的匹配;
      • 避免 O(N²) 扫描,可用 KMP/滑动窗口;
      • 二进制数据要转义或保证不会出现分隔符。
  2. 长度前缀协议

    • 在包头放长度(2/4 字节,指定字节序),流程:
      ① 保证至少读到“长度字段”;② 若缓冲中数据 < 4 + len,继续读;③ 足够时一次性取走完整包。
    • 细节:
      • 上限校验:拒绝超大长度(防 OOM);
      • 字节序(network/big-endian)统一;
      • 解析 API 设计为 peek length -> 检查 -> retrieve

1.6 Reactor 和 Proactor 是否一样

  • 相同点:都为了解决异步 I/O问题,让线程不会长时间阻塞,提高并发。

  • 不同点(核心)

    • Reactor = 就绪通知:内核告诉你“可读/可写了”,应用自己去读/写(epoll/select/poll、libevent)。
    • Proactor = 完成通知:应用先把“读/写任务 + 缓冲”交给内核(或 IO 引擎),完成后再通知应用(Windows IOCP;Linux 的 io_uring 更接近这个模型)。
  • 对缓冲区设计的影响

    • Reactor:应用在回调里把数据 recv用户态缓冲再解析;写侧在回调里 write / writev用户态写缓冲送出。
    • Proactor:应用提前提供/注册缓冲区,完成时直接可用数据;更友好于零拷贝,但需要更复杂的生命周期管理。

1.7 内核协议栈收发流程

接收(RX)

  1. 网卡 DMA:帧进 NIC -> 直接 DMA 到内核预分配的 RX ring(不用 CPU 搬运)。
  2. 中断 / NAPI:高负载启用中断合并,触发后转为 NAPI 轮询 降低抖动。
  3. 软中断NET_RX_SOFTIRQ 中从 ring 取数据,封装成 skb
  4. 链路层:解析以太网头,识别协议类型(IPv4/IPv6/ARP)。
  5. IP 层:校验、(必要时)分片重组、路由判定,投递到传输层。
  6. 传输层:TCP/UDP 根据五元组匹配 socket。
  7. socket 接收队列sk_rcvbuf 入队。
  8. 应用recv/read 取出 -> 用户态读缓冲 -> 粘包解析 -> 业务处理。

发送(TX)

  1. 应用写入send/write -> TCP 进 sk_sndbuf;UDP 通常直接封装 skb 进入发送路径。
  2. 分段/封装:TCP 头 -> IP 头 -> MAC 头/尾;可启用 GSO/TSO 优化分段。
  3. 调度:qdisc(流量整形/优先级),dev_queue_xmit() 交给驱动。
  4. 驱动/TX ring:映射到 TX ring(DMA 可见)。
  5. 网卡发送:DMA 取数据发到物理链路。
  6. 完成中断/回收:释放 skb & DMA 资源。
  7. TCP ACK:释放已确认数据的内核发送缓冲,更新拥塞窗口;应用写缓冲可继续吐。

在用户态的位置:RX:socket->用户读缓冲解析TX:用户写缓冲->socket。两侧都由缓冲对“抖动与速率错配”做“软着陆”。


二、数据结构

2.1 数据结构对比

定长 buffer

  • 优点:实现简单、连续内存、解析方便。

  • 缺点

    1. 内存浪费:分配过大无效占用。
    2. 伸缩性差:超出就崩或频繁重分配。
    3. 频繁腾挪:每次消费后把余下数据 memmove 到首部,O(N) 成本。
  • 适用:固定长度消息或强约束场景。

在这里插入图片描述

ringbuffer

  • 核心思想:读写索引回绕,不回绕数据;降低 memmove。

  • 缺点

    1. 内存浪费/伸缩差:容量固定,扩容复杂。
    2. 数据离散:可读区可能分为“尾段+头段”两块。
    3. 接口复杂:解析时经常要处理跨越两段。
  • 优化:把两段映射成两个 iovec,用 writev 一次写出(解决“离散”问题的标准做法)。

在这里插入图片描述

chainbuffer

  • 核心思想:由多个小块(block)链起,按需增长;读写仅移动块上的 head/tail,不整体搬移。

  • 优势

    • 伸缩性好:按需多块扩展。
    • 少拷贝:天然适配 readv/writev
    • 解析友好:提供跨块 peek/find/retrieve;必要时 coalesce()
  • 代价:块管理开销、跨块搜索复杂;需内存池/对象池优化。

  • 业界实现libeventevbuffer 就是成熟的 chainbuffer。

在这里插入图片描述

2.2 readv/writev/iovec 如何解决“数据离散”

  • 问题:ring/chain 下,数据可能散落在多段,若逐段 write 会多次 syscalls 或需要先合并拷贝。

  • 解法:把多段组成 iovec[]

    struct iovec { void* iov_base; size_t iov_len; };
    ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
    ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
    
  • 发送:多块一次性写出(Gather Write);接收:把内核数据直接“散落”填入多个空块(Scatter Read)。

  • 收益:减少系统调用次数,减少/避免中间拷贝;非常适合 chainbuffer,也能弥补 ringbuffer 的“两段性”。


三、代码实现

基于 vector 的线性缓冲 + 懒腾挪 + 扩容 1/2 + readv 溢出到栈缓冲再并入

3.1 设计:动态 vector 缓冲区

  • 设计思想

    1. 底层用 std::vector<uint8_t> 管理内存,天然支持动态扩容。
    2. rpos_(读指针)和 wpos_(写指针)标记数据区,避免数据覆盖。
    3. 不做“环绕”,而是通过 Normalize(腾挪数据到首部)来回收空间。
    4. 如果腾挪后依然不够,就 扩容(扩容时至少扩大 50%)。
  • 优点

    • 避免频繁搬移(仅在必要时腾挪一次)。
    • 动态伸缩,内存利用率更高。
    • 数据连续存放(对上层解析友好)。

3.2 设计要点

(1)类结构

  • buffer_:实际存储容器(vector)。
  • rpos_:读位置。
  • wpos_:写位置。

(2)数据访问接口

  • GetReadPointer():当前可读数据的起始位置。
  • GetWritePointer():当前可写数据的起始位置。
  • ReadCompleted(size) / WriteCompleted(size):移动指针。

把指针逻辑封装起来,避免调用者直接操作原始内存。

(3)内存管理策略

  • Normalize():当读了部分数据后,把剩余数据整体挪到首部,释放尾部空间。
  • EnsureFreeSpace(size)
    1. 如果总容量 - 活跃数据 < 需求 —> 需要扩容。
    2. 扩容时:先 Normalize,再扩展 vector(原来的一半或刚好够)。

(4)写入

void Write(const uint8_t *data, std::size_t size) {
    EnsureFreeSpace(size); // 保证空间足够
    std::memcpy(GetWritePointer(), data, size);
    WriteCompleted(size);
}
  • 每次写入前先检查是否要腾挪/扩容。
  • 数据写入后,移动 wpos_

(5)粘包处理

std::pair<uint8_t *, std::size_t> GetDataUntilCRLF()
  • 在缓冲区中查找 \r\n 分隔符,提取一个完整数据包。
  • 如果没找到,就返回空,等待更多数据。
  • 典型应用:HTTP 协议、文本协议。

(6)readv 优化读取(零拷贝思路)

struct iovec iov[2];
iov[0].iov_base = GetWritePointer();
iov[0].iov_len = GetFreeSize();
iov[1].iov_base = extra;
iov[1].iov_len = sizeof(extra);
std::size_t n = readv(fd, iov, 2);
  • iov[0]:直接写到缓冲区剩余空间。
  • iov[1]:用栈上的临时数组接收“溢出数据”。
  • 如果溢出,再写入 buffer(触发扩容)。
  • 避免多次系统调用。
  • 避免数据先读到栈再拷贝到堆(常见低效写法)。

3.3 完整代码

#pragma once

#include <bits/types/struct_iovec.h>
#include <stdint.h>
#include <vector>
#include <cstring>
#include <sys/uio.h>
#include <errno.h>

class MessageBuffer
{
public:
    MessageBuffer() : rpos_(0), wpos_(0)
    {
        buffer_.resize(4096); // Initial size
    }

    explicit MessageBuffer(std::size_t size) : rpos_(0), wpos_(0)
    {
        buffer_.resize(size);
    }

    // 不允许拷贝
    MessageBuffer(const MessageBuffer &) = delete;
    MessageBuffer &operator=(const MessageBuffer &) = delete;

    // 允许移动
    MessageBuffer(MessageBuffer &&other) noexcept
        : buffer_(std::move(other.buffer_)), rpos_(other.rpos_), wpos_(other.wpos_)
    {
        other.rpos_ = 0;
        other.wpos_ = 0;
    }

    MessageBuffer &operator=(MessageBuffer &&other) noexcept
    {
        if (this != &other)
        {
            buffer_ = std::move(other.buffer_);
            rpos_ = other.rpos_;
            wpos_ = other.wpos_;
            other.rpos_ = 0;
            other.wpos_ = 0;
        }
        return *this;
    }

    uint8_t *GetBasePointer()
    {
        return buffer_.data();
    }

    uint8_t *GetReadPointer()
    {
        return buffer_.data() + rpos_;
    }

    uint8_t *GetWritePointer()
    {
        return buffer_.data() + wpos_;
    }

    void ReadCompleted(std::size_t size)
    {
        rpos_ += size;
    }

    void WriteCompleted(std::size_t size)
    {
        wpos_ += size;
    }

    std::size_t GetActiveSize() const
    {
        return wpos_ - rpos_;
    }

    std::size_t GetFreeSize() const
    {
        return buffer_.size() - wpos_;
    }

    std::size_t GetBufferSize() const
    {
        return buffer_.size();
    }

    // 数据腾挪到最前面
    void Normalize()
    {
        if (rpos_ > 0)
        {
            std::memmove(buffer_.data(), buffer_.data() + rpos_, GetActiveSize());
            wpos_ -= rpos_;
            rpos_ = 0;
        }
    }

    // 检查是否需要扩容
    // n = read();  --->  确认缓冲区剩余空间是否足够
    // 1. 将已使用的数据腾挪到首部后空间足够  2. 腾挪后空间也不够  3. 剩余空间足够(不用考虑)
    void EnsureFreeSpace(std::size_t size)
    {
        if (GetBufferSize() - GetActiveSize() < size)
        {
            Normalize();
            buffer_.resize(buffer_.size() + std::max(size, buffer_.size() / 2));
        }
        else if (GetFreeSize() < size)
        {
            Normalize();
        }
    }

    // windows iocp  boost.asio
    void Write(const uint8_t *data, std::size_t size)
    {
        if (size > 0)
        {
            EnsureFreeSpace(size);
            std::memcpy(GetWritePointer(), data, size);
            WriteCompleted(size);
        }
    }

    std::pair<uint8_t *, std::size_t> GetAllData()
    {
        return {GetReadPointer(), GetActiveSize()};
    }

    // 获取第一个 \r\n 之前的数据的指针和大小(若未找到返回nullptr和0)
    std::pair<uint8_t *, std::size_t> GetDataUntilCRLF()
    {
        uint8_t *data = GetReadPointer();
        std::size_t active_size = GetActiveSize();
        for (std::size_t i = 0; i < active_size - 1; ++i)
        {
            if (data[i] == '\r' && data[i + 1] == '\n')
            {
                return {data, i}; // 数据长度为i,不包含\r\n
            }
        }
        return {nullptr, 0}; // 未找到
    }

    // linux reactor readv
    // 1. 尽可能的不腾挪数据
    // 2. 避免了每次都从栈上拷贝到堆上
    int Recv(int fd, int *err)
    {
        char extra[65535]; // 65535 UDP 最大值
        struct iovec iov[2];
        iov[0].iov_base = GetWritePointer();
        iov[0].iov_len = GetFreeSize();
        iov[1].iov_base = extra;
        iov[1].iov_len = sizeof(extra);
        std::size_t n = readv(fd, iov, 2);
        if (n < 0)  // 错误
        {
            *err = errno;
            return n;
        }
        else if (n == 0)
        {
            *err = ECONNRESET;
            return 0;
        }
        else if (n <= GetFreeSize())
        {
            WriteCompleted(n);
            return n;
        }
        else // 后面的剩余空间不够
        {
            // WRN: GetfreeSize() 在 WriteCompleted() 中会被更新, extra_size 需要提前计算
            std::size_t extra_size = n - GetFreeSize();
            WriteCompleted(GetFreeSize());  // 已经用完剩余空间
            Write(reinterpret_cast<uint8_t *>(extra), extra_size);
            return n;
        }
    }

private:
    std::vector<uint8_t> buffer_;
    std::size_t rpos_;
    std::size_t wpos_;
};

3.4 测试代码

#include <iostream>
#include <cassert>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include "MessageBuffer.h"

// 辅助打印函数
void PrintTestResult(const char* test_name, bool passed) {
    std::cout << test_name << ": " << (passed ? "PASSED" : "FAILED") << std::endl;
}

// 测试1: 基本读写操作
void TestBasicReadWrite() {
    MessageBuffer buf;
    bool passed = true;

    // 验证初始状态
    passed &= (buf.GetActiveSize() == 0);
    passed &= (buf.GetFreeSize() == buf.GetBufferSize());

    // 写入数据
    const char* test_data = "ABCDEFGH";
    buf.Write(reinterpret_cast<const uint8_t*>(test_data), 8);
    
    // 验证写入后状态
    passed &= (buf.GetActiveSize() == 8);
    passed &= (buf.GetFreeSize() == buf.GetBufferSize() - 8);
    
    // 验证数据内容
    auto all_data = buf.GetAllData();
    passed &= (all_data.second == 8);
    passed &= (memcmp(all_data.first, test_data, 8) == 0);

    // 读取部分数据
    buf.ReadCompleted(4);
    passed &= (buf.GetActiveSize() == 4);
    passed &= (memcmp(buf.GetReadPointer(), "EFGH", 4) == 0);

    PrintTestResult("TestBasicReadWrite", passed);
}

// 测试2: 缓冲区扩容
void TestBufferExpansion() {
    MessageBuffer buf(4);  // 初始大小4字节
    bool passed = true;

    // 写入3字节(剩余1字节)
    buf.Write(reinterpret_cast<const uint8_t*>("123"), 3);
    passed &= (buf.GetBufferSize() == 4);

    // 再写入3字节(触发扩容)
    buf.Write(reinterpret_cast<const uint8_t*>("456"), 3);
    passed &= (buf.GetBufferSize() > 4);  // 应扩容至4 + max(3, 2) = 6
    
    // 验证数据完整性
    auto all_data = buf.GetAllData();
    passed &= (all_data.second == 6);
    passed &= (memcmp(all_data.first, "123456", 6) == 0);

    PrintTestResult("TestBufferExpansion", passed);
}

// 测试3: 标准化操作
void TestNormalization() {
    MessageBuffer buf(8);
    bool passed = true;

    // 写入8字节
    buf.Write(reinterpret_cast<const uint8_t*>("12345678"), 8);
    buf.ReadCompleted(4);  // 读走前4字节
    
    // 标准化前状态
    passed &= (buf.GetReadPointer() - buf.GetBasePointer() == 4);
    passed &= (buf.GetActiveSize() == 4);

    // 执行标准化
    buf.Normalize();
    
    // 标准化后状态
    passed &= (buf.GetReadPointer() == buf.GetBasePointer());
    passed &= (buf.GetActiveSize() == 4);
    passed &= (memcmp(buf.GetReadPointer(), "5678", 4) == 0);

    PrintTestResult("TestNormalization", passed);
}

// 测试4: 获取完整数据
void TestGetAllData() {
    MessageBuffer buf;
    bool passed = true;

    // 空缓冲区测试
    auto empty_data = buf.GetAllData();
    passed &= (empty_data.second == 0);

    // 分片写入测试
    buf.Write(reinterpret_cast<const uint8_t*>("Hello"), 5);
    buf.Write(reinterpret_cast<const uint8_t*>(" World"), 6);
    
    auto all_data = buf.GetAllData();
    passed &= (all_data.second == 11);
    passed &= (memcmp(all_data.first, "Hello World", 11) == 0);

    PrintTestResult("TestGetAllData", passed);
}

// 测试5: 查找CRLF
void TestGetDataUntilCRLF() {
    MessageBuffer buf;
    bool passed = true;

    // 测试无CRLF情况
    buf.Write(reinterpret_cast<const uint8_t*>("NO_CRLF_HERE"), 11);
    auto crlf_data = buf.GetDataUntilCRLF();
    passed &= (crlf_data.first == nullptr);
    passed &= (crlf_data.second == 0);

    // 测试有CRLF情况
    buf.Write(reinterpret_cast<const uint8_t*>("\r\nNextLine"), 10);
    crlf_data = buf.GetDataUntilCRLF();
    passed &= (crlf_data.second == 11);  // "NO_CRLF_HERE" + \r之前的长度
    passed &= (memcmp(crlf_data.first, "NO_CRLF_HERE", 11) == 0);

    // 测试多个CRLF情况
    MessageBuffer buf2;
    buf2.Write(reinterpret_cast<const uint8_t*>("First\r\nSecond\r\nThird"), 17);
    auto first_crlf = buf2.GetDataUntilCRLF();
    passed &= (first_crlf.second == 5);  // "First"
    passed &= (memcmp(first_crlf.first, "First", 5) == 0);

    PrintTestResult("TestGetDataUntilCRLF", passed);
}

void TestRecvLogic() {
    MessageBuffer buf(4);  // 初始缓冲区大小4字节
    int err = 0;
    bool passed = true;

    // 模拟读取数据:主缓冲区剩余2字节,额外数据3字节
    // 假设通过mock或pipe提供输入数据 "12345"
    int fds[2];
    pipe(fds);
    write(fds[1], "12345", 5);
    close(fds[1]);

    // 读取5字节(主缓冲区4字节,实际空闲2字节)
    ssize_t n = buf.Recv(fds[0], &err);
    passed &= (n == 5);
    passed &= (buf.GetActiveSize() == 5);
    
    // 验证数据内容
    auto data = buf.GetAllData();
    passed &= (memcmp(data.first, "12345", 5) == 0);

    close(fds[0]);
    PrintTestResult("TestRecvLogic", passed);
}

int main() {
    TestBasicReadWrite();
    TestBufferExpansion();
    TestNormalization();
    TestGetAllData();
    TestGetDataUntilCRLF();
    TestRecvLogic();
    std::cout << "\nAll tests completed." << std::endl;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值