c++怎样实现一个简易的网络缓冲区的实践

这期内容当中小编将会给大家带来有关c++怎样实现一个简易的网络缓冲区的实践,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。

1. 前言

请思考以下几个问题:

1).为什么需要设计网络缓冲区,内核中不是有读写缓冲区吗?

需要设计的网络缓冲区和内核中TCP缓冲区的关系如下图所示,通过socket进行进行绑定。具体来说网络缓冲区包括读(接收)缓冲区和写(发送)缓冲区。设计读缓冲区的目的是:当从TCP中读数据时,不能确定读到的是一个完整的数据包,如果是不完整的数据包,需要先放入缓冲区中进行缓存,直到数据包完整才进行业务处理。设计写缓冲区的目的是:向TCP写数据不能保证所有数据写成功,如果TCP写缓冲区已满,则会丢弃数据包,所以需要一个写缓冲区暂时存储需要写的数据。

c++怎样实现一个简易的网络缓冲区的实践

2).缓冲区应该设置为堆内存还是栈内存?

假设有一个服务端程序,需要同时连接多个客户端,每一个socket就是一个连接对象,所以不同的socket都需要自己对应的读写缓冲区。如果将缓冲区设置为栈内存,很容易爆掉,故将将其设置为堆内存更加合理。此外,缓冲区容量上限一般是有限制的,一开始不需要分配过大,仅仅在缓冲区不足时进行扩展。

c++怎样实现一个简易的网络缓冲区的实践

3).读写缓冲区的基本要求是什么?

通过以上分析,不难得出读写缓冲区虽然是两个独立的缓冲区,但是其核心功能相同,可以复用其代码。
读写缓冲区至少提供两类接口:存储数据和读取数据
读写缓冲区要求:先进先出,保证存储的数据是有序的

4).如何界定数据包?

第一种使用特殊字符界定数据包:例如\n,\r\n,第二种通过长度界定数据包,数据包中首先存储的是整个数据包的长度,再根据长度进行读取。

5).几种常见的缓冲区设计

①ringbuffer+读写指针
ringbuffer是一段连续的内存,当末端已经写入数据后,会从头部继续写数据,所以感觉上像一个环,实际是一个循环数组。ringbuffer的缺点也很明显:不能够扩展、对于首尾部分的数据需要增加一次IO调用。

c++怎样实现一个简易的网络缓冲区的实践

②可扩展的读写缓冲区+读写指针
下图设计了一种可扩展的读写缓冲区,在创建时会分配一块固定大小的内存,整个结构分为预留空间数据空间。预留空间用于存储必要的信息,真正存储数据的空间由连续内存组成。此种缓冲区设计相对于ringbuffer能够扩展,但是也有一定的缺点:由于需要最大化利用空间,会将数据移动至开头,移动操作会降低读写速度。

本文实现可扩展的读写缓冲区+读写指针

c++怎样实现一个简易的网络缓冲区的实践

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;
}

c++怎样实现一个简易的网络缓冲区的实践