Muduo网络库 - Buffer模块
目录
设计思路
类的设计
模块实现
私有接口实现
共有接口的实现
实现过程的疑问
主函数
主函数实现
主函数测试
设计思路
Buffer模块是用于通信套接字的缓冲区,用于用户态的输入输出的数据缓存。
为什么要有用户态的输入缓冲区?TCP协议是字节流的,我们无法保证一次收到的数据就刚好是一个完整的报文。 但是我们从TCP的输出缓冲区提取出来之后,在TCP的内核缓冲区中就已经将读取出来的数据无效了,那么我们就势必需要一个用户态的缓冲区用于应对这种情况。
那么为什么需要用户态输出缓冲区呢? 因为如果我们直接向套接字的输出缓冲区写入的话,有可能写条件并不具备,那么这时候我们就会阻塞在写入或者说写入失败,这两种情况要么就是降低了效率,要么就是数据丢失,我们都无法接收,所以我们不能直接向套接字中进行写入,而是每次有新的数据需要写入时,先不写入,而是监听该套接字的写事件,数据线暂存在用户态的输出缓冲区,等到内核缓冲区写时间就绪时再进行写入,这时候就不会阻塞或者因为空间不足而写入失败了。
而如何设计缓冲区呢?
这其实没什么难的,我们只需要讲读取出来或者需要写入的数据以字节为单位缓存起来就行了。
而缓冲区的功能无非就三个 : 缓存数据, 写入数据, 读取数据。
而缓冲区无非就是一块空间,那么我们可以直接使用 vector<char> 来管理这块空间。 为什么不使用 string 呢? 因为 string 更偏向于字符串,他的接口也都是一些字符串的接口,对于我们的缓冲区的实现其实并没有多大的帮助,相反,他的一系列字符串的特性比如说以 \0 结尾,反而会影响我们的操作。 而vector<char> 的底层也是一块线性的空间,操作起来也很简单,同时我们并不需要如何考虑字符串的一些特性。
那么 Buffer 类要如何实现呢?
由于我们使用线性空间来存储数据,就意味着,数据其实在不断的读取和写入的过程中,我们读取数据的时候以及写入数据的时候,大多数情况下,读取和写入的位置都不是这块空间的起始位置。
类的设计
class Buffer
{
//成员变量
private:
std::vector<char> _buf; //缓冲区空间
uint64_t _readerIndex; //读偏移量
uint64_t _writerIndex; //写偏移量
//成员函数 - 内部实现
private:
char* Begin(); //迭代器的起始位置
char* ReadPosition() //获取读的起始位置
char* WritePosition(); //获取写的起始位置
uint64_t FrontReadSize() const; //获取读偏移之前的空闲空间
uint64_t BehindWriteSize() const; //获取写偏移之后的空闲空间
uint64_t TotalFreeSize() const; //获取总的空闲空间
void MoveReadIndex();//移动读偏移
void MoveWriteIndex();//移动写偏移
void EnsureWriteSize();//用来移动数据以及扩容,保证写空间足够
char* FindCRLF();//查找换行符\n
public:
uint64_t ReadSize() const; //获取可读数据的大小
void Read(); //读取数据
void Write(); //写入数据
void ReadAndPop(); //读取数据并且更新读偏移
void WriteAndPush(); //写入数据并且更新写偏移
std::string ReadAsString(); //读取数据,返回string
void WriteAsString(); //写入数据
std::string ReadAsStringAndPop(); //读取数据,返回string,并移动读偏移
void WriteAsStringAndPush(); //写入数据,返回string,并移动写偏移
std::string GetLine(); //获取一行的数据
std::string GetLineAndPop(); //获取一行的数据并且更新读偏移量
void Clear(); //清除缓冲区的数据
};
模块实现
私有接口实现
//成员函数 - 内部实现
private:
char* Begin() //迭代器的起始位置
{
return &(*_buffer.begin());
}
char* ReadPosition()//获取读的起始位置
{
return Begin() + _readerIndex;
}
char* WritePosition() //获取写的起始位置
{
return Begin() + _writerIndex;
}
uint64_t FrontReadSize() const //获取读偏移之前的空闲空间
{
return _writerIndex - _readerIndex;
}
uint64_t BehindWriteSize() const //获取写偏移之后的空闲空间
{
return _buffer.size() - _writerIndex;
}
uint64_t TotalFreeSize() const //获取总的空闲空间
{
return _readerIndex + BehindWriteSize();
}
void MoveReadIndex(size_t len)//移动读偏移
{
assert(ReadSize() >= len);
_readerIndex = _readerIndex + len;
}
void MoveWritelndex(size_t len)//移动写偏移
{
assert(BehindWriteSize() >= len);
_writerIndex = _writerIndex + len;
}
void EnsureWriteSize(size_t len)//用来移动数据以及扩容,保证写空间足够
{
if (BehindWriteSize() >= len)
{
return;
}
if (TotalFreeSize() >= len)
{
uint64_t readsize = ReadSize();
std::copy(ReadPosition(), ReadPosition() + len, Begin());
_readerIndex = 0;
_writerIndex = readsize;
}
else
{
_buffer.resize(_buffer.size() + len);
}
}
char* FindCRLF()
{
char* pos = (char*)memchr(ReadPosition(), '\n', ReadSize());
return pos;
}
共有接口的实现
//成员函数 - 外部实现
public:
Buffer()
:_buffer(1024),_readerIndex(0),_writerIndex(0)
{}
uint64_t ReadSize() //获取可读数据的大小
{
return _writerIndex - _readerIndex;
}
void Read(void* buf, size_t len) //读取数据,不改变读偏移量
{
assert(ReadSize() >= len);
std::copy(ReadPosition(), ReadPosition() + len, (char*)buf);
}
void Write(const void* data, size_t len) //写入数据,不改变写偏移量
{
EnsureWriteSize(len);
std::copy((const char*)data, (const char*)data + len, WritePosition()); //因为void*没有步长的概念 所以要强转成char*类型的
}
void ReadAndPop(void* buf, size_t len) //读取数据并且更新读偏移
{
Read(buf, len);
MoveReadIndex(len);
}
void WriteAndPush(const void* data, size_t len) //写入数据并且更新写偏移
{
Write(data, len);
MoveWritelndex(len);
}
std::string ReadAsString(size_t len)
{
assert(ReadSize() >= len); // 确保可读取数据的大小大于等于 len
std::string ret;
ret.resize(len); // 调整字符串大小
Read(&ret[0], len); // 将数据拷贝到字符串中
return ret;
}
void WriteAsString(const string &data) //写入数据
{
Write(data.c_str(), data.size()); //这里的return它只是告诉编译器,当前函数已经执行完毕,可以退出并返回控制权给调用者。
}
std::string ReadAsStringAndPop(size_t len) //读取数据,返回string,并移动读偏移
{
assert(ReadSize() >= len);
string ret = ReadAsString(len);
MoveReadIndex(len);
return ret;
}
void WriteAsStringAndPush(const string &data) //写入数据,返回string,并移动写偏移
{
WriteAsString(data);
MoveWritelndex(data.size());
}
std::string GetLine() //获取一行的数据
{
char* pos = FindCRLF();
if (pos == nullptr)
{
return "";
}
return ReadAsString(pos - ReadPosition() + 1);
}
std::string GetLineAndPop() //获取一行的数据并且更新读偏移量
{
string str = GetLine();
MoveReadIndex(str.size());
return str;
}
void Clear() //清除缓冲区的数据
{
_buffer.clear();
_readerIndex = 0;
_writerIndex = 0;
}
实现过程的疑问
read和write要传入缓冲区?它们难道不知道吗?
为啥要传入这个缓冲区,是不是在实际应用中 不同的线程都会有一个缓冲区 而不是所有的线程都放在一个缓冲区里面?比如说A和B聊天 他俩有一个缓冲区 C和D聊天 他俩有一个缓冲区
copy函数的用法
std::copy(input_iterator_start, input_iterator_end, output_iterator);
- input_iterator_start:源范围的起始位置(通常是源容器或数组的 begin())。
- input_iterator_end:源范围的结束位置(通常是源容器或数组的 end())。
- output_iterator:目标范围的起始位置(目标容器的 begin())。
获取一行的返回类型为啥是string?
因为是要获取一行的字符串 所以要用string。不能用int char等
read/write是需要提供一个用户级缓冲区的
然后把数据到用户级缓冲区 或者把 用户级缓冲区的数据写入到内部中
为什么要先保存可读数据的大小
memchr函数的用法
void* memchr(const void* ptr, int value, size_t num);
参数说明:
-
ptr
: 指向内存块的指针,表示要搜索的内存区域。 -
value
: 要查找的字符(以int
类型传递),它会被转换为unsigned char
类型进行比较。通常是一个字符或者一个字节值。 -
num
: 要搜索的字节数,指定了从ptr
开始的内存区域的大小。
返回值:
-
如果找到指定的字符,
memchr()
返回一个指向该字符在内存块中第一次出现位置的指针。 -
如果未找到指定的字符,
memchr()
返回NULL
。
void WriteAsString(const string &data) 为啥带引用,并且不带len呢?
void WriteAsString(const string &data)这里的返回类型为啥是void而不是string类型呢?
这里为啥要返回ret
std::string ReadAsStringAndPop(size_t len) //读取数据,返回string,并移动读偏移
{
assert(ReadSize() >= len);
string ret = ReadAsString(len);
MoveReadLndex(len);
return ret;
}
我的错误写法
正确写法
string ReadAsString(uint64_t len)
{
assert(len <= ReadAbleSize()); // 确保要读取的数据量小于等于可读数据的大小
string str; // 创建一个空的字符串
str.resize(len); // 调整字符串大小,以适应读取的数据
// 不能使用 s_tr(),因为是 const,无法修改
// 要传起始地址就要找到第一个字符取地址
Read(&str[0], len); // 从缓冲区读取数据到字符串的内存中
return str; // 返回读取的数据作为字符串
}
主函数
主函数实现
#include "Buffer.hpp"
#include <vector>
#include <iostream>
#include <string>
int main() {
// 创建一个 Buffer 实例
Buffer buffer;
// 写入数据
std::string data = "Hello, world!\nThis is a test.\nsdsd";
buffer.WriteAndPush(data.c_str(), data.size()); // 写入数据
//读取全部数据 不更新读偏移量
char bf[1024] = {0};
buffer.Read(bf,buffer.ReadSize());
std::cout << bf << std::endl;
// 读取并输出一行数据
std::string line1 = buffer.GetLineAndPop(); // 获取第一行并更新读偏移
std::cout << "Line 1: " << line1 << std::endl;
// 再次读取并输出另一行数据
std::string line2 = buffer.GetLineAndPop(); // 获取第二行并更新读偏移
std::cout << "Line 2: " << line2 << std::endl;
// 尝试读取并输出剩余数据
std::string remaining = buffer.GetLineAndPop();
std::cout << "Remaining: " << remaining << std::endl;
// 清除缓冲区
buffer.Clear();
std::cout << "Buffer cleared!" << std::endl;
// 检查清空后的缓冲区内容
std::string afterClear = buffer.GetLineAndPop();
std::cout << "After clear: " << afterClear << std::endl; // 应该是空字符串
return 0;
}
std::cout << bf << std::endl; 这里不用循环打印吗?