muduo缓冲区buffer类

268 阅读4分钟

Buffer是一个缓冲区,这个缓冲区在我们基于nonblocking非阻塞IO的服务端编程里面,一个缓冲区还是非常有必要,而且我们在TCP编程里面经常出现TCP粘包问题,我们一般都会在通讯的数据头来描述数据的长度,每一次根据数据的长度来截取包的大小进行反序列化进行处理,可能这一次截取的数据比较多,但是我们应用只需要根据包头的大小读取一部分,所以未读的数据就需要在缓冲区存放。同理,发送端可能需要发送的数据比较多,一次无法发送完成,剩下的数据也需要存放在缓冲区。

一、Buffer底层的简单描述

数据成员

image.png

image.png

  • 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

  1. readv函数介绍
ssize_t readFd(int fd, int* saveErrno);

Buffer缓冲区是有大小的(占用堆区内存),但是我们无法知道fd上的流式数据有多少,如果我们将缓冲区开的非常大,大到肯定能容纳所有读取的数据,这就太浪费空间了,muduo库中使用readv方法,根据读取的数据多少开动态开辟缓冲区 image.png 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:缓冲区的可允许写的长度
  1. 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;
}