RingBuffer:面向网络编程的环形缓冲区实现

RingBuffer:面向网络编程的环形缓冲区实现

一、RingBuffer 是什么

RingBuffer 也叫环形缓冲区,本质上是一段固定大小的连续内存

它的特点是,写到末尾后可以从头继续写,逻辑上看起来像一个环

在网络编程中,RingBuffer 常用于保存 socket 收到但还没处理完的数据,或者保存即将发送但还没写完的数据

比如一次 read 只读到半个 HTTP 请求,就可以先放进 RingBuffer,等下一次数据到来后再继续解析

二、RingBuffer 的核心思想

1 使用 head 和 tail 表示数据范围

这个实现中使用 m_headm_tail 表示缓冲区中数据的起点和终点

m_head 指向可读数据的开始位置

m_tail 指向可写数据的结束位置

当前已经使用的空间是 m_tail - m_head

剩余可写空间是 m_size - (m_tail - m_head)

2 逻辑下标不断增长,物理下标取模

m_headm_tail 不需要每次回绕,而是一直递增

真正访问数组时,再通过取模映射到真实下标

普通写法是 index % m_size

这个实现中为了提高效率,把缓冲区大小调整为 2 的幂,然后使用 index & (m_size - 1) 代替取模

比如 m_tail & (m_size - 1) 就是当前真实写入位置

3 为什么要调整为 2 的幂

因为只有当 m_size 是 2 的幂时,index & (m_size - 1) 才等价于 index % m_size

例如大小为 8 时,m_size - 1 是 7,二进制是 111

此时 index & 7 可以快速得到 0 ~ 7 的循环下标

三、普通读写接口

1 写入数据 add

add 的作用是把用户传入的数据写入 RingBuffer

如果剩余空间不够,直接返回 -1

如果写入位置到数组末尾不够放,就分成两段写

cpp 复制代码
int RingBuffer::add(const char* data, int len) {
    int remain = m_size - (m_tail - m_head);
    if(len > remain) return -1;

    int tail_pos = m_tail & (m_size - 1);
    int first = std::min(len, m_size - tail_pos);

    memcpy(m_buffer + tail_pos, data, first);
    memcpy(m_buffer, data + first, len - first);

    m_tail += len;
    return len;
}

这里的关键是分段写入

第一段从 tail_pos 写到数组末尾

第二段从数组开头继续写

2 读取数据 read

read 的作用是从 RingBuffer 中读取指定长度的数据到用户缓冲区

如果可读数据不够,也返回 -1

cpp 复制代码
int RingBuffer::read(char* dst, int len) {
    int used = m_tail - m_head;
    if(used < len) return -1;

    int head_pos = m_head & (m_size - 1);
    int first = std::min(len, m_size - head_pos);

    memcpy(dst, m_buffer + head_pos, first);
    memcpy(dst + first, m_buffer, len - first);

    m_head += len;
    return len;
}

普通的 addread 更适合业务层手动读写数据

但是在 socket 编程中,还可以进一步优化

四、面向 socket 的读写接口

这个 RingBuffer 的一个特点是,专门提供了 read_fdwrite_fdrecv_fdsend_fd

它们不是简单地先读到临时数组,再拷贝到 RingBuffer

而是直接把 RingBuffer 的空闲区域交给系统调用

这样可以减少一次中间拷贝

五、使用 readv / writev 处理环形空间

1 为什么需要 iovec

环形缓冲区的可写空间或可读数据,可能被数组末尾切成两段

比如当前 tail_pos 在数组靠后的位置,剩余空间可能是:

第一段:tail_pos ~ m_size - 1

第二段:0 ~ 某个位置

readvwritev 支持一次传入多个内存片段,所以刚好适合 RingBuffer

2 writable_iov:为 readv 准备写入空间

writable_iov 用来获取当前 RingBuffer 中可以写入的空间

它最多返回两段 iovec

cpp 复制代码
int RingBuffer::writable_iov(struct iovec iov[2], int to_write) {
    int remain = m_size - (m_tail - m_head);
    remain = std::min(remain, to_write);

    if(remain == 0) return 0;

    int tail_pos = m_tail & (m_size - 1);
    int first = std::min(m_size - tail_pos, remain);

    iov[0].iov_base = m_buffer + tail_pos;
    iov[0].iov_len = first;

    int second = remain - first;
    if(second > 0) {
        iov[1].iov_base = m_buffer;
        iov[1].iov_len = second;
        return 2;
    }

    return 1;
}

这样 readv 就可以把 socket 中的数据直接读到 RingBuffer 中

3 readable_iov:为 writev 准备可读数据

readable_iov 用来获取当前 RingBuffer 中可以发送的数据

它同样最多返回两段 iovec

cpp 复制代码
int RingBuffer::readable_iov(struct iovec iov[2], int to_read) {
    int used = m_tail - m_head;
    used = std::min(used, to_read);

    if(used == 0) return 0;

    int head_pos = m_head & (m_size - 1);
    int first = std::min(m_size - head_pos, used);

    iov[0].iov_base = m_buffer + head_pos;
    iov[0].iov_len = first;

    int second = used - first;
    if(second > 0) {
        iov[1].iov_base = m_buffer;
        iov[1].iov_len = second;
        return 2;
    }

    return 1;
}

这样 writev 就可以直接把 RingBuffer 中的数据写入 socket

六、read_fd 和 write_fd 的实现

1 read_fd:从 fd 读入 RingBuffer

read_fd 内部先调用 writable_iov 获取可写空间

然后通过 readv 把数据直接读入 RingBuffer

cpp 复制代码
int RingBuffer::read_fd(int fd, int to_read) {
    struct iovec iov[2];
    int iov_cnt = writable_iov(iov, to_read);

    if(iov_cnt == 0) {
        errno = ENOSPC;
        return -1;
    }

    ssize_t n = ::readv(fd, iov, iov_cnt);
    if(n > 0) {
        m_tail += static_cast<int>(n);
    }

    return static_cast<int>(n);
}

对于非阻塞 socket,如果返回 -1errnoEAGAINEWOULDBLOCK,说明当前没有数据可读,不应该移动 m_tail

2 write_fd:从 RingBuffer 写入 fd

write_fd 内部先调用 readable_iov 获取可读数据

然后通过 writev 写入 socket

cpp 复制代码
int RingBuffer::write_fd(int fd, int to_write) {
    struct iovec iov[2];
    int iov_cnt = readable_iov(iov, to_write);

    if(iov_cnt == 0) {
        return 0;
    }

    ssize_t n = ::writev(fd, iov, iov_cnt);
    if(n > 0) {
        m_head += static_cast<int>(n);
    }

    return static_cast<int>(n);
}

对于非阻塞 socket,一次 writev 不一定能把所有数据都写完

所以只需要根据实际写入的字节数移动 m_head

剩余数据继续留在 RingBuffer 中,等待下一次 EPOLLOUT 再发送

七、recv_fd 和 send_fd

1 recv_fd

recv_fdread_fd 思路类似

区别是 recv_fd 使用 recvmsg

它可以传入 flags

比如可以支持 MSG_DONTWAITMSG_PEEK 等选项

2 send_fd

send_fdwrite_fd 思路类似

区别是 send_fd 使用 sendmsg

它也可以传入 flags

比如可以使用 MSG_NOSIGNAL,避免向已关闭连接发送数据时触发 SIGPIPE

相关推荐
2401_868534788 小时前
RTOS之RT-Thread & Linux:线程/进程管理与调度核心差异分析
c++·python
IT小白杨9 小时前
防关联用指纹浏览器还是虚拟机好?2026年技术架构深度对比与选型决策
chrome·经验分享·microsoft·架构·安全架构·指纹浏览器
一米阳光86619 小时前
软考(中级)软件设计师核心笔记(6)系统开发基础——需求分析、系统设计
笔记·职场发展·需求分析·软考·软件设计师·中级职称
郝学胜-神的一滴9 小时前
Qt 高级编程 040:按钮悬浮弹出滑块弹窗的完整攻略
开发语言·c++·qt·软件工程·用户界面
2301_7807896610 小时前
DDoS 攻击溯源:DNS 水印标记 + 区块链存证的双保险
运维·服务器·网络·云原生·ddos
雨田言炎12 小时前
四、关于Qt项目需要知道的
开发语言·笔记·qt
程序喵大人13 小时前
【C++进阶】STL算法与函数对象 - 02 sort为什么需要随机访问迭代器
开发语言·c++·算法
hanlin0313 小时前
动态规划专练:力扣第121、122题
笔记·算法·leetcode
ZhangShao060714 小时前
题解:CF2249B
c++·算法
摇滚侠14 小时前
SpringBoot 官网 阅读笔记 启用生产就绪功能 端点
spring boot·笔记·后端