手写muduo网络库(八):Buffer

一、引言

在网络编程中,数据的读写是非常常见的操作。由于网络数据的收发往往是异步的,而且数据的大小和到达时间都是不确定的,因此需要一个缓冲区来暂存这些数据。Buffer 类就是为了解决这个问题而设计的,它提供了一个灵活的缓冲区管理机制,能够方便地处理数据的读写操作。在 muduo 网络库中,Buffer 类扮演着重要的角色,下面我们将详细讲解其功能和实现细节。

二、Buffer 类的整体功能概述

Buffer 类主要提供了以下几个核心功能:

  1. 数据的存储:使用 std::vector 来存储数据,方便动态扩容。
  2. 读写操作:提供了读取和写入数据的接口,包括从文件描述符读取数据和向文件描述符写入数据。
  3. 数据管理:通过 readerIndex 和 writerIndex 来管理可读数据和可写数据的范围。

三、Buffer 类的实际结构

3.1 成员变量

private:
    std::vector buffer_;
    size_t readerIndex_;
    size_t writerIndex_;
  • buffer_:使用 std::vector 作为底层存储容器,它可以动态地调整大小,方便存储不同长度的数据。
  • readerIndex_:表示可读数据的起始位置,即从 buffer_ 中 readerIndex_ 位置开始的数据是可以读取的。
  • writerIndex_:表示可写数据的起始位置,即可以从 buffer_ 中 writerIndex_ 位置开始写入数据。

3.2 常量定义

public:
    static const size_t kCheapPrepend = 8;
    static const size_t kInitialSize = 1024;
  • kCheapPrepend:预留的前置空间,用于在数据前面添加一些额外的信息,例如数据长度等。
  • kInitialSize:缓冲区的初始大小。

3.3 构造函数

explicit Buffer(size_t initalSize = kInitialSize)
    : buffer_(kCheapPrepend + initalSize)
    , readerIndex_(kCheapPrepend)
    , writerIndex_(kCheapPrepend)
{
}

构造函数初始化了 buffer_ 的大小为 kCheapPrepend + initalSize,并将 readerIndex_ 和 writerIndex_ 都初始化为 kCheapPrepend,这意味着在 buffer_ 的前 kCheapPrepend 个字节是预留的前置空间。

四、可读缓存和可写缓存设计

4.1 计算可读和可写字节数

size_t readableBytes() const { return writerIndex_ - readerIndex_; }
size_t writableBytes() const { return buffer_.size() - writerIndex_; }
size_t prependableBytes() const { return readerIndex_; }
  • readableBytes():返回当前缓冲区中可读的字节数,即 writerIndex_ 与 readerIndex_ 之间的差值。
  • writableBytes():返回当前缓冲区中可写的字节数,即 buffer_ 的总大小减去 writerIndex_
  • prependableBytes():返回前置空间的字节数,即 readerIndex_

4.2 可读数据的访问

const char *peek() const { return begin() + readerIndex_; }

peek() 函数返回一个指向可读数据起始位置的指针,方便读取数据。

4.3 可写数据的访问

char *beginWrite() { return begin() + writerIndex_; }
const char *beginWrite() const { return begin() + writerIndex_; }

beginWrite() 函数返回一个指向可写数据起始位置的指针,方便写入数据。

五、readerIndex 和 writerIndex 的移动设计

5.1 数据读取后移动 readerIndex

void retrieve(size_t len)
{
    if (len < readableBytes())
    {
        readerIndex_ += len;
    }
    else
    {
        retrieveAll();
    }
}

void retrieveAll()
{
    readerIndex_ = kCheapPrepend;
    writerIndex_ = kCheapPrepend;
}
  • retrieve(size_t len):当读取的数据长度小于可读数据长度时,将 readerIndex_ 向后移动 len 个字节;否则,调用 retrieveAll() 函数将 readerIndex_ 和 writerIndex_ 都重置为 kCheapPrepend
  • retrieveAll():将 readerIndex_ 和 writerIndex_ 都重置为 kCheapPrepend,表示缓冲区中的数据已经全部读取完毕。

5.2 数据写入后移动 writerIndex

void append(const char *data, size_t len)
{
    ensureWritableBytes(len);
    std::copy(data, data+len, beginWrite());
    writerIndex_ += len;
}

append(const char *data, size_t len):在写入数据之前,先调用 ensureWritableBytes(len) 函数确保缓冲区有足够的空间;然后将数据复制到可写位置;最后将 writerIndex_ 向后移动 len 个字节。

5.3 扩容机制

void ensureWritableBytes(size_t len)
{
    if (writableBytes() < len)
    {
        makeSpace(len); // 扩容
    }
}

void makeSpace(size_t len)
{
    if (writableBytes() + prependableBytes() < len + kCheapPrepend)
    {
        buffer_.resize(writerIndex_ + len);
    }
    else
    {
        size_t readable = readableBytes();
        std::copy(begin() + readerIndex_,
                  begin() + writerIndex_,
                  begin() + kCheapPrepend);
        readerIndex_ = kCheapPrepend;
        writerIndex_ = readerIndex_ + readable;
    }
}
  • ensureWritableBytes(size_t len):检查缓冲区是否有足够的可写空间,如果没有,则调用 makeSpace(len) 函数进行扩容。
  • makeSpace(size_t len):当可写空间和前置空间之和小于 len + kCheapPrepend 时,直接将 buffer_ 的大小调整为 writerIndex_ + len;否则,将可读数据移动到前置空间之后,重新调整 readerIndex_ 和 writerIndex_ 的位置。

六、文件描述符的读写操作

6.1 从文件描述符读取数据

Buffer::readFd函数中,使用了readv系统调用实现集中读(也称为 “分散 - 聚集读”),其核心思想是从一个数据源(如文件描述符)读取数据并分散存储到多个缓冲区中。这一机制在网络编程中非常实用,尤其当缓冲区空间不足时,可通过额外的临时缓冲区避免数据丢失。

ssize_t Buffer::readFd(int fd, int *saveErrno)
{
    char extrabuf[65536] = {0};
    struct iovec vec[2];
    const size_t writable = writableBytes();

    vec[0].iov_base = begin() + writerIndex_;
    vec[0].iov_len = writable;

    vec[1].iov_base = extrabuf;
    vec[1].iov_len = sizeof(extrabuf);
    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)
    {
        writerIndex_ += n;
    }
    else
    {
        writerIndex_ = buffer_.size();
        append(extrabuf, n - writable);
    }
    return n;
}

readFd(int fd, int *saveErrno):使用 readv 函数从文件描述符 fd 读取数据,readv 函数可以将数据分散到多个缓冲区中。如果读取的数据长度小于等于可写空间,则直接更新 writerIndex_;否则,将剩余的数据追加到缓冲区中。

集中读的核心优势

  • 减少系统调用次数:一次readv可替代多次read,降低内核态与用户态切换开销。
  • 灵活处理缓冲区空间:当 Buffer 自身可写空间不足时,通过临时缓冲区extrabuf扩展,避免因空间不足导致数据读取不完整。
  • 零拷贝潜力:数据直接读取到多个目标缓冲区,减少内存拷贝次数。

6.2 向文件描述符写入数据

ssize_t Buffer::writeFd(int fd, int *saveErrno)
{
    ssize_t n = ::write(fd, peek(), readableBytes());
    if (n < 0)
    {
        *saveErrno = errno;
    }
    return n;
}

writeFd(int fd, int *saveErrno):使用 write 函数将缓冲区中的可读数据写入到文件描述符 fd 中。

七、总结

Buffer 类是 muduo 网络库中一个非常重要的组件,它提供了一个灵活的缓冲区管理机制,能够方便地处理数据的读写操作。通过 readerIndex 和 writerIndex 的移动设计,实现了对可读数据和可写数据的有效管理。同时,通过扩容机制,保证了缓冲区能够动态地适应不同大小的数据。在实际应用中,需要考虑线程安全和性能优化等问题,以提高系统的稳定性和性能。

你可能感兴趣的:(linux网络编程与服务器开发,网络,开发语言,linux,服务器,c++)