muduo网络库(十三):Buffer 缓冲区类

muduo网络库(十三):Buffer 缓冲区类

muduo网络库(十三):Buffer 缓冲区类

一句话理解 Buffer

把网络收发想象成收快递:

  • 没有驿站:快递员(内核)每次到货都要求你当场签收、当场处理------你手头正忙就完蛋了
  • 有驿站(Buffer) :快递先堆在货架上,你有空了按顺序来取;取走的位置不用立刻清空,货架还能继续堆新包裹
    muduo 的 Buffer 就是网络数据的中转驿站。它是 TCP 连接的"储物间",所有到达的数据先在这里攒着,攒够了完整的消息再交给业务处理;发不出去的数据也暂存在这里,等网络空闲了再发。

为什么必须有应用层缓冲区

初学网络编程很容易忽略缓冲区的必要性,三个原因说明它不可或缺:

1. 非阻塞读:能读多少读多少

socket 设置为非阻塞后,read() 有多少数据就能读多少。一次 EPOLLIN 事件到来,你可能收到 100 字节,也可能收到 10000 字节------读到的数据总得有地方放,这就是 inputBuffer。

2. 非阻塞写:写不完先存着

write() 也可能失败------内核发送缓冲区满了会返回 EAGAIN。剩余没发出去的数据必须暂存在 outputBuffer 里,并注册 EPOLLOUT 事件,等 socket 可写时继续发。

3. TCP 是字节流:粘包与半包

TCP 不保证"消息边界"。客户端连发两条消息,服务器可能一次读到一条半(半包);也可能两条拼在一次读取里(粘包)。必须在 Buffer 里攒数据,凑够完整消息再解析

复制代码
客户端发送: [消息A][消息B]
         ┌─ 一次读: [消息A][消息B]  → 粘包
                   服务器读取 ─────────────────────────┤
                          ├─ 第一次读: [消息A前半]  → 半包
                                          └─ 第二次读: [消息A后半]

设计目标

陈硕在《Linux 多线程服务端编程》中提出了 muduo Buffer 的设计准则:

  • 对外表现为一块连续内存 ,方便 peek() 查看、send() 发送
  • 大小自动增长,不用事先预估数据量
  • 节省内存:空闲时不占用大块内存
  • 读方向一次系统调用尽量多读 ,配合 LT 触发模式减少 epoll 通知次数
    muduo 的实现用四个特性交出答卷:零拷贝消费、内存高效复用、readv 系统调用优化、extrabuf 安全兜底

内存布局:三段式设计

复制代码
┌─────────────────────────────────────────────────────────────┐
│  buffer_ (std::vector<char>)                               │
├────────────┬─────────────────┬──────────────────────────────┤
│ 预留区域    │    可读区域      │          可写区域            │
│ kCheapPrep │  [read, write)  │  [write, buffer_.size())    │
│   8 bytes  │   已接收未读取   │      空闲可写入              │
└────────────┴─────────────────┴──────────────────────────────┘
              ^                 ^
                       readIndex_        writeIndex_
区域 范围 用途
预留区 [0, kCheapPrepend) 快速追加协议头部(如长度字段)
可读区 [readIndex_, writeIndex_) 已接收但未读取的数据
可写区 [writeIndex_, buffer_.size()) 空闲空间,用于写入新数据

核心成员只有三个,全部 O(1) 可计算:

cpp 复制代码
std::vector<char> buffer_;  // 底层存储,连续内存
size_t readIndex_;          // 读指针:指向下一个可读字节
size_t writeIndex_;         // 写指针:指向下一个可写字节

size_t readableBytes() const  { return writeIndex_ - readIndex_; }       // 可读字节数
size_t writableBytes() const  { return buffer_.size() - writeIndex_; }   // 可写字节数
size_t prependableBytes() const { return readIndex_ - kCheapPrepend; }   // 头部空闲区大小

这个布局的关键洞察:数据消费后不必清空内存,只需移动 readIndex_。已读区域不是垃圾,而是下一轮写入的"预备空间"。

预留区 kCheapPrepend:为协议头留的前排座位

cpp 复制代码
static const size_t kCheapPrepend = 8;  // 预留8字节

什么场景需要在数据前面 加东西?最典型的是长度前缀协议:先收到数据体,再补一个"总长度"字段到头部,方便对端解析。

cpp 复制代码
// 在数据前追加4字节长度
void prepend(const void* data, size_t len) {
    assert(len <= prependableBytes());
        readIndex_ -= len;                              // 读指针往前退
            const char* d = static_cast<const char*>(data);
                std::copy(d, d+len, begin()+readIndex_);         // 在腾出的位置写入
                }

图解 prepend 的过程:

复制代码
prepend 前:
[预留8B][ ABCD | 可写区...]
         ^readIndex
prepend(len=2) 后:
[预留6B][ XY | ABCD | 可写区...]
          ^readIndex(后退2格)

如果头部空间不够呢?muduo 会先搬移数据腾出前置空间。没有预留区的话,每次加协议头都得整体搬移数据,O(n) 变 O(1) 的差别就在这 8 个字节。

append 与扩容:先整理,再搬家

cpp 复制代码
void append(const char* data, size_t len) {
    ensureWriteableBytes(len);        // 先确保缓冲区有足够空间
        std::copy(data, data+len, beginWrite());
            writeIndex_ += len;
            }

空间不足时,expandSpace 有两条路径,决策流程如下:
#mermaid-svg-OT3WJXhHe5ZJRFcF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OT3WJXhHe5ZJRFcF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OT3WJXhHe5ZJRFcF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OT3WJXhHe5ZJRFcF .error-icon{fill:#552222;}#mermaid-svg-OT3WJXhHe5ZJRFcF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OT3WJXhHe5ZJRFcF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OT3WJXhHe5ZJRFcF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OT3WJXhHe5ZJRFcF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OT3WJXhHe5ZJRFcF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OT3WJXhHe5ZJRFcF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OT3WJXhHe5ZJRFcF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OT3WJXhHe5ZJRFcF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OT3WJXhHe5ZJRFcF .marker.cross{stroke:#333333;}#mermaid-svg-OT3WJXhHe5ZJRFcF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OT3WJXhHe5ZJRFcF p{margin:0;}#mermaid-svg-OT3WJXhHe5ZJRFcF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-OT3WJXhHe5ZJRFcF .cluster-label text{fill:#333;}#mermaid-svg-OT3WJXhHe5ZJRFcF .cluster-label span{color:#333;}#mermaid-svg-OT3WJXhHe5ZJRFcF .cluster-label span p{background-color:transparent;}#mermaid-svg-OT3WJXhHe5ZJRFcF .label text,#mermaid-svg-OT3WJXhHe5ZJRFcF span{fill:#333;color:#333;}#mermaid-svg-OT3WJXhHe5ZJRFcF .node rect,#mermaid-svg-OT3WJXhHe5ZJRFcF .node circle,#mermaid-svg-OT3WJXhHe5ZJRFcF .node ellipse,#mermaid-svg-OT3WJXhHe5ZJRFcF .node polygon,#mermaid-svg-OT3WJXhHe5ZJRFcF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OT3WJXhHe5ZJRFcF .rough-node .label text,#mermaid-svg-OT3WJXhHe5ZJRFcF .node .label text,#mermaid-svg-OT3WJXhHe5ZJRFcF .image-shape .label,#mermaid-svg-OT3WJXhHe5ZJRFcF .icon-shape .label{text-anchor:middle;}#mermaid-svg-OT3WJXhHe5ZJRFcF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OT3WJXhHe5ZJRFcF .rough-node .label,#mermaid-svg-OT3WJXhHe5ZJRFcF .node .label,#mermaid-svg-OT3WJXhHe5ZJRFcF .image-shape .label,#mermaid-svg-OT3WJXhHe5ZJRFcF .icon-shape .label{text-align:center;}#mermaid-svg-OT3WJXhHe5ZJRFcF .node.clickable{cursor:pointer;}#mermaid-svg-OT3WJXhHe5ZJRFcF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-OT3WJXhHe5ZJRFcF .arrowheadPath{fill:#333333;}#mermaid-svg-OT3WJXhHe5ZJRFcF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OT3WJXhHe5ZJRFcF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OT3WJXhHe5ZJRFcF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OT3WJXhHe5ZJRFcF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OT3WJXhHe5ZJRFcF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OT3WJXhHe5ZJRFcF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-OT3WJXhHe5ZJRFcF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OT3WJXhHe5ZJRFcF .cluster text{fill:#333;}#mermaid-svg-OT3WJXhHe5ZJRFcF .cluster span{color:#333;}#mermaid-svg-OT3WJXhHe5ZJRFcF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-OT3WJXhHe5ZJRFcF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OT3WJXhHe5ZJRFcF rect.text{fill:none;stroke-width:0;}#mermaid-svg-OT3WJXhHe5ZJRFcF .icon-shape,#mermaid-svg-OT3WJXhHe5ZJRFcF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OT3WJXhHe5ZJRFcF .icon-shape p,#mermaid-svg-OT3WJXhHe5ZJRFcF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-OT3WJXhHe5ZJRFcF .icon-shape .label rect,#mermaid-svg-OT3WJXhHe5ZJRFcF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OT3WJXhHe5ZJRFcF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OT3WJXhHe5ZJRFcF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OT3WJXhHe5ZJRFcF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是



append 需要 len 字节空间
可写区 >= len ?
直接写入 writeIndex 前移
可写区加头部空闲区 >= len + kCheapPrepend ?
策略1 数据搬移 std::copy 一次搞定
策略2 真扩容 buffer_.resize

对应源码:

cpp 复制代码
void expandSpace(size_t len) {
    // 策略2(真扩容):前方+后方空闲空间拼起来都不够
        if (writableBytes() + prependableBytes() < len + kCheapPrepend) {
                buffer_.resize(writeIndex_ + len);
                    } else {
                            // 策略1(数据搬移):读操作让前方空出了位置,
                                    // 把可读数据整体前移,把碎片空间连成整块
                                            size_t readable = readableBytes();
                                                    std::copy(begin()+readIndex_, begin()+writeIndex_, begin()+kCheapPrepend);
                                                            readIndex_ = kCheapPrepend;
                                                                    writeIndex_ = kCheapPrepend + readable;
                                                                        }
                                                                        }

注意优先级是先整理、后扩容

  • 数据搬移只是一次 std::copy,不涉及堆上重新分配内存,代价更小

  • 只有拼起来都不够时才 resize 扩大 vector

    搬移前(碎片化):
    [已读空洞 ABC 读过的][EF 可读][一点可写]
    ^readIndex ^writeIndex
    搬移后(空间归一):
    [预留][EF 可读][大大的一段可写空间]
    ^readIndex ^writeIndex

readFd 与 readv:一次系统调用

这是 muduo Buffer 的核心亮点。思考一个两难问题:

  • 每个连接的可写区预留大了→ 10000 个连接各占 64KB,内存爆炸
  • 预留小了 → 一次读不完,得再调一次 read,系统调用翻倍
    muduo 用 readv(分散读)+ 栈上临时缓冲区完美破局:
cpp 复制代码
ssize_t Buffer::readFd(int fd, int* saveError) {
    char extrabuf[65536] = {0};              // 64KB 栈上临时缓冲区
        struct iovec vec[2];
            const size_t writeable = writableBytes();
                vec[0].iov_base = beginWrite();           // 第一块:buffer_ 的可写区
                    vec[0].iov_len = writeable;
                        vec[1].iov_base = extrabuf;               // 第二块:栈上的 extrabuf
                            vec[1].iov_len = sizeof(extrabuf);
    const int iovcnt = (writeable < sizeof(extrabuf)) ? 2 : 1;
        const ssize_t nread = ::readv(fd, vec, iovcnt);   // 一次系统调用读两块
            if (nread < 0) {
                    *saveError = errno;
                        } else if (nread <= writeable) {
                                writeIndex_ += nread;                 // 数据都在 buffer_ 里,指针一移完事
                                    } else {
                                            writeIndex_ = buffer_.size();         // buffer_ 写满了
                                                    append(extrabuf, nread - writeable);  // 溢出部分收编进 buffer_(触发扩容)
                                                        }
                                                            return nread;
                                                            }

执行流程:
#mermaid-svg-V1BOsPekZEv4T1xJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-V1BOsPekZEv4T1xJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-V1BOsPekZEv4T1xJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-V1BOsPekZEv4T1xJ .error-icon{fill:#552222;}#mermaid-svg-V1BOsPekZEv4T1xJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-V1BOsPekZEv4T1xJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-V1BOsPekZEv4T1xJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-V1BOsPekZEv4T1xJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-V1BOsPekZEv4T1xJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-V1BOsPekZEv4T1xJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-V1BOsPekZEv4T1xJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-V1BOsPekZEv4T1xJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-V1BOsPekZEv4T1xJ .marker.cross{stroke:#333333;}#mermaid-svg-V1BOsPekZEv4T1xJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-V1BOsPekZEv4T1xJ p{margin:0;}#mermaid-svg-V1BOsPekZEv4T1xJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-V1BOsPekZEv4T1xJ .cluster-label text{fill:#333;}#mermaid-svg-V1BOsPekZEv4T1xJ .cluster-label span{color:#333;}#mermaid-svg-V1BOsPekZEv4T1xJ .cluster-label span p{background-color:transparent;}#mermaid-svg-V1BOsPekZEv4T1xJ .label text,#mermaid-svg-V1BOsPekZEv4T1xJ span{fill:#333;color:#333;}#mermaid-svg-V1BOsPekZEv4T1xJ .node rect,#mermaid-svg-V1BOsPekZEv4T1xJ .node circle,#mermaid-svg-V1BOsPekZEv4T1xJ .node ellipse,#mermaid-svg-V1BOsPekZEv4T1xJ .node polygon,#mermaid-svg-V1BOsPekZEv4T1xJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-V1BOsPekZEv4T1xJ .rough-node .label text,#mermaid-svg-V1BOsPekZEv4T1xJ .node .label text,#mermaid-svg-V1BOsPekZEv4T1xJ .image-shape .label,#mermaid-svg-V1BOsPekZEv4T1xJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-V1BOsPekZEv4T1xJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-V1BOsPekZEv4T1xJ .rough-node .label,#mermaid-svg-V1BOsPekZEv4T1xJ .node .label,#mermaid-svg-V1BOsPekZEv4T1xJ .image-shape .label,#mermaid-svg-V1BOsPekZEv4T1xJ .icon-shape .label{text-align:center;}#mermaid-svg-V1BOsPekZEv4T1xJ .node.clickable{cursor:pointer;}#mermaid-svg-V1BOsPekZEv4T1xJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-V1BOsPekZEv4T1xJ .arrowheadPath{fill:#333333;}#mermaid-svg-V1BOsPekZEv4T1xJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-V1BOsPekZEv4T1xJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-V1BOsPekZEv4T1xJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-V1BOsPekZEv4T1xJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-V1BOsPekZEv4T1xJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-V1BOsPekZEv4T1xJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-V1BOsPekZEv4T1xJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-V1BOsPekZEv4T1xJ .cluster text{fill:#333;}#mermaid-svg-V1BOsPekZEv4T1xJ .cluster span{color:#333;}#mermaid-svg-V1BOsPekZEv4T1xJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-V1BOsPekZEv4T1xJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-V1BOsPekZEv4T1xJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-V1BOsPekZEv4T1xJ .icon-shape,#mermaid-svg-V1BOsPekZEv4T1xJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-V1BOsPekZEv4T1xJ .icon-shape p,#mermaid-svg-V1BOsPekZEv4T1xJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-V1BOsPekZEv4T1xJ .icon-shape .label rect,#mermaid-svg-V1BOsPekZEv4T1xJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-V1BOsPekZEv4T1xJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-V1BOsPekZEv4T1xJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-V1BOsPekZEv4T1xJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

只装满 vec0
溢出到 vec1
readFd socket 有数据可读
准备 iovec 数组 vec0 加 vec1
可写区小于 64KB ?
iovcnt 等于 2 一次 readv 读两块
iovcnt 等于 1 只读 buffer
nread 落在哪里
writeIndex 前移 零额外开销
append 触发搬移扩容 收编 extrabuf

三个设计意图逐一拆解:

1. 为什么 extrabuf 在栈上?

64KB 的临时缓冲区放在栈上,函数返回即释放,不占用任何持久内存。绝大多数情况下数据量不大,extrabuf 根本用不上------等于白捡了一个"无限容量"的兜底,而平时成本为零。

2. 为什么用 readv 而不是两次 read?

一次 readv 同时试探两块缓冲区,数据溢出时不需要第二次系统调用。配合 LT 触发模式,muduo 的理念是"一次把数据读完",避免 epoll 反复通知。

3. 溢出时才扩容

只有数据真的多到装不下,才通过 append 触发搬移/扩容。扩容是例外而非常态------这就是"内存高效"的底气。

retrieve:消费数据只是挪指针

cpp 复制代码
std::string retrieveAsString(size_t len) {
    std::string result(peek(), len);  // peek(): 不消费地看一眼
        retrieve(len);                    // 确认消费
            return result;
            }
void retrieve(size_t len) {
    if (len < readableBytes()) {
            readIndex_ += len;   // 部分消费:一条加法指令
                } else {
                        retrieveAll();       // 全部消费:两个指针归位
                            }
                            }

peek() 返回可读区起始指针但不移动任何东西,retrieve() 才真正消费。peek + retrieve 的组合拳正是解决粘包半包的钥匙:先 peek 出头部看长度够不够一个完整包,够了才 retrieve。

消费的成本仅仅是 readIndex_ += len------零拷贝。被消费的区域不做任何清除动作,它的内存将在下一轮 expandSpace 时被回收复用。

实战:粘包半包处理模板

cpp 复制代码
// 网络数据接收 + 长度前缀协议解析
Buffer buf;
int savedErrno;
ssize_t n = buf.readFd(sockfd, &savedErrno);   // 一次尽量多读

while (buf.readableBytes() >= headerLen) {
    const char* header = buf.peek();            // 只看不消费
        int bodyLen = parseHeader(header);
    if (buf.readableBytes() >= headerLen + bodyLen) {
            buf.retrieve(headerLen);                // 够一个完整包,消费头部
                    std::string body(buf.peek(), bodyLen);
                            buf.retrieve(bodyLen);                  // 消费包体
                                    processMessage(body);                   // 交给业务处理
                                        } else {
                                                break;  // 数据不足一个完整包,留着等下次 EPOLLIN
                                                    }
                                                    }

处理逻辑分三步:

  1. peek 偷看头部:读取长度字段,但不消费数据
  2. 判断够不够:可读字节数 ≥ 头部 + 包体,才动手
  3. 不够就等 :剩余半包留在 Buffer 里,等下一次读事件把数据补齐
    这是 LT + 非阻塞 IO + 应用层缓冲区的经典配合:读到多少算多少,攒在 Buffer 里慢慢解析。

设计精髓总结

设计要点 技术实现 收益
预留头部空间 kCheapPrepend = 8 O(1) 追加协议头部
双指针管理 readIndex_ / writeIndex_ 无拷贝的数据消费
内存复用 expandSpace() 先搬移后扩容 减少内存分配次数
分散读优化 readv() + 栈上 extrabuf 一次系统调用,内存与效率兼得
peek/retrieve 先看后消费 粘包半包的天然解法
vector 底层 std::vector<char> 自动内存管理,连续地址空间

系列串联

Buffer 是下一章主角 TcpConnection 的左膀右臂:每条连接持有 inputBuffer_(收数据)和 outputBuffer_(发数据),读写事件触发时与 Buffer 配合完成非阻塞收发。有了 Channel 的事件分发、Poller 的 IO 复用、EventLoop 的循环驱动、Acceptor 的接客和 Buffer 的中转,剩下的事就是把它们粘合成完整的 TCP 服务器------这正是 TcpServer 与 TcpConnection 的使命。

相关推荐
杨连江1 小时前
REF30系列CMOS串联型带隙电压基准源原理与供应链现象研究
经验分享
很楠爱上2 小时前
从“AI 看合同”到可举证的合同决策链:CounterClause(对薄) 的架构设计与工程实践
人工智能·经验分享·python·学习·agent
liulilittle2 小时前
XTCP简述
网络·c++·tcp/ip·ip·tcp·通信
一只旭宝2 小时前
预约系统版本2(基于第一版改良)
服务器·数据库·c++
调试到凌晨2 小时前
2026年配音工具避坑实测:免费额度、长文本稳定性与API集成能力全记录
人工智能·经验分享·实时音视频
被怪兽吃掉了2 小时前
5.2.1一维组数定义方式
开发语言·c++·算法
旖旎夜光3 小时前
LeetCode 69:x 的平方根(二分查找) —— 题解
数据结构·c++·算法·leetcode·二分查找
j7~3 小时前
【C++】《C++二叉搜索树(BST)从入门到精通:概念、实现与Key/Value模型全解析》
开发语言·c++·学习·二叉搜索树
hehelm3 小时前
仿muduo库实现高并发服务器—Channel类
linux·服务器·开发语言·网络·c++