muduo源码剖析--Buffer

news2024/11/15 20:39:57

Buffer类

Buffer类是自定义处理数据输入缓冲的类,底层是vector< char >,通过readIdx和writeIdx将缓冲区分为3个部分,第一部分是预留的8字节+已经读出的缓冲区字节数、第二部分是还未读出的部分、第三部分是可写的部分。
在这里插入图片描述
Buffer类的设计是TcpConnection类设计的核心,一个TcpConnection必须有一个inputBuffer和一个outputBuffer。
必须存在inputBuffer的原因:Tcp是一个无边界的字节流协议,接收方必须要处理"收到的数据尚不构成一条完整的消息”和“一次收到两条消息的数据”等情况,例如对应数据不完整的情况,则收到的数据先放到inputBuffer里,等构成一条完整的消息再通知程序的业务逻辑。
必须存在outputBuffer的原因:考虑这样一种场景,程序想通过Tcp连接发送100kb的数据,但是在write调用中只接受了80kb数据,那么还剩余的20kb数据应该存入outputBuffer中,并注册POLLOUT事件,如果写完了20kb的数据,应该立即停止关注POLLOUT事件,防止busy loop。因为对于应用程序而言,它只管生成数据,不关心数据是一次发送还是几次发送。

// 网络库底层的缓冲区类型定义
class Buffer
{
public:
    static const size_t kCheapPrepend = 8;
    static const size_t kInitialSize = 1024;

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

    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_; }


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

    // 从fd上读取数据
    ssize_t readFd(int fd, int *saveErrno);
    // 通过fd发送数据
    ssize_t writeFd(int fd, int *saveErrno);

private:
    // vector底层数组首元素的地址 也就是数组的起始地址
    char *begin() { return &*buffer_.begin(); }
    const char *begin() const { return &*buffer_.begin(); }

    std::vector<char> buffer_;
    size_t readerIndex_;  //可读的起始位置
    size_t writerIndex_;  //可写的起始位置
};

读buffer的数据并输出为string,主要通过retrieveAllAsString()和retrieveAsString()方法,前者读出buffer所有的数据,后者读出buffer长度为len字节的数据,读出后对readIdx进行相应的维护

    void retrieve(size_t len)
    {
        if (len < readableBytes())
        {
            readerIndex_ += len; // 说明应用只读取了可读缓冲区数据的一部分,就是len长度 还剩下readerIndex+=len到writerIndex_的数据未读
        }
        else // len == readableBytes()
        {
            retrieveAll();
        }
    }
    void retrieveAll()
    {
        readerIndex_ = kCheapPrepend;
        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;
    }

buffer每次append数据的时候,都会保证所有数据都能写入缓冲区,核心是扩容操作,扩容是先检查写入数据是否可以大于已读出的数据的部分+预留部分+可写部分,如果大于则需要进行扩容操作,否则先将未读的数据移动到readIdx起始的位置(readidx=kCheapPrepend, writeIdx =readidx+readableBytes(),然后将需要写入数据append到wireIdx后面


    // 把[data, data+len]内存上的数据添加到writable缓冲区当中
    void append(const char *data, size_t len)
    {
        ensureWritableBytes(len);
        std::copy(data, data+len, beginWrite());
        writerIndex_ += len;
    }

    // buffer_.size - writerIndex_
    void ensureWritableBytes(size_t len)
    {
        if (writableBytes() < len)
        {
            makeSpace(len); // 扩容
        }
    }

    void makeSpace(size_t len)  //扩容操作
    {
        /**
         * | kCheapPrepend |xxx| reader | writer |                     // xxx标示reader中已读的部分
         * | kCheapPrepend | reader |          len          |
         **/
        if (writableBytes() + prependableBytes() < len + kCheapPrepend) // 也就是说 len > xxx + writer的部分
        {
            buffer_.resize(writerIndex_ + len);
        }
        else // 这里说明 len <= xxx + writer 把reader搬到从xxx开始 使得xxx后面是一段连续空间
        {
            size_t readable = readableBytes(); // readable = reader的长度
            std::copy(begin() + readerIndex_,
                      begin() + writerIndex_,  // 把这一部分数据拷贝到begin+kCheapPrepend起始处
                      begin() + kCheapPrepend);
            readerIndex_ = kCheapPrepend;
            writerIndex_ = readerIndex_ + readable;
        }
    }

通过fd读数据到buffer以及通过buffer写入数据到fd,从fd读数据稍显复杂,利用了两个缓冲,,第一个缓冲是buffer自己可写部分的缓冲区,第二个缓冲是在栈空间开辟, 从fd读的数据首先写入第一个缓冲区(buffer自带), 如果数据大于这个缓冲区的大小,则将剩余未写完的数据放入第二个缓冲区,然后将这个缓冲区的数据调用append()方法添加到buffer的后面(可能发生扩容操作)
关于为什么采取双缓冲(buffer缓冲+栈空间缓冲)的方法而不采取一个缓冲区(buffer缓冲):当调用read函数读取数据的时候,从从内核的缓冲区读入到用户态的缓冲,而采取一个缓冲的情况下如果读数据读到一半发现缓冲满了,随后进行扩容操作,但这个时间段内核态的数据可能还会源源不断的输入数据,而这边没办法读入数据(扩容之中),这使得操作系统没法腾出空间容纳更多的数据,这样就得不偿失了采取双缓冲的好处是,从将所有数据一次性从内核态读入到用户态(快速读完),这样操作系统也有可以容纳的空间给新到来的数据,从而提高效率也间接的增大了系统的并发量。

/**
 * 从fd上读取数据 Poller工作在LT模式
 * Buffer缓冲区是有大小的! 但是从fd上读取数据的时候 却不知道tcp数据的最终大小
 *
 * @description: 从socket读到缓冲区的方法是使用readv先读至buffer_,
 * Buffer_空间如果不够会读入到栈上65536个字节大小的空间,然后以append的
 * 方式追加入buffer_。既考虑了避免系统调用带来开销,又不影响数据的接收。
 **/
ssize_t Buffer::readFd(int fd, int *saveErrno)
{
    // 栈额外空间,用于从套接字往出读时,当buffer_暂时不够用时暂存数据,待buffer_重新分配足够空间后,在把数据交换给buffer_。
    char extrabuf[65536] = {0}; // 栈上内存空间 65536/1024 = 64KB

    /*
    struct iovec {
        ptr_t iov_base; // iov_base指向的缓冲区存放的是readv所接收的数据或是writev将要发送的数据
        size_t iov_len; // iov_len在各种情况下分别确定了接收的最大长度以及实际写入的长度
    };
    */

    // 使用iovec分配两个连续的缓冲区
    struct iovec vec[2];
    const size_t writable = writableBytes(); // 这是Buffer底层缓冲区剩余的可写空间大小 不一定能完全存储从fd读出的数据

    // 第一块缓冲区,指向可写空间
    vec[0].iov_base = begin() + writerIndex_;
    vec[0].iov_len = writable;
    // 第二块缓冲区,指向栈空间
    vec[1].iov_base = extrabuf;
    vec[1].iov_len = sizeof(extrabuf);

    // when there is enough space in this buffer, don't read into extrabuf.
    // when extrabuf is used, we read 128k-1 bytes at most.
    // 这里之所以说最多128k-1字节,是因为若writable为64k-1,那么需要两个缓冲区 第一个64k-1 第二个64k 所以做多128k-1
    // 如果第一个缓冲区>=64k 那就只采用一个缓冲区 而不使用栈空间extrabuf[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) // Buffer的可写缓冲区已经够存储读出来的数据了
    {
        writerIndex_ += n;
    }
    else // extrabuf里面也写入了n-writable长度的数据
    {
        writerIndex_ = buffer_.size();
        append(extrabuf, n - writable); // 对buffer_扩容 并将extrabuf存储的另一部分数据追加至buffer_
    }
    return n;
}

// inputBuffer_.readFd表示将对端数据读到inputBuffer_中,移动writerIndex_指针
// outputBuffer_.writeFd标示将数据写入到outputBuffer_中,从readerIndex_开始,可以写readableBytes()个字节
ssize_t Buffer::writeFd(int fd, int *saveErrno)
{
    ssize_t n = ::write(fd, peek(), readableBytes());
    if (n < 0)
    {
        *saveErrno = errno;
    }
    return n;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/431562.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaScript【三】JavaScript中的数组

文章目录&#x1f31f;前言&#x1f31f;数组&#x1f31f;声明&#xff1a;&#x1f31f; 隐式创建&#xff1a;&#x1f31f; 实例化构造函数&#xff1a;&#x1f31f; 注意&#xff1a;一个值为数组的长度。&#x1f31f; 访问&#xff1a;&#x1f31f; 遍历&#xff1a;&…

C++练级之初级:第三篇

C练级之初级&#xff1a;第三篇 1.探索C中函数重载的本质 &#x1f914;首先我们先解决一下为什么C支持函数重载&#xff0c;而C语言不支持&#xff1f; 这里就不得不提起编译链接了&#x1f601;&#xff1b; &#x1f449;这是编译链接篇 以这三个简单的文件为例&#xff1…

C51单片机串口通信(概念部分)

1.通信的基本概念 1.1&#xff1a;串行通信与并行通信 &#xff08;1&#xff09;.串行通信 串行通信是指用一根数据线将 一个字节的八个bit位连接&#xff0c;从低位开始依次传输。 优点&#xff1a;成本便宜&#xff0c;传输稳定 缺点&#xff1a;速度慢 并行通信是指将一…

重学Java设计模式-行为型模式-责任链模式

重学Java设计模式-行为型模式-责任链模式 内容摘自&#xff1a;https://bugstack.cn/md/develop/design-pattern/2020-06-18-重学 Java 设计模式《实战责任链模式》.html#重学-java-设计模式-实战责任链模式「模拟618电商大促期间-项目上线流程多级负责人审批场景」 责任链模…

stegano(图片隐写、摩斯密码)

附件是PDF&#xff0c;我们在选择内容时发现光标溢出了文本 说明这里还存在一些我们看不到的内容 直接CtrlA全选&#xff0c;CtrlC复制后新建一个纯文本文件 将复制的东西粘贴过去 粘贴后发现果然多出来了一些东西&#xff0c;提取出来 BABA BBB BA BBA ABA AB B AAB ABAA A…

3.2 三角分解法

思维导图&#xff1a; 3.2 矩阵的三角分解 3.2.1 什么是矩阵的三角分解&#xff1a; 矩阵的三角分解&#xff0c;也称为LU分解&#xff0c;是一种将一个矩阵分解为一个下三角矩阵和一个上三角矩阵的方法。该分解通常用于解线性方程组和计算矩阵的行列式和逆矩阵。 设A为n*n的…

【通世智库】宁晓红:医疗更完整的样子

2022年的10月&#xff0c;北京协和医院缓和医学中心成立了&#xff0c;这是巨大的好消息&#xff01;北京协和医院连续13年蝉联中国医院排行榜榜首&#xff0c;它率先成立了缓和医学中心&#xff0c;可见缓和医疗在医学领域的重要地位和不可估量的价值。【作者&#xff1a;宁晓…

软件安全之CRC检测

CRC介绍 在玩某些游戏&#xff0c;例如fps类游戏时&#xff0c;你想要修改某些特定的数值实现一些功能&#xff0c;这时你很有可能会被查封账号甚至禁封机器码。因为你更改了游戏中的数据&#xff0c;从而导致接收方收到”错误的数据“。为尽量提高接收方收到数据的正确率&…

可视化Echarts中title、tooltip、legend的常用属性设置

title中常用的设置 配置项--tooltip 配置项--legend title中常用的设置 title 标题组件&#xff0c;包含主标题和副标题。 以下是常用的对标题的设置 title:{//设置图表的标题text:"主标题",link:"baidu.com", //设置标题超链接target:"self&q…

2023最新谷粒商城笔记之支付服务篇(全文总共13万字,超详细)

支付服务 这里我们是使用的支付宝进行支付&#xff0c;所以需要调用支付宝的相关API&#xff0c;下面来了解一下怎样使用支付宝进行线上支付。 支付宝配置相关概念 支付宝开放平台传送门&#xff1a;支付宝开放平台 网站支付DEMO传送门&#xff1a;手机网站支付 DEMO &…

数字滤波器设计——IIR 滤波器

数字滤波器设计实践介绍 此示例说明如何使用 Signal Processing Toolbox 产品中的 designfilt 函数&#xff0c;根据频率响应设定设计 FIR 和 IIR 滤波器。该示例重点讲述低通滤波器&#xff0c;但大多数结果也适用于其他响应类型。 此示例主要介绍数字滤波器的设计&#xff…

D3.js实现线条的流动效果(从一端移动到另一端并且变色)

参考&#xff1a; SVG&#xff1a;理解stroke-dasharray和stroke-dashoffset属性 使用SVG CSS实现动态霓虹灯文字效果 纯CSS实现帅气的SVG路径描边动画效果 实现的效果为&#xff1a;路径左移到完全看不见的地方&#xff0c;然后一边右移&#xff0c;一边从黑色变为红色 <…

社科院与杜兰大学金融管理硕士项目—人生的每一条路都可以看作是正确的路

成年人的世界里没有什么是容易的。生活中经常听到人说&#xff1a;早知道现在过得这么辛苦&#xff0c;当年真应该好好读书&#xff1b;早知道这个行业这么难出头&#xff0c;当年真不应该踏入这一行&#xff1b;早知道爱人这么不靠谱&#xff0c;当年不跟他结婚就好了……有时…

系统集成项目管理工程师软考知识点(每天更新)

第一章指路&#xff1a;系统集成项目管理工程师软考知识点&#xff08;第一章已完结&#xff09;_程序猿幼苗的博客-CSDN博客 第二章指路&#xff1a;系统集成项目管理工程师软考知识点&#xff08;第二章已完结&#xff09;_程序猿幼苗的博客-CSDN博客 本专栏将会更新完整~ …

【DRF开发手册】使用 Django Rest Framework 的 @action 定义自定义方法

本文节选自笔者博客&#xff1a; https://www.blog.zeeland.cn/archives/so3f209hfeac &#x1f496; 作者简介&#xff1a;大家好&#xff0c;我是Zeeland&#xff0c;全栈领域优质创作者。&#x1f4dd; CSDN主页&#xff1a;Zeeland&#x1f525;&#x1f4e3; 我的博客&…

C++ Primer Plus(第6版) 全书重点学习笔记

目录 第10章 对象和类 10.1 过程性编程和面向对象编程 10.2 抽象和类 10.2.1 类简介 10.2.2 实现类成员函数 10.3 类的构造函数和析构函数 10.3.1 声明和定义构造函数 10.3.2 使用构造函数 10.3.3 默认构造函数 10.3.4 析构函数 10.4 this指针 10.5 对象数组 10.6 …

[长安杯 2021学生组]baigei

Index 前言介绍漏洞 利用思路利用过程一.编写交互函数二.填充Tcache Bin三.释放Tcache Bin四.获取Libc地址五.Tcache Bin Attack六.完整EXP&#xff1a; 前言 最近有点迷茫&#xff0c;开始放松自己了。 心态还不是很对&#xff0c;需要继续调整。 介绍 本题是一题经典的堆题…

Java学习笔记:内部类,静态内部类,匿名内部类

​这是本人学习的总结&#xff0c;主要学习资料如下 疯狂Java讲义第三版&#xff0c;李刚编&#xff0c;电子工业出版社出版 目录 1、内部类1.1、内部类简介1.2、内部类与外部类的关系和区别&#xff1a;1.3、内部类的语法 2、 非静态内部类3、静态内部类4、匿名内部类 1、内部…

“链引擎”入驻案例 | 每天超过35万条存证上链,长安链支撑链上价值流动

引言 长安链“链引擎”计划&#xff08;Powered by Chainmaker&#xff09;(简称&#xff1a;PBC计划)是由长安链生态联盟发起的一项应用赋能计划&#xff0c;旨在以长安链技术体系为核心支撑&#xff0c;汇聚产业各方力量&#xff0c;为应用方提供技术、品牌、生态等支持&…

Keil系列教程03_主窗口和工具栏详细说明

1写在前面 本文先让大家简单认识一下Keil的主窗口界面&#xff0c;然后再进一步认识Keil的文件、编译和调试工具栏。 Toolbars工具栏就是在菜单下面的两行快捷图标按钮&#xff0c;这些快捷按钮之所以在工具栏里面&#xff0c;在于它们使用的频率较高。比如保存按钮、编译按钮…