Buffer模块
目录
模块设计思想
编辑
模块实现
测试
模块设计思想
Buffer模块是用于通信套接字的缓冲区,用于用户态的输入输出的数据缓存。
为什么要有用户态的输入缓冲区?TCP协议是字节流的,我们无法保证一次收到的数据就刚好是一个完整的报文。 但是我们从TCP的输出缓冲区提取出来之后,在TCP的内核缓冲区中就已经将读取出来的数据无效了,那么我们就势必需要一个用户态的缓冲区用于应对这种情况。
那么为什么需要用户态输出缓冲区呢? 因为如果我们直接向套接字的输出缓冲区写入的话,有可能写条件并不具备,那么这时候我们就会阻塞在写入或者说写入失败,这两种情况要么就是降低了效率,要么就是数据丢失,我们都无法接收,所以我们不能直接向套接字中进行写入,而是每次有新的数据需要写入时,先不写入,而是监听该套接字的写事件,数据线暂存在用户态的输出缓冲区,等到内核缓冲区写时间就绪时再进行写入,这时候就不会阻塞或者因为空间不足而写入失败了。
而如何设计缓冲区呢?
这其实没什么难的,我们只需要讲读取出来或者需要写入的数据以字节为单位缓存起来就行了。
而缓冲区的功能无非就三个 : 缓存数据, 写入数据, 读取数据。
而缓冲区无非就是一块空间,那么我们可以直接使用 vector<char> 来管理这块空间。 为什么不使用 string 呢? 因为 string 更偏向于字符串,他的接口也都是一些字符串的接口,对于我们的缓冲区的实现其实并没有多大的帮助,相反,他的一系列字符串的特性比如说以 \0 结尾,反而会影响我们的操作。 而vector<char> 的底层也是一块线性的空间,操作起来也很简单,同时我们并不需要如何考虑字符串的一些特性。
那么 Buffer 类要如何实现呢?
由于我们使用线性空间来存储数据,就意味着,数据其实在不断的读取和写入的过程中,我们读取数据的时候以及写入数据的时候,大多数情况下,读取和写入的位置都不是这块空间的起始位置。
如图:
所以,我们在设计Buffer类的时候,并不只是需要一个 vector<char> 来管理一块线性空间,还需要两个变量用于维护下一次的读位置和下一次的写位置,我们可以设计成 读偏移和写偏移。
那么未来在读取数据的时候,是需要通过读偏移获取读起始位置进行读取的。同时,未来在写入数据的时候,也是需要通过写偏移获取写的起始位置来进行写入。
而未来在写入的时候就注定会遇到一个问题,就是写起始位置之后的空间不够写入了,这时候应该怎么做呢?
从上面的图中我们就可以看出来,其实空闲空间一两部分,一部分是读偏移之前的空间,另一部分才是写偏移之后的空间。
当我们进行写入的时候,如果写偏移后的空间足够,那么就直接进行写入。
如果写偏移之后的空闲空间不够,那么就需要判断,总的空闲空间是否足够。 也就是我们需要计算出总共的空闲空间。
如果总空闲空间够写入,那么我们其实并不需要进行扩容,我们只需要将可读数据部分移动到前面,把所有的剩余空间都放到后面去。
如果总空闲空间不够新数据的写入,那么这时候我们也没必要挪动数据了,直接扩容,那么扩容到多少呢? 只需要扩容到写偏移之后的空间足够写入就行了,没必要扩容太多浪费空间。
Buffer模块对外需要提供什么功能呢?其实他提供的就是各种读取和写入的接口,以及将缓冲区清空的接口。
不过为了更好的实现这些接口,我们可以再设计一些私有的接口,方便我们进行调用。
首先我们要提供读取数据和写入数据的接口,但是这种读取和写入是不进行数据删除也就是说不移动读偏移和写偏移的,严格意义上来说,这并不是真正的读和写,但是在有些场景下我们确实是需要的,就比如说我们需要判断是否足够一个报文这样的情况。
而读取和写入的时候,我们是需要获取到读起始位置和写起始位置的,那么我们也需要提供这些接口。
既然上面的接口不会移动读偏移和写偏移,那么我们也需要一些接口来移动读偏移和写偏移。
最终就是提供一些整合之后的接口,也就是读取并删除读出来的数据,以及写入数据并移动写偏移。
而读取和写入的时候,为了方便用户调用,我们也可以提供C语言的版本以及C++的版本,也就是原生的char*的版本以及string的版本。
那么我们总结下来,类的设计如下:
class Buffer
{
private:std::vector<char> _buf; //空间uint64_t _read_offset; //读偏移uint64_t _write_offset; //写偏移
private: //私有接口,不对外,用于内部功能的实现char* ReadPosition(); //获取读起始位置uint64_t FrontWriteSize()const; //获取读偏移之前的空闲空间uint64_t BehindWriteSize()const; //获取写偏移之后的空闲空间uint64_t TotalWriteSize()const; //获取总的可写空间char* WritePosition(); //写入的起始位置bool EnsureWriteSize(); //用来移动数据以及扩容等,保证写空间足够bool MoveReadOffset(); //移动读偏移bool MoveWriteOffset(); //游动写偏移
public: //对外提供的功能接口uint64_t ReadSize() const; //获取可读数据大小bool Read(); //读取数据,不移动读偏移bool ReadAndPop(); //读取数据并移动读偏移 bool Write(); //写入数据,不移动写偏移bool WriteAndPush(); //写入数据并移动写偏移std::string ReadAsString(); //读取数据,返回string,不移动读偏移std::string ReadAsStringAndPop(); //读取数据,返回string,并移动读偏移bool WriteString(); //写入string的数据不移动写偏移bool WriteStringAndPush(); //写入string的数据并移动写偏移std::string GetLine(); //获取一行数据std::string GetLineAndPop(); //获取一行数据并移动读偏移void Clear(); //清除缓冲区数据
};
模块实现
实现我们设计的接口
这个类的设计很简单,我们就不讲解思路了
私有接口实现
private: //私有接口,不对外,用于内部功能的实现char* ReadPosition() //获取读起始位置{return &_buf[_read_offset];}uint64_t FrontWriteSize()const //获取读偏移之前的空闲空间{return _read_offset;}uint64_t BehindWriteSize()const //获取写偏移之后的空闲空间{return _buf.size()-_write_offset;}uint64_t TotalWriteSize()const //获取总的可写空间{return FrontWriteSize()+BehindWriteSize();}char* WritePosition() //写入的起始位置{return &_buf[_write_offset];}bool EnsureWriteSize(size_t len) //用来移动数据以及扩容等,保证写空间足够{if(BehindWriteSize()>=len) return true;if(TotalWriteSize()>=len){//将可读数据往前挪动memcpy(&_buf[0],ReadPosition(),ReadSize()); _read_offset =0;_write_offset = ReadSize(); return true;}//空间不够需要扩容_buf.resize(_write_offset+len);return true;} bool MoveReadOffset(size_t len) //移动读偏移{assert(len<=ReadSize());_read_offset += len;return true;}bool MoveWriteOffset(size_t len) //移动写偏移{assert(len<=BehindWriteSize());_write_offset += len;return true;}
功能接口实现:
public: //对外提供的功能接口
#define INIT_SIZE 1024Buffer() //构造函数 :_buf(1024),_read_offset(0),_write_offset(0){} uint64_t ReadSize() const //获取可读数据大小{return _write_offset-_read_offset;}bool Read(char*out,size_t len) //读取数据,不移动读偏移{assert(len<=ReadSize());memcpy(out,ReadPosition(),len);return true;}bool ReadAndPop(char*out,size_t len) //读取数据并移动读偏移 {Read(out,len);MoveReadOffset(len);return true;} bool Write(const char*in,size_t len) //写入数据,不移动写偏移{EnsureWriteSize(len);memcpy(WritePosition(),in,len);return true;}bool WriteAndPush(const char*in,size_t len) //写入数据并移动写偏移{Write(in,len);MoveWriteOffset(len);return true;}std::string ReadAsString(size_t len) //读取数据,返回string,不移动读偏移{assert(len<=ReadSize());std::string ret;ret.resize(len);memcpy(&ret[0],ReadPosition(),len);return ret;}std::string ReadAsStringAndPop(size_t len) //读取数据,返回string,并移动读偏移{std::string ret = ReadAsString(len);MoveReadOffset(len);return ret;}bool WriteString(const std::string& in) //写入string的数据不移动写偏移{return Write(WritePosition(),in.size());}bool WriteStringAndPush(const std::string&in) //写入string的数据并移动写偏移{WriteString(in);return MoveWriteOffset(in.size());}
#define MAX_LINE_SIZE 8092 //我们规定一行不能超过8092,因为一般来说不可能出现怎么长的一行内容,出现了,就说明可能出问题了std::string GetLine() //获取一行数据{int i = 0;char* start = ReadPosition();for(;i<ReadSize()&&i<MAX_LINE_SIZE;++i){if(start[i]=='\n') break;}std::string ret;if(i!=ReadSize()&&i!=MAX_LINE_SIZE) ret = std::string(start,i+1); //注意要提取 i+1 个字节,而不是i个return ret;}std::string GetLineAndPop() //获取一行数据并移动读偏移{std::string ret = GetLine();//如果没有获取到一行,ret.size() 就是 0MoveReadOffset(ret.size());return ret; }void Clear() //清除缓冲区数据{_buf.clear();}
我们这里的重点其实就是在读取一行的实现上,我们需要为其设定一个最大值,一行不能超过8kb,超过8kb,我们就认为出问题了。
测试
Buffer bf;std::string s = "hello world";bf.WriteStringAndPush(s);char buffer[1024]={0};std::string ret = bf.ReadAsStringAndPop(bf.ReadSize());std::cout<<ret<<std::endl;s="clclcllc\n";bf.WriteStringAndPush(s);bf.WriteStringAndPush(s);ret = bf.GetLineAndPop();std::cout<<ret<<std::endl;
目前来看我们的Buffer模块没有问题,后续如果出现问题我们可以再来修改