Buffer 类用于处理 socket 的 I/O 缓存 —— 负责:
- 从 socket 读取数据(read)
- 写入 socket 发送数据(write)
- 管理数据区的结构与索引,以避免频繁内存拷贝
成员变量
static const size_t KCheapPrepend=8;//作用:预留头部空间(prependable space通常用于在 buffer 前面“追加”一些控制信息(比如长度头部等协议格式) static const size_t KInitialSize=1024;//Buffer 初始化时的默认大小,即初始 buffer 容量 std::vector<char>buffer_;//真正存放数据的原始空间,大小可能会增长 size_t readerIndex_;//可读区域的起始位置(也就是数据有效负载的起点)初始值为 KCheapPrepend = 8,跳过头部预留空间 size_t writeIndex_;可写区域的起始位置(新的数据 append 时写入的位置)初始值也是 8(没有数据时读写索引相同)
数据区域结构大概可以划分为三段:
设置缓冲区(Buffer)是网络编程中必不可少的一部分,它的主要目的是
1. 一、解决“数据不一次收完/发完”的问题
2.提高性能,减少系统调用,系统调用(如 read()/write())是“用户态”到“内核态”的切换,有成本
3.实现高效的异步/非阻塞 IO,在 Reactor 模型中,socket 都是非阻塞的:你不能保证什么时候 socket 可以写
所以必须把“待写数据”先存在缓冲区中,一旦可以写,就从 buffer 中拷贝一部分写出去
explicit Buffer(size_t initialSize=KInitialSize)://指定初始容量,默认是 KInitialSize = 1024 字节 buffer_(KCheapPrepend+initialSize),//分配一个 std::vector<char>,大小是 initialSize + KCheapPrepend readerIndex_(KCheapPrepend),读指针从第 8 字节开始(跳过 prepend 区域) writeIndex_(KCheapPrepend)写指针从第 8 字节开始(跳过 prepend 区域) { } size_t readableBytes()const//可读的数据 { return writeIndex_-readerIndex_; } size_t writeableBytes()const//可写数据 { return buffer_.size()-writeIndex_; } size_t prependableBytes()const{ return readerIndex_; }//0 ~ readerIndex_ 之间的区域,就是已经读过的数据,但我们没有释放掉; //这个区域可以被拿来做 prepend 用,因此被称为“prependable” 区。 //返回缓冲区中可读数据的起始地址 const char*peek()const{ return begin()+readerIndex_; } void retrieve(size_t len)//是 Buffer 缓冲区的一个数据消费接口,用于在数据读取后推进读指针 readerIndex_,表示“已经读取或发送”了这部分数据。 { if(len<readableBytes())// 把 readerIndex_ 移 len 字节,表示消费(发送)了这部分数据 { readerIndex_+=len;//应用只读取了可读缓冲区数据的一部分len,还剩下 readerIndex_+=len -》 writeindex_ } else//len>=readableBytes { retrieveAll();// 表示读完(发送完)所有数据,直接调用 retrieveAll() 来重置索引 } } void retrieveAll()// ,将读写指针都复位到 KCheapPrepend,清空状态(类似重新开始) { readerIndex_=writeIndex_=KCheapPrepend; } std::string retrieveAllAsString()//将所有可读数据(readableBytes())作为一个 std::string 返回。 { return retrieveAsString(readableBytes()); } std::string retrieveAsString(size_t len) { std::string result(peek(),len);//从 buffer 中拷贝 len 字节数据构造字符串,len字节的数据就是要读取的数据 retrieve(len);//上面一句把缓冲区所有可读数据,已经读取出来,现在这一句要对缓冲区进行复位操作(表示读完所有数据,直接调用 retrieveAll() 来重置索引) return result;//返回构造好的字符串 } void ensureWriteableBytes(size_t len)//:确保当前 buffer 至少有 len 字节的可写空间(writeable space) { if(writeableBytes()<len) { //扩容 makeSpace(len); } } void append(const char*data,size_t len)//将一段长度为 len 的内存数据(由 data 指向)追加写入到当前 buffer 的可写区域末尾。 { ensureWriteableBytes(len);// 确保 buffer 当前有足够的空间可写,如果没有,就自动调用 makeSpace() 来扩容或搬移已有数据。 std::copy(data,data+len,beginwrite());//使用 std::copy() 将传入的 data 数据拷贝 len 字节到 buffer 的写指针位置。 //把 [first, last) 范围内的元素 复制到从 result 开始的目标区域中。 writeIndex_+=len;//数据写入成功后,需要更新写指针,表示新的数据已经被写进 buffer,可读区域向后扩大 } char *begin()//为其他函数(如 peek(), beginWrite(), append() 等)提供统一的底层数据访问入口。 { return &*buffer_.begin();// 指向 buffer_ 开头的指针 用于内部偏移计算、写入数据等 } const char* begin()const { return &*buffer_.begin();//只读版本 用于不修改内容的场景,例如读缓冲 } void makeSpace(size_t len)//确保 有足够空间写入 len 字节的数据, Buffer 缓冲区扩容 或 空间复用的核心逻辑 { if(writeableBytes()+prependableBytes()<len+KCheapPrepend)//当前 buffer 空闲空间(可写空间 + 可移动头部空间)是否足以容纳 len 字节的数据 + 预留头部空间(KCheapPrepend)? { buffer_.resize(writeIndex_+len);//resize() 会让 buffer 的容量扩展到:当前写入位置 + 要写入的字节数 } else//:空间够,但前部空闲太多,没利用起来 → 复用它! { size_t readable=readableBytes(); std::copy(begin()+readerIndex_,begin()+writeIndex_,begin()+KCheapPrepend);//将未读数据 [readerIndex_, writeIndex_) 拷贝到 KCheapPrepend 后的位置(即重新排布数据),:把前面读掉的空间让出来,避免浪费内存 readerIndex_=KCheapPrepend; writeIndex_=readerIndex_+readable;//复用前部已读空间 } } //在 Buffer 中,数据是否“存在”不重要,只要我们保证索引指向的是有效区域,前面未清除的内容不会影响逻辑或造成污染,所以不清除是安全且高效的。 char*beginwrite() { return begin()+writeIndex_; } const char*beginwrite()const { return begin()+writeIndex_; }// 第一个 const:修饰返回值 //第二个 const:修饰成员函数,承诺不会修改当前对象的任何成员变量 // /** * struct iovec { void* iov_base; // 可以接受任何类型的指针 size_t iov_len; //高效从 socket 描述符读取数据到缓冲区的经典做法 ssize_t Buffer::readFD(int fd,int*saveErrno)//saveErrno 是用来 保存系统调用 readv() 失败时 errno 的值 的一个输出参数指针(int* 类型)。这是一种常见的错误信息传递方式,避免在函数内部直接处理错误,而是把错误代码返回给调用者去处理 { char extrabuf[65536]={0};//栈上的内存空间,为防止主缓冲区空间不够,额外准备一个大块栈空间暂存多余数据,避免动态扩容开销。 struct iovec vec[2]; //vec[0] → buffer_ 中尚未使用的区域 vec[1] → 栈上的 extrabuf const size_t writeable=writeableBytes(); //buffer底层缓冲区剩余的可写空间大小 vec[0].iov_base=begin()+writeIndex_; //char* + size_t 结果是:指向 buffer 中 可写位置的 char* 指针。 vec[0].iov_len=writeable;//vec[0] 指向主缓冲区可写位置 vec[1].iov_base=extrabuf; vec[1].iov_len=sizeof extrabuf; const int iovcnt=(writeable<sizeof extrabuf)?2:1;//若主缓冲区可写空间不足 64KB,就启用备用缓冲区 const ssize_t n=::readv(fd,vec,iovcnt);//用 readv() 一次性读取数据,自动写入 vec 中定义的缓冲区,如果数据很小,会直接写到 vec[0],如果数据超出主缓冲区,就溢出到 vec[1] 中 if(n<0) { *saveErrno=errno; } else if(n<=writeable)//如果数据完全写入了主缓冲区,则更新写入索引。 { writeIndex_+=n; } /** * 如果有多余数据写入了备用缓冲区,则先将主缓冲区视为满; 再将备用缓冲区的数据追加进主缓冲区,保持数据连续。 */ else //extrabuf里面也写入了数据 { writeIndex_=buffer_.size(); append(extrabuf,n-writeable);//writeIndex_开始写n-writeable大小的数据,append() 内部会自动扩容或整理空间 } return n; } ssize_t Buffer::writeFD(int fd,int*saveErrno)//将 Buffer 中已有的数据写入某个 socket 文件描述符(fd)中 { ssize_t n=::write(fd,peek(),readableBytes());//系统调用,向 fd 写数据,peek(): 返回 Buffer 当前可读数据的起始地址(即 begin() + readerIndex_)。readableBytes(): 返回 Buffer 中当前可读的数据长度 if(n<0) { *saveErrno=errno; } return n;//返回写入的字节数(>0) }