Buffer是一个缓冲区,这个缓冲区在我们基于nonblocking非阻塞IO的服务端编程里面,一个缓冲区还是非常有必要,而且我们在TCP编程里面经常出现TCP粘包问题,我们一般都会在通讯的数据头来描述数据的长度,每一次根据数据的长度来截取包的大小进行反序列化进行处理,可能这一次截取的数据比较多,但是我们应用只需要根据包头的大小读取一部分,所以未读的数据就需要在缓冲区存放。同理,发送端可能需要发送的数据比较多,一次无法发送完成,剩下的数据也需要存放在缓冲区。
一、Buffer底层的简单描述
数据成员
- prependable bytes:表示数据包的字节数
- readerIndex:应用程序从readerIndex指向的位置开始读缓冲区,[readerIndex, writerIndex]表示待读取数据,读完后向后移动len(retrieve方法)
- writerIndex:应用程序从writerIndex指向的位置开始写缓冲区,写完后writerIndex向后移动len(append方法)
- [readerIndex_, writerIndex_]:标识可读数据区间(readableBytes方法)
二、Buffer.h
#pragma once
#include <vector>
#include <string>
#include <algorithm>
//网络库底层的缓冲区类型定义
class Buffer
{
public:
static const size_t kCheapPrepend = 8;//数据包长度
static const size_t kInitialSize = 1024; //缓冲区大小
explicit Buffer(size_t initialSize = kInitialSize)
:buffer_(kCheapPrepend + initialSize)
,readerIndex_(kCheapPrepend)
,writerIndex_(kCheapPrepend)
{}
//因为底层的内存资源是通过vector直接管理的,所以buffer_也不需要自己去析构资源,当前对象析构的时候成员对象析构,vector在析构的时候会自动释放外面堆内存管理的资源
//可读数据长度
size_t readableBytes() const
{
return writerIndex_ - readerIndex_;
}
//可写空间大小
size_t writableBytes() const
{
return buffer_.size() - writerIndex_;
}
size_t prependableBytes() const
{
return readerIndex_;
}
//返回缓冲区中可读数据的起始地址
const char* peek() const
{
return begin() + readerIndex_;
}
/**
*在底层相关connection有数据到来的时候,muduo库会注册回调onMessage
*把数据放到一个buffer里面,我们一般都会调用Buffer的retrieveAllAsString
*把数据从buffer转成c++的string类型
*/
void retrieve(size_t len)
{
// len就是应用程序从Buffer缓冲区读取的数据长度
// 必须要保证len <= readableBytes()
if(len < readableBytes())
{
// 这里就是可读数据没有读完
//应用只读取了可读缓冲区数据的一部分,就是len,还剩下readableIndex_ += len - writerIndex_
readerIndex_ += len;
}
else
{
// len == readableBytes()
// 可读数据读完了,readerIndex_和writerIndex_都要复位
retrieveAll();
}
}
void retrieveAll()
{
readerIndex_ = writerIndex_ = kCheapPrepend;
}
//把onMessage函数上报的Buffer数据,转成string类型的数据返回
std::string retrieveAllAsString()
{
return retrieveAsString(readableBytes());//应用可读取数据的长度
}
std::string retrieveAsString(size_t len)
{
std::string result(peek(), len);
retrieve(len);//上面一句把缓冲区中可读的数据,已经读取出来,这里肯定要对缓冲区进行复位操作
return result;
}
void ensureWritableBytes(size_t len)
{
if(writableBytes() < len)
{
makeSpace(len);//扩容函数
}
}
//不管是从fd上读数据写到缓冲区inputBuffer_,还是发数据要写入outputBuffer_,我们都要往writeable区间内添加数据
void append(const char* data, size_t len)
{
// 确保可写空间不小于len
ensureWritableBytes(len);
// 把[data,data+len]内存上的数据,添加到writable缓冲区当中
std::copy(data, data + len, beginWrite());
writerIndex_ += len;
}
char* beginWrite()
{
return begin() + writerIndex_;
}
const char* beginWrite() const
{
return begin() + writerIndex_;
}
//从fd上读取数据,存放到writerIndex_,返回实际读取的数据大小
ssize_t readFd(int fd, int* saveErrno);
ssize_t writeFd(int fd, int* saveErrno);
private:
//返回buffer底层数组首元素的地址,也就是数组的起始地址
char* begin()
{
return &(*buffer_.begin());
}
const char* begin() const
{
return &(*buffer_.begin());
}
/**
*
*/
void makeSpace(size_t len)
{
//如果需要写入缓冲区数据的长度要大于Buffer对象底层vector空闲的长度了,就需要扩容,其中len表示需要写入数据的长度
if(writableBytes() + prependableBytes() < len + kCheapPrepend)
{
// 直接在writerIndex_后面再扩大len的空间
buffer_.resize(len + writerIndex_);
}
else // 如果是空闲空间足够存放len字节的数据,就把未读取的数据统一往前移,移到kCheapPrepend的位置
{
size_t readable = readableBytes();// 这表示剩余需要读取的数据
// 把[readerIndex_, writerIndex_]整体搬到从kCheapPrepend开始的位置
std::copy(begin() + readerIndex_,
begin() + writerIndex_,
begin() + kCheapPrepend);
readerIndex_ = kCheapPrepend;
writerIndex_ = readerIndex_ + readable;// writerIndex_指向待读取数据的末尾
}
}
std::vector<char> buffer_;
size_t readerIndex_;
size_t writerIndex_;
};
三、Buffer.cc
- readv函数介绍
ssize_t readFd(int fd, int* saveErrno);
Buffer缓冲区是有大小的(占用堆区内存),但是我们无法知道fd上的流式数据有多少,如果我们将缓冲区开的非常大,大到肯定能容纳所有读取的数据,这就太浪费空间了,muduo库中使用readv方法,根据读取的数据多少开动态开辟缓冲区
readv可以在不连续的多个地址写入同一个fd上读取的数据,readv可以根据读出的数据自动的去填充多个缓冲区。
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
- fd:读取数据的文件描述符
- iov:封装了缓冲区地址和可写空间大小的结构体
- iovcnt:缓冲区个数
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
- iov_base:缓冲区的起始地址
- iov_len:缓冲区的可允许写的长度
- Buffer.cc
#include "Buffer.h"
#include <errno.h>
#include <sys/uio.h>
#include <unistd.h>
//从fd上读取数据,底层的Poller工作在LT模式,存放到writerIndex_,返回实际读取的数据大小
//底层的buffer缓冲区是有大小的,但是从fd上读数据的时候,却不知道tcp数据最终的大小
//Buffer缓冲区是有大小的(占用堆区内存),但是我们无法知道fd上的流式数据有多少,
//如果我们将缓冲区开的非常大,大到肯定能容纳所有读取的数据,这就太浪费空间了,
//muduo库中使用readv方法,根据读取的数据多少开动态开辟缓冲区
ssize_t Buffer::readFd(int fd, int* saveErrno)
{
char extrabuf[65536] = {0}; // 64K栈空间,会随着函数栈帧回退,内存自动回收
struct iovec vec[2];
const size_t writable = writableBytes(); // 这是Buffer底层缓冲区剩余的可写空间大小
/**
* 当我们用readv从fd上读数据,会先填充vec[0]的缓冲区
* vec[0]填充满以后会自动把数据填充在extrabuf里面
* 最后extrabuf里面如果有内容的话,就把extrabuf里面的内容添加在缓冲区里面
* 这样的结果就是缓冲区刚好存放所有需要写入的内容,内存空间利用率高
*/
vec[0].iov_base = begin() + writerIndex_; // 第一块缓冲区
vec[0].iov_len = writable; // iov_base缓冲区可写的大小
vec[1].iov_base = extrabuf; // 第二块缓冲区
vec[1].iov_len = sizeof extrabuf;
// 如果Buffer有65536字节的空闲空间,就不使用栈上的缓冲区
//如果不够65536字节,就使用栈上的缓冲区,即readv一次最多读取65536字节数据
const int iovcnt = (writable < sizeof extrabuf) ? 2 : 1;
const ssize_t n = ::readv(fd, vec, iovcnt);
if(n < 0)
{
*saveErrno = errno;
}
else if(n <= writable)
{
// 读取的数据n小于Buffer底层的可写空间,readv会直接把数据存放在begin() + writerIndex_
writerIndex_ += n;
}
else
{
//extrabuf里面也写入了数据
writerIndex_ = buffer_.size();
// 从extrabuff里读取 n - writable 字节的数据存入Buffer底层的缓冲区
append(extrabuf, n - writable);
}
return n;
}
ssize_t Buffer::writeFd(int fd, int* saveErrno)
{
ssize_t n = ::write(fd, peek(), readableBytes());
if(n < 0)
{
*saveErrno = errno;
}
return n;
}