xiaofengzi的TinyWebService学习之路——buffer类

xiaofengzi的TinyWebService学习之路——buffer类

一、开头导入

首先先放两张图片上来
请添加图片描述
请添加图片描述
这是大佬的博客里画出来的图,我也是跟着他来理解的。其中ReadFd和WriteFd应该是重点,外部调用接口也是会调用这两个。

另外就是几个下标之间的关系,根据这个图来好好看一下,一定不要弄混了,写是往writeIndex的指针写,读取则是从readIndex的指针读。当外部写入fd的时候,是将buffer中的readable写入fd;当需要读取fd的内容时,则是读到writable的位置,注意区分写和读。

二、整体代码

#include "buffer.h"

// Buffer类的初始化,把读写下标初始化,vector<char>初始化
Buffer::Buffer(int initBuffSize) : buffer_(initBuffSize), readPos_(0), writePos_(0) {}

// 可写的数量:buffer大小 - 写下标
size_t Buffer::WritableBytes() const
{
  return buffer_.size() - writePos_;
}

// 可读的数量:写下标 - 读下标
size_t Buffer::ReadableBytes() const
{
  return writePos_ - readPos_;
}

// 可预留空间:已经读过的就没用了,等于读下标
size_t Buffer::PrependableBytes() const
{
  return readPos_;
}

// 读指针的位置
const char *Buffer::Peek() const
{
  return &buffer_[readPos_];
}

// 确保可写的长度
void Buffer::EnsureWriteable(size_t len)
{
  if (len > WritableBytes())
  {
    MakeSpace_(len);
  }
  assert(len <= WritableBytes());
}

// 移动写下标,在Append中使用
void Buffer::HasWritten(size_t len)
{
  writePos_ += len;
}

// 读取len长度,移动读下标
void Buffer::Retrieve(size_t len)
{
  readPos_ += len;
}

// 读取到end位置
void Buffer::RetrieveUntil(const char *end)
{
  assert(Peek() <= end);
  Retrieve(end - Peek()); // end指针 - 读指针  长度
}

// 取出所有数据,buffer归零,读写下标归零,在别的函数中会用到
void Buffer::RetrieveAll()
{
  memset(&buffer_[0], 0, buffer_.size()); // 自 C11 标准起,bzero 已经被标记为不推荐使用,因为它的名称可能会与宏定义冲突,并且存在更安全的替代函数,如 memset。
  readPos_ = 0;
  writePos_ = 0;
}

// 取出剩余可读的str
std::string Buffer::RetrieveAllToStr()
{
  std::string str(Peek(), ReadableBytes());
  RetrieveAll();
}

// 写指针的位置
const char *Buffer::BeginWriteConst() const
{
  return &buffer_[writePos_];
}

char *Buffer::BeginWrite()
{
  return &buffer_[writePos_];
}

// 添加str到缓冲区
void Buffer::Append(const std::string &str)
{
  Append(str.c_str(), str.size());
}

void Buffer::Append(const char *str, size_t len)
{
  assert(str);
  EnsureWriteable(len);                    // 确保可写的长度
  std::copy(str, str + len, BeginWrite()); // 将str放到写下标开始的方法
  HasWritten(len);                         // 移动写下标
}

void Buffer::Append(const void *data, size_t len)
{
  Append(static_cast<const char *>(data), len);
}

// 将buffer中的读下标的地方放到该buffer中的写下标位置
void Buffer::Append(const Buffer &buff)
{
  Append(buff.Peek(), buff.ReadableBytes());
}

// 将fd的内容读到缓冲区,即writable的位置
ssize_t Buffer::ReadFd(int fd, int *Errno)
{
  char buff[65535]; // 栈区
  struct iovec iov[2];
  size_t writeable = WritableBytes(); // 先记录能写多少
  // 分散读,保证数据能全部读完
  iov[0].iov_base = BeginWrite();
  iov[0].iov_len = writeable;
  iov[1].iov_base = buff;
  iov[1].iov_len = sizeof(buff);

  ssize_t len = readv(fd, iov, 2);
  if (len < 0)
  {
    *Errno = errno;
  }
  else if (static_cast<size_t>(len) <= writeable)
  {                   // 若len小于writable,说明写区可以容纳len
    writePos_ += len; // 直接移动写下标
  }
  else
  {
    writePos_ = buffer_.size();                         // 写区写满了,下标移动到最后
    Append(buff, static_cast<size_t>(len - writeable)); // 剩余的长度
  }
  return len;
}

// 将buffer中可读的区域写入fd中
ssize_t Buffer::WriteFd(int fd, int *Errno)
{
  ssize_t len = write(fd, Peek(), ReadableBytes());
  if (len < 0)
  {
    *Errno = errno;
    return len;
  }
  Retrieve(len);
  return len;
}

char *Buffer::BeginPtr_()
{
  return &buffer_[0];
}

const char *Buffer::BeginPtr_() const
{
  return &buffer_[0];
}

// 扩展空间
void Buffer::MakeSpace_(size_t len)
{
  if (WritableBytes() + PrependableBytes() < len)
  {
    buffer_.resize(writePos_ + len + 1);
  }
  else
  {
    size_t readable = ReadableBytes();
    std::copy(BeginPtr_() + readPos_, BeginPtr_() + writePos_, BeginPtr_());
    readPos_ = 0;
    writePos_ = readable;
    assert(readable == ReadableBytes());
  }
}

三、详解解读

Buffer::Buffer(int initBuffSize) : buffer_(initBuffSize), readPos_(0), writePos_(0) {}

Buffer 类的构造函数,接受一个初始化缓冲区大小的参数 initBuffSize,使用初始化列表初始化私有成员变量 buffer_(一个 vector 类型的动态缓冲区)、readPos_(读位置指针,初始化为0)和 writePos_(写位置指针,初始化为0)。然后这样写的话可以在进行初始化之前就对buffer_以及读写下标进行初始化了。

size_t Buffer::WritableBytes() const
{
  return buffer_.size() - writePos_;
}

计算并返回可写入字节的数量,即缓冲区大小与当前写位置指针的差值。
在C++中,const 关键字用于表示函数不会修改它所属对象的状态。当将 const 修饰符添加到成员函数后,该函数承诺不会改变任何成员变量的值,也不会调用任何非 const 成员函数

size_t Buffer::ReadableBytes() const
{
  return writePos_ - readPos_;
}

计算并返回可读取字节的数量,即当前写位置指针与读位置指针的差值。

size_t Buffer::PrependableBytes() const
{
  return readPos_;
}

返回可以在缓冲区前追加的字节数量,由于已经读过的数据不再使用,所以这个值等于读指针的当前位置。

const char *Buffer::Peek() const
{
  return &buffer_[readPos_];
}

返回一个指向缓冲区当前读位置的指针。

void Buffer::EnsureWriteable(size_t len)
{
  if (len > WritableBytes())
  {
    MakeSpace_(len);
  }
  assert(len <= WritableBytes());
}

确保缓冲区至少有 len 字节的空间可用于写入。如果没有足够的空间,调用 MakeSpace_ 方法来重新分配空间。使用 assert 确保在函数执行后有足够的可写空间。

void Buffer::HasWritten(size_t len)
{
  writePos_ += len;
}

写入 len 字节后,将写指针向前移动。

void Buffer::Retrieve(size_t len)
{
  readPos_ += len;
}

从缓冲区读取 len 字节,并将读指针向前移动。

void Buffer::RetrieveUntil(const char *end)
{
  assert(Peek() <= end);
  Retrieve(end - Peek());
}

从当前读指针位置读取到 end 指定的位置,并移动读指针。

void Buffer::RetrieveAll()
{
  memset(&buffer_[0], 0, buffer_.size());
  readPos_ = 0;
  writePos_ = 0;
}

清空缓冲区中的所有数据,并将读、写指针重置为0。

std::string Buffer::RetrieveAllToStr()
{
  std::string str(Peek(), ReadableBytes());
  RetrieveAll();
}

将缓冲区中的所有可读数据转换为 std::string 并返回,然后清空缓冲区。

const char *Buffer::BeginWriteConst() const
{
  return &buffer_[writePos_];
}

返回一个指向缓冲区当前写位置的 const 指针。

char *Buffer::BeginWrite()
{
  return &buffer_[writePos_];
}

返回一个指向缓冲区当前写位置的 char* 指针。

void Buffer::Append(const std::string &str)
{
  Append(str.c_str(), str.size());
}

将 std::string 类型的字符串添加到缓冲区。

void Buffer::Append(const char *str, size_t len)
{
  assert(str);
  EnsureWriteable(len);
  std::copy(str, str + len, BeginWrite());
  HasWritten(len);
}

将 len 长度的字符数组 str 添加到缓冲区。

void Buffer::Append(const void *data, size_t len)
{
  Append(static_cast<const char *>(data), len);
}

重载 Append 方法,接受 void* 类型的数据指针。

void Buffer::Append(const Buffer &buff)
{
  Append(buff.Peek(), buff.ReadableBytes());
}

将另一个 Buffer 对象中的数据追加到当前缓冲区。

/* 将fd的内容读到缓冲区,即writable的位置
这个函数通过 readv 函数实现了分散读取(scatter read),允许数据同时被写入到两个不同的内存区域,这在处理大量数据时可以提高效率。
同时,它还处理了可能发生的错误,并将读取的数据正确地存储到了 Buffer 对象的内部缓冲区中。*/
ssize_t Buffer::ReadFd(int fd, int *Errno)
{
  char buff[65535]; // 栈区 在栈上分配一个大小为 65535 字节的字符数组,用作临时缓冲区。
  struct iovec iov[2]; // 声明一个 iovec 结构体数组 iov,它包含两个元素。iovec 结构用于 readv 函数,可以指定多个内存区域进行读写。
  size_t writeable = WritableBytes(); // 先记录能写多少 调用成员函数 WritableBytes 来获取当前缓冲区可写入的字节数,并将其存储在变量 writeable 中。
  // 分散读,保证数据能全部读完
  //设置 iov 数组的第一个元素,iov_base 指向当前可写位置 BeginWrite,iov_len 设置为可写入的字节数 writeable。
  iov[0].iov_base = BeginWrite();
  iov[0].iov_len = writeable;
  //设置 iov 数组的第二个元素,iov_base 指向栈上分配的临时缓冲区 buff,iov_len 设置为 buff 的大小。
  iov[1].iov_base = buff;
  iov[1].iov_len = sizeof(buff);

  /*调用 readv 函数,从文件描述符 fd 读取数据到 iov 指定的两个内存区域。
  readv 会尝试从 fd 读取数据到 iov[0] 和 iov[1] 中,直到填满这两个区域或者 fd 中没有更多数据可读。len 存储读取的总字节数。
  这行代码调用了 readv 函数,从文件描述符 fd 读取数据,最多尝试填满 iov 数组中指定的两个内存区域。具体读取过程如下:
  readv 开始从 fd 读取数据。
  它首先尝试将数据读入 iov[0] 指定的内存区域,即 BeginWrite 处的缓冲区,直到该区域填满或者没有更多数据可读。
  如果 iov[0] 的区域被填满,或者还有更多数据需要读取,readv 会继续从 fd 读取数据,并将数据读入 iov[1] 指定的内存区域,即栈上分配的临时缓冲区 buff。
  readv 会一直读取,直到以下任一条件满足:
  所有 iov 指定的内存区域都被填满。
  fd 中没有更多数据可读。
  遇到了文件的 EOF(文件结束标志)。
  len 变量将存储实际读取的字节数。如果 len 小于 0,表示读取过程中发生了错误,Errno 指针将被用来记录错误码。

  使用 readv 的好处是可以减少数据复制的次数,提高读取大量数据时的效率。它允许系统一次系统调用就向多个目标缓冲区写入数据,而不是多次调用 read 函数并将数据复制到不同的位置。*/
  ssize_t len = readv(fd, iov, 2);

  if (len < 0)  //如果 readv 函数返回一个负值,表示读取失败。
  {
    *Errno = errno; //将当前的 errno(一个全局变量,存储了最近一次系统调用的错误码)值赋值给 Errno 指针指向的变量。
  }
  else if (static_cast<size_t>(len) <= writeable) //如果读取的字节数 len(转换为 size_t 类型)小于或等于可写入的字节数 writeable。
  {                   // 若len小于writable,说明写区可以容纳len
    writePos_ += len; // 将写指针 writePos_ 向前移动 len 字节,因为缓冲区的写区域已经填满了这么多数据。
  }
  else //否则,如果读取的字节数超过了缓冲区的可写入空间。
  {
    //将写指针移动到缓冲区的末尾,然后调用 Append 函数,将临时缓冲区 buff 中剩余的数据追加到 Buffer 对象的内部缓冲区 buffer_ 中。
    writePos_ = buffer_.size();                         // 写区写满了,下标移动到最后
    Append(buff, static_cast<size_t>(len - writeable)); // 剩余的长度
  }
  return len; //返回 readv 函数读取的总字节数。
}

从文件描述符 fd 读取数据到缓冲区,并处理错误。

// 将buffer中可读的区域写入fd中
ssize_t Buffer::WriteFd(int fd, int *Errno)
{
  //调用标准库函数 write,尝试将 Buffer 类中可读的数据写入到文件描述符 fd。
  //Peek 函数返回一个指向缓冲区当前读位置的指针,ReadableBytes 返回可读的字节数。
  //write 函数返回实际写入的字节数,存储在变量 len 中。
  ssize_t len = write(fd, Peek(), ReadableBytes());
  if (len < 0) //如果 write 函数返回的 len 小于 0,表示写入操作失败。
  {
    *Errno = errno;
    return len;
  }
  Retrieve(len); //调用 Buffer 类的 Retrieve 函数,将缓冲区中已写入的 len 字节的数据移除,更新读位置指针。
  return len;
}

将缓冲区中的数据写入文件描述符 fd,并处理错误。

char *Buffer::BeginPtr_()
{
  return &buffer_[0];
}

const char *Buffer::BeginPtr_() const
{
  return &buffer_[0];
}

获取指向缓冲区起始位置的指针,重载以提供 const 和非 const 版本。

void Buffer::MakeSpace_(size_t len)
{
  //检查当前缓冲区的可写空间(WritableBytes())加上可前置空间(PrependableBytes(),即读指针之前的空间)是否小于 len。
  //如果是,说明当前空间不足以满足需求。
  if (WritableBytes() + PrependableBytes() < len)
  {
    /*如果空间不足,使用 resize 方法调整 buffer_ 的大小。
    新的大小是当前写位置 writePos_ 加上需要的长度 len 加 1,
    加 1 表示可能需要额外的空间来存储一个空字符(如果处理的是字符串),或者为其他目的预留空间。*/
    buffer_.resize(writePos_ + len + 1);
  }
  else //如果当前空间足够,执行 else 分支,开始移动缓冲区中的数据。
  {
    size_t readable = ReadableBytes(); //计算当前可读的字节数,即从读指针到写指针之间的字节数。
    //使用 std::copy 算法将从读指针到写指针的数据复制到缓冲区的开头。这是为了将所有未读的数据移动到缓冲区的起始位置,以便在它们之后写入新数据。
    std::copy(BeginPtr_() + readPos_, BeginPtr_() + writePos_, BeginPtr_());
    readPos_ = 0; //重置读指针到缓冲区的开始位置。
    writePos_ = readable; //将写指针设置为当前可读的字节数,因为所有数据已经被复制到了缓冲区的开始。
    assert(readable == ReadableBytes()); //使用断言来确保移动数据后,可读的字节数没有变化。这是一个调试检查,确保缓冲区的状态是一致的。
  }
}

私有方法,用于扩展缓冲区空间以满足写入需求。

  • 26
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值