[muduo网络库]——muduo库Buffer类(剖析muduo网络库核心部分、设计思想)

2024-05-12 15:28

本文主要是介绍[muduo网络库]——muduo库Buffer类(剖析muduo网络库核心部分、设计思想),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

接着之前我们[muduo网络库]——muduo库Socket类(剖析muduo网络库核心部分、设计思想),我们接下来继续看muduo库中的Buffer类。其实Buffer在我的另一篇博客里面已经介绍过了深究muduo网络库的Buffer类!!!,这里还是和这个之前几个类的方法保持一致,在梳理一遍Buffer类,给大家提供一个舒适的观感~

Buffer类

Buffer类封装了一个用户缓冲区,以及向这个缓冲区写数据读数据等一系列控制方法。

为什么要用Buffer

non-blocking网络编程中,non-blocking IO核心思想是避免阻塞在read()/write()或其他IO系统调用上,可以最大限度复用thread-of-control,让一个线程能服务于多个socket连接。而IO线程只能阻塞在IO-multiplexing函数上,如select()/poll()/epoll_wait(),这样应用层的缓冲区就是必须的,每个TCP socket都要有stateful的input buffer和output buffer。

设计思想

先来谈谈Buffer类整体的设计思想。因为这个编程思想,今后在各个需要缓冲区的项目编程中都可以用到。
muduo的Buffer的定义如下,其内部是 一个 std::vector,且还存在两个size_t类型的readerIndex_writerIndex_标识来表示读写的位置。结构图如下:
在这里插入图片描述
readIndex、writeIndex把整个vector内容分为3块:prependable、readable、writable,各块大小关系:

  • prependable = readIndex
  • readable = writeIndex - readIndex
  • writable = buffer.size() - writeIndex
    Buffer类是可以动态扩容的,在下面的成员函数中,会详细介绍。

重要成员变量

static const size_t kCheapPrepend = 8;  //缓冲区头部
static const size_t kInitialSize = 1024; //缓冲区读写初始大小
std::vector<char> buffer_;
size_t readerIndex_;
size_t writerIndex_;

他们的作用就显而易见了

重要成员函数

  • 通过构造函数,使两个标志位都指向kCheapPrepend
explicit Buffer(size_t initialSize = kInitialSize): buffer_(initialSize + kCheapPrepend), readerIndex_(kCheapPrepend), writerIndex_(kCheapPrepend){}
  • 求出可读可写以及预留区的大小
size_t readableBytes() const { return writerIndex_ - readerIndex_; }
size_t writerableBytes() const { return buffer_.size() - writerIndex_; }
size_t prependableBytes() const { return readerIndex_; }
  • peek()函数,求出缓冲区可读数据的起始位置
const char* peek() const
{return begin() + readerIndex_; 
}
  • 重置标志位readerIndex_writerIndex_
void retrieve(size_t len) //len表示已经读了的
{if(len < readableBytes()) {//已经读的小于可读的,只读了一部分len//还剩readerIndex_ += len 到 writerIndex_readerIndex_ += len; }else //len == readableBytes(){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); //从起始位置读len长retrieve(len);return result;
}
  • 将data数据添加到缓冲区中,并且更新writerIndex_ 指向。
void append(const char* data, size_t len) //添加数据
{ensureWriterableBytes(len);std::copy(data, data+len, beginWrite());writerIndex_ += len;
}

先来看看beginWrite,开始写得位置。

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

注意到这里有一个ensureWriterableBytes,它判断了缓冲区还有多少能写的位置,以及扩容

void ensureWriterableBytes(size_t len)
{if (writerableBytes() < len){makeSpace(len); //扩容}     
}
  • 如何正确扩容,这就是Buffer的精髓了
void makeSpace(size_t len){if (prependableBytes() + writerableBytes() < len + kCheapPrepend){buffer_.resize(writerIndex_ + len);}else{size_t readable = readableBytes(); //保存一下没有读取的数据std::copy(begin()+readerIndex_, begin()+writerIndex_, begin()+ kCheapPrepend); //挪一挪readerIndex_ = kCheapPrepend;writerIndex_ = readerIndex_+readable;}}

扩容巧妙思想在于,因为两个指针的不断移动,导致指向可读数据的指针一直后移,预留区越来越大,如果一味的扩容,会导致前面预留区越来越大,这样造成了浪费,所以muduo库采用了以下思路进行判断,何时需要扩容:

  • 利用prependableBytes() + writerableBytes() 判断了整个Buffer上面剩余的可写入的空间,如果这个空间小于要写入的以及预留的8字节位置的总和,那么直接扩容!!
  • 如果大于说明目前剩余的位置还足够存放要写入的数据,那么通过vector的数据拷贝,把Buffer里面的数据挪一挪,这时候readerIndex_就指向了初始位置,writerIndex_的位置就是目前可写入的首地址,这样在进行写入,就不需要一味的扩容。
    过程图如下:
    在这里插入图片描述在这里插入图片描述
  • 客户端发来数据,readFd从该TCP接收缓冲区中将数据读出来并放到Buffer中。
ssize_t Buffer::readFd(int fd,int* saveErrno)
{char extrabuf[65536] = { 0 }; //栈上内存空间struct iovec vec[2];const size_t writable = writerableBytes(); //buffer底层缓冲区剩余的可写的空间大小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) //buffer可写的缓冲区已经够存储读取出来的数据{writerIndex_ += n;}else //extrabufl里面也写入了数据{writerIndex_ = buffer_.size();append(extrabuf,n-writable);  //writerIndex_ 开始写n-writable的数据}return n;
}

巧妙点在哪里呢?
我们在读数据的时候,不知道数据的最终大小是多少,所以采用了如下的方法:

  1. 首先定义了一个64K栈缓存extrabuf临时存储,利用栈的好处是可以自动的释放,并计算出目前剩余可写的空间大小;
  2. 利用结构体 iovec 指定了两块缓冲区,一块是目前剩余的可写的Buffer,一个是临时的缓冲区,指定了起始位置以及缓冲区的大小;
  3. const int iovcnt = (writable < sizeof extrabuf) ? 2 : 1; 如果writable < sizeof extrabuf就选2块内存readv,否则一块就够用;
  4. 读数据const ssize_t n = ::readv(fd, vec, iovcnt);
  5. 若读取的数据超过现有内部buffer_的writable空间大小时, 启用备用的extrabuf 64KB空间, 并将这些数据添加到内部buffer_的末尾。
  • 服务端要向这条TCP连接发送数据,将Buffer中的数据拷贝到TCP发送缓冲区中。
ssize_t Buffer::writeFd(int fd,int* saveErrno)
{ssize_t n = ::write(fd, peek(), readableBytes());if(n < 0){*saveErrno = errno;}return n;
}

这就是调用了系统的write函数。

iovec结构体定义

#include <sys/uio.h>struct iovec {ptr_t iov_base; /* Starting address */size_t iov_len; /* Length in bytes */
};

struct iovec定义了一个向量元素。通常,这个结构用作一个多元素的数组。对于每一个传输的元素,指针成员iov_base指向一个缓冲区,这个缓冲区是存放的是readv所接收的数据或是writev将要发送的数据。成员iov_len在各种情况下分别确定了接收的最大长度以及实际写入的长度。

代码地址:https://github.com/Cheeron955/mymuduo/tree/master

好了~ 有关于muduo库Buffer类的细节就到此结束了,Buffer的设计思想还是很巧妙的,在我们之后用到缓冲区的地方都可以借鉴。接下来我们会介绍muduo库的TcpConnection类,我们下一节见~~

这篇关于[muduo网络库]——muduo库Buffer类(剖析muduo网络库核心部分、设计思想)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


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

相关文章

python如何下载网络文件到本地指定文件夹

《python如何下载网络文件到本地指定文件夹》这篇文章主要为大家详细介绍了python如何实现下载网络文件到本地指定文件夹,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下...  在python中下载文件到本地指定文件夹可以通过以下步骤实现,使用requests库处理HTTP请求,并结合o

Java Jackson核心注解使用详解

《JavaJackson核心注解使用详解》:本文主要介绍JavaJackson核心注解的使用,​​Jackson核心注解​​用于控制Java对象与JSON之间的序列化、反序列化行为,简化字段映射... 目录前言一、@jsonProperty-指定JSON字段名二、@JsonIgnore-忽略字段三、@Jso

Linux高并发场景下的网络参数调优实战指南

《Linux高并发场景下的网络参数调优实战指南》在高并发网络服务场景中,Linux内核的默认网络参数往往无法满足需求,导致性能瓶颈、连接超时甚至服务崩溃,本文基于真实案例分析,从参数解读、问题诊断到优... 目录一、问题背景:当并发连接遇上性能瓶颈1.1 案例环境1.2 初始参数分析二、深度诊断:连接状态与

Qt实现网络数据解析的方法总结

《Qt实现网络数据解析的方法总结》在Qt中解析网络数据通常涉及接收原始字节流,并将其转换为有意义的应用层数据,这篇文章为大家介绍了详细步骤和示例,感兴趣的小伙伴可以了解下... 目录1. 网络数据接收2. 缓冲区管理(处理粘包/拆包)3. 常见数据格式解析3.1 jsON解析3.2 XML解析3.3 自定义

一文详解如何在Python中从字符串中提取部分内容

《一文详解如何在Python中从字符串中提取部分内容》:本文主要介绍如何在Python中从字符串中提取部分内容的相关资料,包括使用正则表达式、Pyparsing库、AST(抽象语法树)、字符串操作... 目录前言解决方案方法一:使用正则表达式方法二:使用 Pyparsing方法三:使用 AST方法四:使用字

Python列表去重的4种核心方法与实战指南详解

《Python列表去重的4种核心方法与实战指南详解》在Python开发中,处理列表数据时经常需要去除重复元素,本文将详细介绍4种最实用的列表去重方法,有需要的小伙伴可以根据自己的需要进行选择... 目录方法1:集合(set)去重法(最快速)方法2:顺序遍历法(保持顺序)方法3:副本删除法(原地修改)方法4:

SpringQuartz定时任务核心组件JobDetail与Trigger配置

《SpringQuartz定时任务核心组件JobDetail与Trigger配置》Spring框架与Quartz调度器的集成提供了强大而灵活的定时任务解决方案,本文主要介绍了SpringQuartz定... 目录引言一、Spring Quartz基础架构1.1 核心组件概述1.2 Spring集成优势二、J

Linux系统配置NAT网络模式的详细步骤(附图文)

《Linux系统配置NAT网络模式的详细步骤(附图文)》本文详细指导如何在VMware环境下配置NAT网络模式,包括设置主机和虚拟机的IP地址、网关,以及针对Linux和Windows系统的具体步骤,... 目录一、配置NAT网络模式二、设置虚拟机交换机网关2.1 打开虚拟机2.2 管理员授权2.3 设置子

揭秘Python Socket网络编程的7种硬核用法

《揭秘PythonSocket网络编程的7种硬核用法》Socket不仅能做聊天室,还能干一大堆硬核操作,这篇文章就带大家看看Python网络编程的7种超实用玩法,感兴趣的小伙伴可以跟随小编一起... 目录1.端口扫描器:探测开放端口2.简易 HTTP 服务器:10 秒搭个网页3.局域网游戏:多人联机对战4.

Mysql删除几亿条数据表中的部分数据的方法实现

《Mysql删除几亿条数据表中的部分数据的方法实现》在MySQL中删除一个大表中的数据时,需要特别注意操作的性能和对系统的影响,本文主要介绍了Mysql删除几亿条数据表中的部分数据的方法实现,具有一定... 目录1、需求2、方案1. 使用 DELETE 语句分批删除2. 使用 INPLACE ALTER T