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

相关推荐
坐望云起1 小时前
机器学习笔记 - 基于C++的深度学习 四、实现梯度下降
笔记·深度学习·机器学习
码途漫谈1 小时前
把笔记变成可生长的知识系统:Obsidian 技术介绍
笔记·ai·obsidian
坚果派·白晓明1 小时前
【鸿蒙PC三方库移植适配框架解读系列】第六篇:关键注意事项与最佳实践
c语言·开发语言·c++·华为·harmonyos·开源鸿蒙
郝学胜-神的一滴1 小时前
中级OpenGL教程 005:为球体&平面注入法线灵魂
c++·unity·图形渲染·three.js·opengl·unreal
承渊政道1 小时前
【贪心算法】(经典实战应用解析(二):最⻓递增⼦序列、递增的三元⼦序列、最⻓连续递增序列、买卖股票的最佳时机、买卖股票的最佳时机II)
数据结构·c++·学习·算法·leetcode·贪心算法·哈希算法
li星野1 小时前
动态规划十题通关:从爬楼梯到编辑距离(Python + C++)
c++·python·学习·算法·动态规划
康谋自动驾驶1 小时前
CoppeliaSim 机器人仿真平台全解析:功能、对比与选型指南
经验分享
披着假发的程序唐1 小时前
STM32 H743 MPU的配置使用方法
linux·c语言·c++·驱动开发·stm32·单片机·mcu
Tutankaaa1 小时前
学校知识竞赛怎么组织?从班级到年级的进阶方案
经验分享·学习·算法·职场和发展