muduo网络库(十三):Buffer 缓冲区类
- [muduo网络库(十三):Buffer 缓冲区类](#muduo网络库(十三):Buffer 缓冲区类)
-
- [一句话理解 Buffer](#一句话理解 Buffer)
- 为什么必须有应用层缓冲区
- 设计目标
- 内存布局:三段式设计
- [预留区 kCheapPrepend:为协议头留的前排座位](#预留区 kCheapPrepend:为协议头留的前排座位)
- [append 与扩容:先整理,再搬家](#append 与扩容:先整理,再搬家)
- [readFd 与 readv:一次系统调用](#readFd 与 readv:一次系统调用)
- retrieve:消费数据只是挪指针
- 实战:粘包半包处理模板
- 设计精髓总结
- 系列串联
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
}
}
处理逻辑分三步:
- peek 偷看头部:读取长度字段,但不消费数据
- 判断够不够:可读字节数 ≥ 头部 + 包体,才动手
- 不够就等 :剩余半包留在 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 的使命。