1. 2.3 RingBuffer 与「零拷贝」读路径

2.2 的定界与解析依赖可读字节驻留在固定容量环中;本节说明 zhenyi-base/znet 里 RingBuffer 的职责、API 形态及与单连接读循环的配合(实现与注释以 ringbuffer.go 为准)。

1.1. 2.3.1 为什么需要 RingBuffer?

标准做法是从连接读取数据时,每次分配新 buffer:

buf := make([]byte, 1024)
n, _ := conn.Read(buf)

问题在于:

问题 影响
每次分配新内存 GC 压力大
数据拷贝到新 buffer CPU 浪费
buffer 大小固定 大包浪费,小包不够

RingBuffer 的思路不同:预分配一块固定大小的内存,循环复用。

┌─────────────────────────────────────┐
│ [已读数据] [未读数据] [可用空间]      │
└─────────────────────────────────────┘
           ↑          ↑
          read       write

读数据:移动 read 指针,不分配内存
写数据:追加到 write 位置,不分配内存

1.2. 2.3.2 环形结构

为什么叫"环形"?因为当 write 到达末尾后,会绕回到开头继续写:

初始状态:                         写了一圈后:
┌─────────────────────────────┐    ┌─────────────────────────────┐
│                             │    │ [写]                       │
│                             │    │ [写]              [读]      │
│         [读] → [写]         │    │ [写]              [读]      │
│                             │    └─────────────────────────────┘
└─────────────────────────────┘
write 到达末尾后回到开头,
read 跟着走,两者之间就是未读数据。

为了快速计算位置,缓冲区大小必须是 2 的幂,这样可以用位运算代替取模:

pos := rb.read & rb.mask   // 等价于 rb.read % rb.size,但更快

1.3. 2.3.3 零拷贝读取

"零拷贝"是 RingBuffer 最核心的特性。

普通方式:读数据时拷贝到新 buffer

buf := make([]byte, 1024)  // 分配新内存
copy(buf, sourceData)       // 拷贝数据

零拷贝方式:直接返回底层内存的引用

data := rb.Peek(n)  // 返回 []byte,直接指向 RingBuffer 内部
// 不需要 copy,不需要分配新内存

但有一个问题:数据可能跨越环形边界

┌─────────────────────────────┐
│[可读..]              [可读..]│
└─────────────────────────────┘
                     read       write
数据从 read 一直写到末尾,又从开头写了一段。

zhenyi 的处理方式:

// PeekTwoSlices 返回两个切片,覆盖跨边界的数据
first, second, _ := rb.PeekTwoSlices(offset, length)
// first: 从 read 到末尾的数据
// second: 从开头到 write 的数据

多数时候未读区间在物理上连续(second 为空);仅当写指针已绕回而读指针仍在环的后半段时,逻辑区间才会落在两段物理区间上,频率与负载、缓冲尺寸有关。

1.4. 2.3.4 写入优化:WriteFromReader

普通的做法是先读到临时 buffer,再拷贝到 RingBuffer:

tmp := make([]byte, 4096)
n, _ := conn.Read(tmp)        // 读到临时 buffer
rb.Write(tmp[:n])             // 再拷贝到 RingBuffer(2次拷贝)

zhenyi 的 WriteFromReader 直接把 conn 的数据读入 RingBuffer:

// 直接读入 RingBuffer 内部内存,省掉中间 buffer
n, err := rb.WriteFromReader(conn, 4096)

实现原理:

func (rb *RingBuffer) WriteFromReader(r io.Reader, maxRead int) (int, error) {
    writePos := rb.write & rb.mask

    if endPos <= rb.size {
        // 空间连续,直接读入
        n, err = r.Read(rb.buf[writePos : writePos+free])
    } else {
        // 空间跨边界,分两次读
        n, _ = r.Read(rb.buf[writePos:rb.size])       // 第一段
        n2, _ := r.Read(rb.buf[:free-firstPart])        // 第二段
        n += n2
    }
}

效果:从 2 次拷贝变成 0 次拷贝。

1.5. 2.3.5 对象池

每个连接一个 RingBuffer,如果每次创建新对象,连接多了内存压力大。

zhenyi 用对象池复用:

// 从池获取(默认 4KB)
rb := GetRingBuffer()
defer PutRingBuffer(rb)  // 归还到池

// 池默认 4KB;若与「直接 New 默认 64KB」混用,
// 多连接场景下总占用差一个数量级(源码注释中有数量级估算)。

归还时只回收标准大小的 buffer,非标准大小的直接丢弃让 GC 回收,避免池膨胀。

1.6. 2.3.6 扩容机制

默认 4KB 不够用怎么办?RingBuffer 支持自动扩容:

func (rb *RingBuffer) Grow(n int) bool {
    needed := rb.Len() + n
    newSize := rb.size * 2   // 每次翻倍
    for newSize < needed {
        newSize *= 2
    }
    // 最大不超过 1MB
    if rb.maxSize > 0 && newSize > rb.maxSize {
        return false
    }
    // 分配新 buffer,搬运数据
    newBuf := make([]byte, newSize)
    first, second := rb.PeekAll()
    copy(newBuf, first)
    if len(second) > 0 {
        copy(newBuf[len(first):], second)
    }
}
配置 大小 说明
池默认 4KB 控制内存占用
直接创建默认 64KB 适合单缓冲场景
最大扩容 1MB 防止恶意大包撑爆内存

1.7. 2.3.7 线程安全

RingBuffer 是非线程安全的,设计上就不加锁:

// ⚠️ 同一 RingBuffer 禁止并发调用
// 适用场景:每连接一个 RingBuffer,单 goroutine 驱动读循环

这在 API 上是约束,在「每连接单读协程」模型下却省去锁:

  • 每个连接有自己的 RingBuffer
  • 读循环在单个 goroutine 中运行
  • 不需要加锁,自然就没有锁竞争

这也解释了为什么 zhenyi 的默认模式是每连接 2 个 goroutine:读和写各一个,各自操作各自的逻辑,RingBuffer 只在读 goroutine 中使用,不需要锁。

1.8. 2.3.8 本节要点

  1. RingBuffer:预分配容量、read/write 与掩码寻址;读路径与解析在同一连接上串行使用。
  2. 零拷贝语义Peek* 返回底层切片视图;Discard/后续写入前不得继续使用旧切片(见源码「Peek 与生命周期」)。
  3. 跨段PeekTwoSlices 暴露逻辑连续、物理可能两段的事实。
  4. 池化GetRingBuffer/PutRingBuffer 默认 4KB;非标准尺寸归还时丢弃以免池膨胀。
  5. 扩容GrowMaxSize(如默认 1MB)内尝试扩大;具体策略以 ringbuffer.go 为准。
  6. 线程安全:非线程安全;设计假设为每连接单消费者驱动缓冲。

2.4 节说明 TCP / WebSocket / KCP 如何汇入同一套 net.Conn 抽象。

results matching ""

    No results matching ""