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

相关推荐
小七-七牛开发者10 小时前
TokenPilot:让 LLM Agent 长会话成本降 60%+ 的上下文管理
缓存·agent·token·context·上下文·推理成本
博客18002 天前
酷宝的使用方法,超好用的免费界面库,C++、MFC可用
c++·mfc·界面库·库来帮·酷宝
郝学胜_神的一滴2 天前
CMake 026:属性体系精讲、四大作用域全解 & 实战代码落地
c++·cmake
众少成多积小致巨2 天前
JNI (Java Native Interface) 技术手册中文参考指南
android·java·c++
LinXunFeng3 天前
Obsidian - 使用 Share Note 分享笔记并自部署
前端·笔记·github
zzzzzz3104 天前
9K Star 炸裂开源!这个 C 语言写的代码知识图谱,把 Linux 内核索引压缩到了 3 分钟
linux·服务器·sql
clint4567 天前
C++进阶(1)——前景提要
c++
夜悊7 天前
C++代码示例:进制数简单生成工具
c++
郝学胜_神的一滴7 天前
CMake 021: IF 条件判据详诠
c++·cmake
_wyt0017 天前
洛谷 B3930 [GESP202312 五级] 烹饪问题 题解
c++·gesp