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。

class Buffer{
   
public:
	Buffer();									//构造
	~Buffer();	
	int init();									//分配缓冲区
private:
	char* m_s;									//缓冲区指针
	size_t m_read_index;						//读指针位置
	size_t m_write_index;						//写指针位置
	size_t m_max_size;							//缓冲区长度
	size_t m_expand_par;						//扩展因子
};

构造函数的初始化列表中初始化成员变量。实际初始化缓冲区在init函数中分配内存,大小为m_max_size不在构造函数中初始化缓冲区的原因是:如果构造函数中分配失败,无法处理,也可使用RAII手段进行处理

Buffer::Buffer()
	:m_read_index(0),m_write_index(0),m_max_size(10), m_expand_par(2),m_s(nullptr)
{
   }

Buffer::~Buffer()
{
   
	delete[] m_s;
}

int Buffer::init()
{
   
	m_s = new char[m_max_size]();
	if (m_s == nullptr) {
   
		cout << "分配m_s失败\n";
		return -1;
	}
	return 0;
}

②读写指针的位置变化

当缓冲区为空时,读写指针位置相同都为0。
在这里插入图片描述

在写入长度为6的数据后,读写指针位置如图
在这里插入图片描述
接着读取两个字节后,读写指针如图
在这里插入图片描述

③扩展缓冲区实现

扩展缓冲区实际分为两步,将有效数据前移至缓冲区头(最大化利用数据),再进行扩展。根据成员变量扩展因子m_expand_par的值,将缓冲区按倍数扩大。

假设当前存储数据4个字节,读写指针如下图。需要新增9个字节
在这里插入图片描述
将数据前移至缓冲区头

在这里插入图片描述
扩展缓冲区为2倍
在这里插入图片描述
写入9个字节
在这里插入图片描述
实际需要实现的两个私有成员函数:调整数据位置至缓冲区头adjust_buffer()扩展expand_buffer(),设置为私有属性则是因为不希望用户调用,仅仅在写入缓冲区前判断不够就进行扩展,用户不应该知道与主动调用。

class Buffer {
   
public:
...
private:	
	void adjust_buffer();						//调整数据位置至缓冲区头部头
	void expand_buffer(size_t need_size);		//扩展缓冲区长度
...
}

adjust_buffer()实现如下,注释写的较为清楚,不再赘述

void Buffer::adjust_buffer()
{
   
	if (m_read_index == 0)					//数据已经在头部,直接返回
		return;
	int used_size = m_write_index - m_read_index;
	if (used_size == 0) {
   					//缓冲区为空,重置读写指针
		m_write_index = 0;
		m_read_index = 0;
	}
	else {
   
		cout << "调整前read_index write_index" << m_read_index << " " << m_write_index << endl;
		memcpy(m_s, &m_s[m_read_index], used_size);		//将数据拷贝至头部
		m_write_index -= m_read_index;						//写指针也前移
		cout << "调整了" << used_size << "个字节" << endl;
		m_read_index = 0;								//读指针置0
	}
	
	cout << "调整后read_index write_index" << m_read_index << " " << m_write_index << endl;
}

扩展缓冲区实现如下:

  • 首先根据需要写入的字节数判断缓冲区长度多大才能够容下
  • 申请新的存储区,并将数据拷贝到新存储区
  • 释放旧缓冲区,将新存储区作为缓冲区
void Buffer::expand_buffer(size_t need_size)			//need_size需要写入的字节数
{
   
	size_t used_size = m_write_index - m_read_index;	//used_size表示已经存储的字节数
	size_t remain_size = m_max_size - used_size;		//remain_size表示剩余空间
	size_t expand_size = m_max_size;					
	while (remain_size < need_size) {
   					//剩余空间不够时扩展,用while表示直到扩展至够用
		expand_size *= m_expand_par;
		remain_size = expand_size - used_size;
		//cout << "扩展长度中... 总剩余 总长度 " << remain_size << "  " << expand_size << endl;
	}
	char* s1 = new char[expand_size]();					//申请新的空间
	memcpy(s1, m_s, m_max_size);
	free(m_s);
	m_s = s1;											//将新空间挂载到缓冲区
	m_max_size = expand_size;							//更新缓冲区总长度
	//cout << "扩展结束,总长度为" << m_max_size << endl;
}

3. 外部接口设计与实现

以读缓冲区为例需要提供的接口有:向缓冲区写入数据write_to_buffer(),向缓冲区读取数据read_from_buffer(),得到能够读取的最大字节数readable_bytes()

class Buffer {
   
public:
	void write_to_buffer(char* src);				//从src中写数据
	size_t readable_bytes();						//存储数据的字节数
	size_t read_from_buffer(char *dst,int bytes);	//读数据
	size_t pop_bytes(size_t bytes);					//丢弃数据
}

① 写入缓冲区write_to_buffer()

write_to_buffer()实现的思路如流程图所示:

  • 判断剩余空间:

    剩余空间不够:调整数据至头部、扩展缓冲区
    剩余空间足够:向下继续

  • 判断当前空间&#x

  • 6
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值