【Muduo】缓冲区Buffer类

2024-05-25 01:12
文章标签 muduo buffer 缓冲区

本文主要是介绍【Muduo】缓冲区Buffer类,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在 Muduo 网络库中,Buffer类用于处理网络 I/O 中的数据缓冲。防止应用程序读写太快而网络链路收发速度慢导致的速度不匹配问题。这个类封装了一个内部缓冲区(使用了vector<char>),并提供了一系列方法来操作这个缓冲区,如读取、写入、扩容等。

设计目标

Buffer类的设计目标主要有以下几点:

  1. 高性能:通过减少内存拷贝和分配次数来提高性能。
  2. 易用性:提供简洁的API来方便地进行数据的读写操作。
  3. 灵活性:支持动态扩容,以适应不同大小的数据。

主要成员变量

  • std::vector<char> buffer_:存储数据的内部缓冲区。
  • size_t readerIndex_:指向可读数据的起始位置。
  • size_t writerIndex_:指向可写区域的下一个位置。
  • static const size_t kCheapPrepend = 8:预留的前置空间大小,用于优化小数据的写入。
  • static const size_t kInitialSize = 1024:初始缓冲区大小。

缓冲区结构

/// A buffer class modeled after org.jboss.netty.buffer.ChannelBuffer
/// @code
/// +-------------------+------------------+------------------+
/// | prependable bytes |  readable bytes  |  writable bytes  |
/// |      缓冲区头     |     (CONTENT)    |     可写空间     |
/// +-------------------+------------------+------------------+
/// |                   |                  |                  |
/// 0      <=      readerIndex   <=   writerIndex    <=     size
/// @endcodestatic const size_t kCheapPrepend = 8;   // 缓冲区头长度
static const size_t kInitialSize = 1024; // 默认初始化大小

设计思想

Buffer类的设计思想主要体现在以下几个方面:

  1. 双指针设计:使用readerIndex_writerIndex_两个指针来分别追踪可读数据和可写区域的位置,实现了读写区域的分离。
  2. 预留空间:通过预留一定的前置空间(kCheapPrepend),可以减少小数据写入时的内存移动操作,提高性能。
  3. 动态扩容:当可写空间不足时,Buffer类会自动进行扩容,以满足写入需求。

重要成员函数

  1. 构造函数:初始化Buffer对象,设置初始大小和指针位置。
  2. readableBytes():返回可读字节数。
  3. writableBytes():返回可写字节数。
  4. prependableBytes():返回预留空间的大小。
  5. peek():返回指向可读数据的指针。
  6. retrieve(size_t len):将已读数据从缓冲区中移除,并更新readerIndex_
  7. retrieveAll():移除所有已读数据,重置指针位置。
  8. append(const char data, size_t len):将数据追加到缓冲区末尾,并更新writerIndex_
  9. ensureWritableBytes(size_t len):确保至少有len字节的可写空间,如果不足则进行扩容。
  10. makeSpace(size_t len):根据需要扩容或移动数据以腾出足够的可写空间。

扩容策略

Buffer类的扩容策略是其设计的一个亮点。当可写空间不足时,它首先检查整个缓冲区(包括预留空间和已读数据区域)是否足够容纳新的数据。如果足够,它会通过移动数据来腾出空间,而不是直接扩容。这样可以减少不必要的内存分配和拷贝操作,提高性能。只有当整个缓冲区都不足时,它才会进行扩容。

readFd和writeFd方法

  • readFd(int fd, int saveErrno):从文件描述符fd中读取数据到Buffer中。这个方法使用了readv系统调用来提高读取性能,特别是在读取大量数据时。它首先尝试将数据读取到Buffer的可写区域中,如果不够则使用额外的栈缓冲区来存储剩余的数据。最多可以读取 128k-1 个字节。​​​​​​
  • writeFd(int fd, int saveErrno):将Buffer中的数据写入到文件描述符fd对应的发送缓冲区中。这个方法使用了write系统调用来发送数据。它直接将指向可读数据的指针传递给write函数,避免了不必要的内存拷贝。

源码

Buffer.h
#pragma once#include <vector>
#include <string>/// A buffer class modeled after org.jboss.netty.buffer.ChannelBuffer
///                      模仿
/// @code
/// +-------------------+------------------+------------------+
/// | prependable bytes |  readable bytes  |  writable bytes  |
/// |      缓冲区头     |     (CONTENT)    |     可写空间     |
/// +-------------------+------------------+------------------+
/// |                   |                  |                  |
/// 0      <=      readerIndex   <=   writerIndex    <=     size
/// @endcode// 网络库底层的缓冲区类型
class Buffer
{
public:static const size_t kCheapPrepend = 8;   // 缓冲区头长度static const size_t kInitialSize = 1024; // 默认初始化大小Buffer(size_t initialSize = kInitialSize): buffer_(kCheapPrepend + initialSize),readerIndex_(kCheapPrepend),writerIndex_(kCheapPrepend){}~Buffer() = default;// writerIndex_ - readerIndex_size_t readableBytes() const{return writerIndex_ - readerIndex_;}// buffer_.size() - writerIndex_size_t writableBytes() const{return buffer_.size() - writerIndex_;}// return readerIndex_size_t prependableBytes() const{return readerIndex_; // 随着上层的读取,readerIndex_会变}// 返回第一个可读的数据地址const char *peek() const{// char * + readerIndex_return begin() + readerIndex_;}// 将readerIndex_向后移动已经读走的len长度的字节void retrieve(size_t len){if (len < readableBytes())// 长度小于可读大小{readerIndex_ += len;}else // 否则即为读取所有{retrieveAll();}}void retrieveAll(){readerIndex_ = kCheapPrepend;writerIndex_ = kCheapPrepend;}std::string retrieveAllAsString(){return retrieveAsString(readableBytes());}std::string retrieveAsString(size_t len){std::string result(peek(), len); // 第一个可读数据的地址开始的len长度retrieve(len);return result;}/// @brief 从原位置data处开始的len个字节数据追加到缓冲区中/// @param data 源数据位置/// @param len 要追加的字节个数void append(const char* /*restrict*/ data, size_t len){ensureWritableBytes(len); // 确保容量足够std::copy(data, data+len, beginWrite());writerIndex_ += len;}void append(const void* /*restrict*/ data, size_t len){append(static_cast<const char*>(data), len);}void ensureWritableBytes(size_t len){if (writableBytes() < len){makeSpace(len);}}char* beginWrite(){return begin() + writerIndex_;}const char* beginWrite() const{ return begin() + writerIndex_; }// 通过fd读取数据:从fd中read数据,写到缓冲区中的writable区域ssize_t readFd(int fd, int* savedErrno);// 通过fd发送数据:将buffer的readable区域的数据,写入到fd中ssize_t writeFd(int fd, int* savedErrno);private:char* begin(){ return &*buffer_.begin(); }const char *begin() const{// *buffer_.begin()获取到vector第一个元素,使用&取地址return &*buffer_.begin();}// buffer扩容void makeSpace(size_t len){// 若可写大小 + 0~可读位置 不够 要写的数据长度,则申请空间if (writableBytes() + prependableBytes() < len + kCheapPrepend){// FIXME: move readable databuffer_.resize(writerIndex_+len);}else // 否则不申请空间,将未读的数据向前移动到紧挨包头之后{// move readable data to the front, make space inside buffersize_t readable = readableBytes(); // 还未读的数据长度std::copy(begin()+readerIndex_,begin()+writerIndex_,begin()+kCheapPrepend); // 紧挨包头之后readerIndex_ = kCheapPrepend;writerIndex_ = readerIndex_ + readable;}}std::vector<char> buffer_;size_t readerIndex_;size_t writerIndex_;
};
Buffer.cc
#include "Buffer.h"
#include <sys/uio.h>
#include <sys/socket.h>
#include <unistd.h>// 
/*** 从fd中read数据,写到缓冲区中的writable区域* Poller工作在LT模式,数据不会丢* Buffer缓冲区有大小,但是从fd读数据的时候不知道tcp数据最终大小* 所以使用 iovec + extrabuf 使用额外空间
*/
ssize_t Buffer::readFd(int fd, int *savedErrno)
{char extrabuf[65536] = {0}; // 栈上64k额外数据空间,防止读fd的时候buffer_数组不够用/*** struct iovec* {*    void *iov_base;	// Pointer to data.  *    size_t iov_len;	// Length of data.  *  };* 将iovcnt个iovec传入ssize_t readv(int fd, const struct iovec *iov, int iovcnt);* 可以在从fd读取的时候,顺序依次写入每个iovec中*/iovec vec[2]; // 创建两个iovecconst size_t writable = writableBytes(); // Buffer底层缓冲区剩余的可写大小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.const int iovcnt = (writable < sizeof extrabuf) ? 2 : 1; const ssize_t n = ::readv(fd, vec, iovcnt);if(n < 0){*savedErrno = errno;}else if(n < writable){ // Buffer底层缓冲区剩余的可写大小 已经足够writerIndex_ += n;}else{ // 使用了extrabuf,需要将其移动到Buffer中writerIndex_ = buffer_.size(); // buffer_已经写满// 将extrabuf中数据移动到buffer_中append(extrabuf, n - writable);}return n;
}/*** 将buffer的readable区域的数据,写入到fd中
*/
ssize_t Buffer::writeFd(int fd, int *savedErrno)
{ssize_t n = ::write(fd, peek(), readableBytes());if(n <= 0){*savedErrno = errno;}return n;
}

这篇关于【Muduo】缓冲区Buffer类的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1000065

相关文章

Java 文件读写最好是用buffer对于大文件可以加快速度

参考例子: FileReader fileReader = new FileReader(filename);BufferedReader bufferedReader = new BufferedReader(fileReader);List<String> lines = new ArrayList<String>();String line = null;while ((line =

【Node】Buffer 与 Stream

node 为什么会出现 Buffer 这个模块 在最初的时候,JavaScript 只运行在浏览器端, 对于处理 Unicode 编码的字符串很容易,但是对于处理二进制以及非 Unicode 编码的数据便无能为力。 不过对于 Server 端操作来说 网络I/O 以及 文件I/O 的处理是必须的,所以 Node 中便提供了 Buffer 类处理二进制的数据。 二进制缓冲区 Buffer

【0324】Postgres内核 Shared Buffer Access Rules (共享缓冲区访问规则)说明

0. 章节内容 1. 共享磁盘缓冲区访问机制 (shared disk buffers) 共享磁盘缓冲区有两套独立的访问控制机制:引用计数(a/k/a pin 计数)和缓冲区内容锁。(实际上,还有第三级访问控制:在访问任何属于某个关系表的页面之前,必须持有该关系表的适当类型的锁。这里不讨论关系级锁。) Pins 在对缓冲区做任何操作之前,必须“对缓冲区pin”(即增加其引用计数, re

工作集、granule、缓冲区、缓冲池概念及关系?

工作集、granule、缓冲区、缓冲池概念及关系? granule:为了让内存在db_chache_size和shared_pool_size之间高效的移动,oracle在9i重构SGA,使用固定大小的内存块即为granule。这个参数就是为什么当你分配给shared pool值的时候,为什么有时候比你分配的值要大一点,但是granule的整数倍。 缓冲区:内存存放数据的地方,类似于数

Netty源码解析1-Buffer

大数据成神之路系列: 请戳GitHub原文: https://github.com/wangzhiwubigdata/God-Of-BigData 上一篇文章我们概要介绍了Netty的原理及结构,下面几篇文章我们开始对Netty的各个模块进行比较详细的分析。Netty的结构最底层是buffer机制,这部分也相对独立,我们就先从buffer讲起。 What:buffer简介 buffer中文

圆形缓冲区-MapReduce中的

这篇文章来自一个读者在面试过程中的一个问题,Hadoop在shuffle过程中使用了一个数据结构-环形缓冲区。 环形队列是在实际编程极为有用的数据结构,它是一个首尾相连的FIFO的数据结构,采用数组的线性空间,数据组织简单。能很快知道队列是否满为空。能以很快速度的来存取数据。 因为有简单高效的原因,甚至在硬件都实现了环形队列。 环形队列广泛用于网络数据收发,和不同程序间数据交换(比如内核与应用

【LINUX】“dmesg: read kernel buffer failed: Operation not permitted“ 错误

出现 “dmesg: read kernel buffer failed: Operation not permitted” 错误通常是因为当前用户没有权限读取内核日志缓冲区 这可以通过修改内核参数 kernel.dmesg_restrict 来解决。 你可以尝试以下命令来允许非特权用户读取内核日志: sudo sysctl -w kernel.dmesg_restrict=0 这个命令

Kafka【五】Buffer Cache (缓冲区缓存)、Page Cache (页缓存)和零拷贝技术

【1】Buffer Cache (缓冲区缓存) 在Linux操作系统中,Buffer Cache(缓冲区缓存)是内核用来优化对块设备(如磁盘)读写操作的一种机制(故而有一种说法叫做块缓存)。尽管在较新的Linux内核版本中,Buffer Cache和Page Cache已经被整合在一起,但在理解历史背景和功能时,了解Buffer Cache仍然很有帮助。 Buffer Cache 的历史和定义

浅谈Buffer I/O 和 Direct I/O

通常来说,文件I/O可以分为两种: Buffer I/ODirect I/O Buffer I/O 缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。 在 Linux 的缓存 I/O 机制中,这种访问文件的方式是通过两个系统调用实现的:read() 和 write()。调用read()时,如果 操作系统内核地址空间的页缓存( page cache )

两个月冲刺软考——概念+求已知内存按字节编址从(A)…到(B)…的存储容量+求采用单/双缓冲区需要花费的时间计算 类型题目讲解

1.四个周期的区别与联系 时钟周期:也称为CPU周期或机器周期,是CPU操作的基本时间单位。 指令周期:是指CPU执行一条指令所需的全部时间。一个指令周期通常由多个时钟周期组成,因为执行一条指令可能需要多个步骤,如取指令、译码、执行、访存和写回等。 总线周期:总线周期是数据在计算机总线上传输所需的时间。 它涉及CPU与其他系统组件(如内存、输入/输出设备)之间的数据传输。一个总线周期可能包括