基于多反应堆的高并发服务器【C/C++/Reactor】(中)Buffer的创建和销毁、扩容、写入数据

news2024/11/15 8:49:26

TcpConnection:封装的就是建立连接之后得到的用于通信的文件描述符,然后基于这个文件描述符,在发送数据的时候,需要把数据先写入到一块内存里边,然后再把这块内存里边的数据发送给客户端,除了发送数据,剩下的就是接收数据。接收数据,把收到的数据先存储到一块内存里边。也就意味着,无论是发送数据还是接收数据,都需要一块内存。并且这块内存是需要使用者自己去创建的。所以就可以把这块内存做封装成Buffer。

>>>>>>>>>>>>>>>>>>>>>>>>>>>>学习笔记>>>>>>>>>>>>>>>>>>>>>>>>>>>>

1.文件描述符与数据发送:

  • 在发送数据时,需要先将数据写入内存缓冲区(buffer)。
  • 内存缓冲区可以通过封装成一个Buffer结构体来实现
  • Buffer结构体中包含一个指向内存的指针(data)、内存总大小(capacity)、读数据位置(readPos)和写数据位置(writePos)等成员

2.Buffer结构体及其成员说明:

  • 指针:指向内存地址(data)
  • 总大小:内存块的字节数(capacity)
  • 读位置:当前读取数据的位置(readPos)
  • 写位置:当前写入数据的位置(writePos)

3.Buffer API函数:

  • 提供一系列API函数,以便对buffer中的内存进行操作
  • 主要操作包括初始化buffer和进行读写操作

4.初始化Buffer:

  • 需要为buffer申请指定大小的堆内存
  • 使用malloc函数申请堆内存,并将内存地址返回给调用者
  • 初始化buffer结构体中的成员,包括data指针、容量、读位置和写位置
  • data指针需要指向一个有效的内存块,因此需要再次申请内存
  • 使用memset函数将data指针指向的内存块初始化为零
  • 返回buffer指针给调用者

>>>>>>>>>>>>>>>>>>>>>>>>>>>>Buffer的创建和销毁>>>>>>>>>>>>>>>>>>>>>>>>>>>>

  • Buffer.h 
struct Buffer {
    // 指向内存的指针
    char* data;
    int capacity;
    int readPos;
    int writePos;
}

 (一)Buffer的初始化

// 初始化
struct Buffer* bufferInit(int size);
// 初始化
struct Buffer* bufferInit(int size) {
    struct Buffer* buffer = (struct Buffer*)malloc(sizeof(struct Buffer));
    if(buffer!=NULL) {
        buffer->data = (char*)malloc(sizeof(char) * size);
        buffer->capacity = size;
        buffer->readPos = buffer->writePos = 0;
        memset(buffer->data, 0, size);
    }
    return buffer;
}

 (二)Buffer的销毁

// 销毁
void bufferDestroy(struct Buffer* buf);
// 销毁
void bufferDestroy(struct Buffer* buf) {
    if(buf!=NULL) {
        if(buf->data!=NULL) { // buf->data指向有效的堆内存
            free(buf->data); // 释放
        }
    }
    free(buf);
}


>>>>>>>>>>>>>>>>>>>>>>>>>>>>Buffer的扩容>>>>>>>>>>>>>>>>>>>>>>>>>>> 

(一)readPoswritePos 相对位置发生变化的三种情况:

(1)Buffer初始时 - 未写入任何数据

(2)Buffer - 写入了部分数据

  • 剩余的可写的内存容量 = 可写数据内存大小
// 得到剩余的可写的内存容量
int bufferWriteableSize(struct Buffer* buf);
// 得到剩余的可写的内存容量
int bufferWriteableSize(struct Buffer* buf) {
    return buf->capacity - buf->writePos;
}

 (3)Buffer - 写入了部分数据并读出了部分数据

  • 计算已写数据内存(未读)的大小 
// 已写数据内存(未读)的大小 --- 得到剩余的可读的内存容量
int bufferReadableSize(struct Buffer* buf);
// 已写数据内存(未读)的大小 --- 得到剩余的可读的内存容量
int bufferReadableSize(struct Buffer* buf) {
    return buf->writePos - buf->readPos;
}

对于内存数据已读的区域的数据为无效数据,此处的无效指的是内存数据,由于数据已经被读了出来,故这里边的数据已经无效了。对于这个图来说,剩余的可用内存块一共有多大呢? 

  • 剩余的可写的内存容量 = 内存数据已读大小 + 可写数据内存大小

但这个是理论值,因为这两块内存不是连续的,故即使空间够存储,但是不连续的存放会导致读写麻烦。此时的解决方案是:移动内存实现合并内存

(1)先获取已写数据内存(未读)这块内存的大小,将值赋给readableSize

// 得到已写但未读的内存大小
int readableSize = bufferReadableSize(buf);

(2)然后把这块内存的数据拷贝到前面去,这就实现了合并

// 移动内存实现合并
memcpy(buf->data, buf->data + buf->readPos, readableSize);

 (3)更新位置

// 更新位置
buf->readPos = 0;
buf->writePos = readableSize;

(二)Buffer扩容

当往buffer中写入数据时,如果剩余的内存不足以容纳新的数据,需要进行扩容。有三种情况需要考虑:     

  1. 剩余的可写的内存容量够用- 不需要扩容
  2. 内存需要合并才够用 - 不需要扩容
  3. 内存不够用 - 需要扩容
// 扩容
void bufferExtendRoom(struct Buffer* buf, int size);
// 扩容
void bufferExtendRoom(struct Buffer* buf, int size) {
    // 1.内存够用 - 不需要扩容
    if(bufferWriteableSize(buf)>= size) {
        return;
    }
    // 2.内存需要合并才够用 - 不需要扩容
    // 剩余的可写的内存 +  已读的内存 >= size
    else if(bufferWriteableSize(buf) + bufferReadableSize(buf) >= size) {
        // 得到已写但未读的内存大小
        int readableSize = bufferReadableSize(buf);
        // 移动内存实现合并
        memcpy(buf->data, buf->data + buf->readPos, readableSize);
        // 更新位置
        buf->readPos = 0;
        buf->writePos = readableSize;
    }
    // 3.内存不够用 - 需要扩容
    else{
        void* temp = realloc(buf->data, buf->capacity + size);
        if(temp ==NULL) {
            return;// 失败了
        }  
        memset(temp + buf->capacity, 0, size);// 只需要对拓展出来的大小为size的内存块进行初始化就可以了
        // 更新数据
        buf->data = temp;
        buf->capacity += size;
    }
}

>>>>>>>>>>>>>>>>>>>>>>>>>>>>往Buffer里写入数据>>>>>>>>>>>>>>>>>>>>>>>>>>> 

(1)直接写  

// 写内存 1.直接写 
int bufferAppendData(struct Buffer* buf, const char* data, int size); 

int bufferAppendString(struct Buffer* buf, const char* data); 
// 写内存 1.直接写 
int bufferAppendData(struct Buffer* buf, const char* data, int size) {
    // 判断传入的buf是否为空,data指针指向的是否为有效内存,以及数据大小是否大于零
    if(buf == NULL || data == NULL || size <= 0) {
        return -1;
    }
    // 扩容(试探性的)
    bufferExtendRoom(buf,size);
    // 数据拷贝
    memcpy(buf->data + buf->writePos, data, size);
    // 更新写位置
    buf->writePos += size;
    return 0;
}

int bufferAppendString(struct Buffer* buf, const char* data) {
    int size = strlen(data);
    int ret = bufferAppendData(buf, data, size);
    return ret;
}

实现bufferAppendData函数重点:

1. 实现写内存函数时,需要判断传入的buf是否为空,data指针指向的是否为有效内存,以及数据大小是否大于零

2. 在写数据之前,需要进行内存扩容(试探性的,可能剩余的可写容量就够写入那就不必扩容)

3. 写数据时,需要从上次写入的writePos位置开始

4. 数据写入完成后,需要更新writePos的位置

总结:在实现bufferAppendData函数时,需要考虑如何处理内存的写入和接收数据的情况。在写数据之前,可能需要进行内存扩容以确保有足够的空间。写数据时,需要从上次写入的writePos位置开始。完成写入后,需要再次更新writePos的位置。

(2)接收套接字数据

#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
struct iovec {
    void  *iov_base;    /* Starting address */
    size_t iov_len;     /* Number of bytes to transfer */
};

功能:readv函数从文件描述符(包括TCP Socket)中读取数据,并将读取的数据存储到指定的多个缓冲区中。
-> 成功时返回接收的字节数,失败时返回-1

filedes 传递接收数据的文件(套接字)描述符
iov 包含数据保存位置和大小的iovec结构体数组的地址值
iovcnt 第二个参数中数组的长度

fd:要读取数据的文件描述符,可以是TCP Socket。
iov:存储读取数据的多个缓冲区的数组。
iovcnt:缓冲区数组的长度。
返回值:成功时返回实际读取的字节数,失败时返回-1,并设置errno变量来指示错误的原因。

read/recv/readv  在接收数据的时候,

  • read/recv 只能指定一个数组
  • readv 能指定多个数组(也就是说第一个用完,用第二个...)

readv函数可以一次接收多个缓冲区中的数据,并在内核中减少了多次系统调用的开销。

// 写内存 2.接收套接字数据
int bufferSocketRead(struct Buffer* buf,int fd);
  • bufferSocketRead函数实现功能:当调用这个bufferSocketRead函数之后,一共接收到了多少个字节
  • bufferSocketRead函数具体细节:在这个函数里边,通过malloc申请了一块临时的堆内存(tmpbuf),这个堆内存是用来接收套接字数据的。当buf里边的数组容量不够了,那么就使用这块临时内存来存储数据,还需要把tmpbuf这块堆内存里边的数据再次写入到buf中。当用完了之后,需要释放内存。

注意事项

  • 使用者在调用readv函数时需要准备结构体的数组
  • 在接收数据时,如果内存已满,数据将被写入下一个结构体中的内存
  • 计算buf里边的数组中剩余的写操作内存

内存的扩展和拷贝

  • 调用bufferAppendData函数来实现
// 写内存 2.接收套接字数据
int bufferSocketRead(struct Buffer* buf,int fd) {
    struct iovec vec[2]; // 根据自己的实际需求来
    // 初始化数组元素
    int writeableSize = bufferWriteableSize(buf); // 得到剩余的可写的内存容量
    // 0号数组里的指针指向buf里边的数组,记得 要加writePos,防止覆盖数据
    vec[0].iov_base = buf->data + buf->writePos;
    vec[0].iov_len = writeableSize;

    char* tmpbuf = (char*)malloc(40960); // 申请40k堆内存
    vec[1].iov_base = buf->data + buf->writePos;
    vec[1].iov_len = 40960;
    // 至此,结构体vec的两个元素分别初始化完之后就可以调用接收数据的函数了
    int result = readv(fd, vec, 2);// 表示通过调用readv函数一共接收了多少个字节
    if(result == -1) {
        return -1;// 失败了
    }
    else if (result <= writeableSize) { 
        // 说明在接收数据的时候,全部的数据都被写入到vec[0]对应的数组里边去了,全部写入到
        // buf对应的数组里边去了,直接移动writePos就好
        buf->writePos += result;
    }
    else {
        // 进入这里,说明buf里边的那块内存是不够用的,
        // 所以数据就被写入到我们申请的40k堆内存里边,还需要把tmpbuf这块
        // 堆内存里边的数据再次写入到buf中。
        // 先进行内存的扩展,再进行内存的拷贝,可调用bufferAppendData函数
        // 注意一个细节:在调用bufferAppendData函数之前,通过调用readv函数
        // 把数据写进了buf,但是buf->writePos没有被更新,故在调用bufferAppendData函数
        // 之前,需要先更新buf->writePos
        buf->writePos = buf->capacity; // 需要先更新buf->writePos
        bufferAppendData(buf, tmpbuf, result - writeableSize);
    }
    free(tmpbuf);
    return result;
}

>>总结: 在实现内存扩容函数时,需要考虑如何处理内存的写入和接收数据的情况。写数据之前可能需进行内存扩容,并从上次写入的writePos位置开始,完成写入后再次更新writePos的位置。

写内存的方式

  • 直接写入:将数据存储到buf结构体对应的内存空间
  • 基于套接字接收数据:使用readv等函数

写内存函数的考虑因素

  • 判断指针指向的是否为有效内存
  • 数据大小是否大于零

内存扩容的必要性

  • 在写数据之前,需要进行内存扩容以确保有足够的空间

数据写入的过程

  • 从上次写入的writePos位置开始
  • 数据写入完成后,再次更新writePos的位置

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

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

相关文章

Stable Diffusion好用的显卡推荐

Stable Diffusion 是一款顶级的人工智能艺术生成工具&#xff0c;以其快速的性能、用户友好的界面和显着的效果而闻名。然而&#xff0c;在沉浸体验之前&#xff0c;有必要验证您的计算机&#xff08;显卡&#xff09;是否符合最佳功能所需的严格规范。今天我们将介绍三款高性价…

Ubuntu22.04开机左上角下划线闪烁不开机

按下CtrlAltF2&#xff0c;打开TTY系统&#xff0c;然后通过用户名和密码登录&#xff0c;随后使用 sudo apt --fix-broken install 根据提示排除错误信息&#xff0c;然后使用apt安装lightdm安装就行。 tips:当使用EasyConnect的时候&#xff0c;你可能参考了下面这篇文章知…

基础数据结构第八期 并查集

前言 并查集这部分还是挺重要的&#xff0c;应该要熟练掌握哦&#xff01;&#xff01;&#xff01; 一、并查集的基本概念 作用&#xff1a; 1、将两个集合合并 2、查询是否在一个集合内 基本原理&#xff1a; 每个集合用一棵树来表示&#xff0c;树根的编号就是整个集合…

Pixi.js的魅力

摘要&#xff1a;官网 Web开发的时代&#xff0c;图形和动画已经成为了吸引用户注意力的重要手段之一。而 Pixi.js 作为一款高效、易用的2D渲染引擎&#xff0c;已经成为了许多开发者的首选~~ 项目中&#xff0c;有一些图像的处理操作&#xff08;3D图&#xff0c;2D图都有&…

时序预测 | Matlab基于CNN-LSTM-SAM卷积神经网络-长短期记忆网络结合空间注意力机制的时间序列预测(多指标评价)

时序预测 | Matlab基于CNN-LSTM-SAM卷积神经网络-长短期记忆网络结合空间注意力机制的时间序列预测(多指标评价) 目录 时序预测 | Matlab基于CNN-LSTM-SAM卷积神经网络-长短期记忆网络结合空间注意力机制的时间序列预测(多指标评价)预测效果基本介绍程序设计参考资料 预测效果 …

npm 和 Yarn:一场关于包管理的战争(下)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

Baumer工业相机堡盟工业相机如何通过NEOAPI SDK实现相机的高速图像保存(C++)

Baumer工业相机堡盟工业相机如何通过NEOAPI SDK实现相机的高速图像保存&#xff08;C&#xff09;&#xff09; Baumer工业相机Baumer工业相机的图像高速保存的技术背景Baumer工业相机通过NEOAPI SDK函数图像高速保存在NEOAPI SDK里实现线程高速图像保存&#xff1a;工业相机高…

芯片验证入门踩坑指南(1)

因为一些原因&#xff0c;从华为数通C软件开发到海思这边做芯片验证&#xff0c;快一个月&#xff0c;说下一些心得与体会&#xff1a; 如何快速上手&#xff1a; 因为项目非常赶&#xff0c;几乎没有脱产学习时间&#xff0c;就是直接干项目&#xff0c;一开始不需要知道原理…

【leetcode】力扣热门之合并两个有序列表【简单难度】

题目描述 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 用例 输入&#xff1a;l1 [1,2,4], l2 [1,3,4] 输出&#xff1a;[1,1,2,3,4,4] 输入&#xff1a;l1 [], l2 [] 输出&#xff1a;[] 输入&#xff1a;l1 []…

开源、云原生且实时分析型的现代数据仓库DataBend的介绍,及其与其它开源文件存储的结合使用实例

DataBend介绍 Databend 是一个开源、云原生且实时分析型的现代数据仓库&#xff0c;旨在提供高效的数据存储和处理能力。它采用 Rust 语言开发&#xff0c;并支持 Apache Arrow 格式以实现高性能列式存储与查询处理。 主要特点&#xff1a; 云原生设计&#xff1a;Databend 构…

[VUE]1-创建vue工程

目录 基于脚手架创建前端工程 1、环境要求 2、操作过程 3、工程结构 4、启动前端服务 &#x1f343;作者介绍&#xff1a;双非本科大三网络工程专业在读&#xff0c;阿里云专家博主&#xff0c;专注于Java领域学习&#xff0c;擅长web应用开发、数据结构和算法&#xff0c…

即时战略游戏的AI策略思考

想起来第一次玩RTS游戏&#xff0c;就是框住一大群兵进攻&#xff0c;看他们把对面消灭干净……我接触的第一款游戏是《傲世三国》那会儿是小学&#xff0c;后来高中接触了魔兽地图编辑器&#xff0c;我发现自己喜欢直接看属性而省去争论和试验的步骤——我喜欢能一眼看透的感觉…

20240107移远的4G模块EC20在Firefly的AIO-3399J开发板的Android11下调通能上网

20240107移远的4G模块EC20在Firefly的AIO-3399J开发板的Android11下调通能上网 2024/1/7 11:17 开发板&#xff1a;Firefly的AIO-3399J【RK3399】SDK&#xff1a;rk3399-android-11-r20211216.tar.xz【Android11】 Android11.0.tar.bz2.aa【ToyBrick】 Android11.0.tar.bz2.ab …

【Spring实战】25 Spring Boot Admin 应用

文章目录 1. 查看健康信息2. 使用 Micrometer 和 "/metrics"3. 管理包和类的日志级别4. 其他功能总结 Spring Boot Admin 是一个功能强大的工具&#xff0c;用于监控和管理多个 Spring Boot 应用程序。通过上一篇文章 【Spring实战】24 使用 Spring Boot Admin 管理…

Android getApplication()、getApplicationContext的区别

在Android中&#xff0c;getApplication()和getApplicationContext()是两种获取应用程序上下文的方法&#xff0c;但它们有一些细微的区别。 getApplication()方法&#xff1a; getApplication()方法通常用于Activity或Fragment中&#xff0c;它返回当前Activity或Fragment所属…

MySQL-存储引擎

简介&#xff1a;存储引擎是存储数据&#xff0c;建立索引&#xff0c;更新/查询数据等技术的实现方式。存储引擎是基于表的&#xff0c;而不是基于库的&#xff0c; (同一个数据库的不同表可以选择不同的存储引擎) 所以存储引擎也可被称为表类型。 我们输入 SHOW CREATE TAB…

Django 10 表单

表单的使用流程 1. 定义 1. terminal 输入 django-admin startapp the_14回车 2. tutorial子文件夹 settings.py INSTALLED_APPS 中括号添加 "the_14", INSTALLED_APPS [django.contrib.admin,django.contrib.auth,django.contrib.contenttypes,django.contrib…

Kafka(五)生产者

目录 Kafka生产者1 配置生产者bootstrap.serverskey.serializervalue.serializerclient.id""acksallbuffer.memory33554432(32MB)compression.typenonebatch.size16384(16KB)max.in.flight.requests.per.connection5max.request.size1048576(1MB)receive.buffer.byte…

Fowsniff

靶场搭建 遇到扫描不到的情况&#xff0c;可以尝试改靶机的网络为NAT模式&#xff0c;在靶机启动时按”esc“&#xff0c;进入Advanced options for Ubantu&#xff0c;选择recovery mode&#xff0c;选择network&#xff0c;按方向键”→“&#xff0c;OK&#xff0c;然后res…

Python爬虫获取百度的图片

一. 爬虫的方式&#xff1a; 主要有2种方式: ①ScrapyXpath (API 静态 爬取-直接post get) ②seleniumXpath (点击 动态 爬取-模拟) ScrapyXpath XPath 是 Scrapy 中常用的一种解析器&#xff0c;可以帮助爬虫定位和提取 HTML 或 XML 文档中的数据。 Scrapy 中使用 …