c++实现一个简易的网络缓冲区的实践
1. 前言
请思考以下几个问题:
1).为什么需要设计网络缓冲区,内核中不是有读写缓冲区吗?
需要设计的网络缓冲区和内核中TCP缓冲区的关系如下图所示,通过socket进行进行绑定。具体来说网络缓冲区包括读(接收)缓冲区和写(发送)缓冲区。设计读缓冲区的目的是:当从TCP中读数据时,不能确定读到的是一个完整的数据包,如果是不完整的数据包,需要先放入缓冲区中进行缓存,直到数据包完整才进行业务处理。设计写缓冲区的目的是:向TCP写数据不能保证所有数据写成功,如果TCP写缓冲区已满,则会丢弃数据包,所以需要一个写缓冲区暂时存储需要写的数据。
2).缓冲区应该设置为堆内存还是栈内存?
假设有一个服务端程序,需要同时连接多个客户端,每一个socket就是一个连接对象,所以不同的socket都需要自己对应的读写缓冲区。如果将缓冲区设置为栈内存,很容易爆掉,故将将其设置为堆内存更加合理。此外,缓冲区容量上限一般是有限制的,一开始不需要分配过大,仅仅在缓冲区不足时进行扩展。
3).读写缓冲区的基本要求是什么?
通过以上分析,不难得出读写缓冲区虽然是两个独立的缓冲区,但是其核心功能相同,可以复用其代码。
读写缓冲区至少提供两类接口:存储数据和读取数据
读写缓冲区要求:先进先出,保证存储的数据是有序的
4).如何界定数据包?
第一种使用特殊字符界定数据包:例如\n
,\r\n
,第二种通过长度界定数据包,数据包中首先存储的是整个数据包的长度,再根据长度进行读取。
5).几种常见的缓冲区设计
①ringbuffer+读写指针
ringbuffer是一段连续的内存,当末端已经写入数据后,会从头部继续写数据,所以感觉上像一个环,实际是一个循环数组。ringbuffer的缺点也很明显:不能够扩展、对于首尾部分的数据需要增加一次IO调用。
②可扩展的读写缓冲区+读写指针
下图设计了一种可扩展的读写缓冲区,在创建时会分配一块固定大小的内存,整个结构分为预留空间数据空间。预留空间用于存储必要的信息,真正存储数据的空间由连续内存组成。此种缓冲区设计相对于ringbuffer能够扩展,但是也有一定的缺点:由于需要最大化利用空间,会将数据移动至开头,移动操作会降低读写速度。
本文实现可扩展的读写缓冲区+读写指针
2. 数据结构
①Buffer类的设计与初始化
Buffer类的数据结构如下所示,m_s
是指向缓冲区的指针,m_max_size
是缓冲区的长度,初始设置为10,并根据扩展因子m_expand_par
进行倍增。扩展因子m_expand_par
设置为2,表示每次扩增长度翻倍,也就是说缓冲区的长度随扩展依次为10、20、40、80。
构造函数的初始化列表中初始化成员变量。实际初始化缓冲区在init函数中分配内存,大小为m_max_size
。不在构造函数中初始化缓冲区的原因是:如果构造函数中分配失败,无法处理,也可使用RAII手段进行处理
②读写指针的位置变化
当缓冲区为空时,读写指针位置相同都为0。
在写入长度为6的数据后,读写指针位置如图
接着读取两个字节后,读写指针如图
③扩展缓冲区实现
扩展缓冲区实际分为两步,将有效数据前移至缓冲区头(最大化利用数据),再进行扩展。根据成员变量扩展因子m_expand_par
的值,将缓冲区按倍数扩大。
假设当前存储数据4个字节,读写指针如下图。需要新增9个字节
将数据前移至缓冲区头
扩展缓冲区为2倍
写入9个字节
实际需要实现的两个私有成员函数:调整数据位置至缓冲区头adjust_buffer()
和扩展expand_buffer()
,设置为私有属性则是因为不希望用户调用,仅仅在写入缓冲区前判断不够就进行扩展,用户不应该知道与主动调用。
adjust_buffer()
实现如下,注释写的较为清楚,不再赘述
扩展缓冲区实现如下:
- 首先根据需要写入的字节数判断缓冲区长度多大才能够容下
- 申请新的存储区,并将数据拷贝到新存储区
- 释放旧缓冲区,将新存储区作为缓冲区
3. 外部接口设计与实现
以读缓冲区为例需要提供的接口有:向缓冲区写入数据write_to_buffer()
,向缓冲区读取数据read_from_buffer()
,得到能够读取的最大字节数readable_bytes()
。
① 写入缓冲区write_to_buffer()
write_to_buffer()
实现的思路如流程图所示:
判断剩余空间:
剩余空间不够:调整数据至头部、扩展缓冲区
剩余空间足够:向下继续
判断当前空间:
当前空间不够:调整数据至头部
剩余空间足够:向下继续
存储数据
根据流程图实现起来逻辑非常清晰,src表示原始数据
流程图中还出现随机一段数据,这是用来调试的。随机初始化一段长度为0~ 40,字符a~ z的数据,并写缓存区
② 读取缓冲区read_from_buffer()
read_from_buffer(char*dst,int read_size)
传入需要拷贝到目的地址和需要读取的字节数,需要注意的是需要读取的字节数为-1
表示全部读取,函数返回实际读取的字节数。实现如流程图所示:
代码如下
③ 丢弃数据pop_bytes
size_t pop_bytes(size_t size)
传入需要丢弃的字节数,需要注意的是需要丢弃的字节数为-1
表示全部丢弃;-2表示随机丢弃0~ 40字节,函数返回实际丢弃的字节数。实现如流程图所示:
④ 其他接口
peek_read()
和peek_write()
返回读写指针的位置
4. 完整代码与测试
① 完整代码
Buffer.h
Buffer.cpp
:
② 测试
随机1000000次测试
到此这篇关于c++实现一个简易的网络缓冲区的实践的文章就介绍到这了,更多相关c++ 网络缓冲区内容请搜索编程学习网以前的文章希望大家以后多多支持编程学习网!