本系列主要记录自己在学习过程的思路、问题、解决方法。最后实现一个高性能的HTTP 服务器,用于学习cpp、网络编程、服务器相关的内容。
系列目录
本文记录了对于动态缓冲区的学习思路和设计、实现过程,供大家参考。
设计要求
如果不设置读缓冲区,每次都需要申请一个定长的数组,可能会导致资源浪费或内存越界,也不便于提交给上层进行进一步处理。
故本缓冲区希望让tcp连接能够动态管理收到的内容大小,做到一次性读取所有客户端数据,再统一发给客户端。
//不使用缓冲区
char buf[READ_BUFFER];
ssize_t bytes_read = read(sockfd, buf, sizeof(buf));
缓冲区类设计
- 底层以
vector<char>作为储存结构,便于动态扩容。
相比于使用string,两者都采用指数扩容,但是vector更适合二进制数据(TCP/UDP 报文)的存储。
- 采用索引复用来管理读写空间,当数据被消费(应用层处理完),只移动 read_index_,而不是立即清空缓冲区。当有新数据写入时,从 write_index_ 开始写,必要时扩容或移动数据。避免每次都清空缓冲区导致频繁分配和释放内存。
read_index_当前读指针,指向尚未消费的数据起始位置write_index_当前写指针,指向已经写入数据的末尾。
缓冲区示意图:
buffer_: [已读数据][可读数据][可写空间]
^ ^ ^
0 readIndex_ writeIndex_
class MyBuffer
{
public:
MyBuffer();
~MyBuffer();
void readToBuffer(const int &fd);
ssize_t writeFromBuffer(const int &fd);
void addToBuffer(const char *data, size_t len);
void addToBuffer(const std::string &data);
void cleanBuffer();
size_t getUnreadSize() const { return cur_write_index_ - cur_read_index_; }
std::string getContentAsString();
std::string getContentAsString(size_t len);
private:
std::vector<char> buffer_;
size_t cur_read_index_;
size_t cur_write_index_;
};
成员函数设计
readToBuffer
这个函数主要的功能是把socket中的内容读取到缓冲区中,由于使用非阻塞读,所以需要多次读直到返回EAGAIN或EWOULDBLOCK。同时实现缓冲区不足时自动扩容
char temp_buffer[TEMP_BUFFER_SIZE];
while (true)
{
ssize_t n = read(fd, temp_buffer, TEMP_BUFFER_SIZE);
if (n > 0)
{
if (write_index_ + n > buffer_.size())
{
buffer_.resize(write_index_ + n + TEMP_BUFFER_SIZE); // 扩容
}
std::copy(temp_buffer, temp_buffer + n, buffer_.begin() + write_index_);
write_index_ += n;
}
}
可以把 temp_buffer 定义为类成员,或者用固定大小的栈数组,如果使用vector会导致反复分配资源
writeFromBuffer
这个函数主要的功能是把缓冲区中的内容发送到socket中。
while (read_index_ < write_index_)
{
ssize_t n = write(fd, buffer_.data() + read_index_, write_index_ - read_index_);
if (n > 0)
{
read_index_ += n;
}
}
// 如果数据全部写完,重置索引以复用缓冲区
if (read_index_ == write_index_)
{
read_index_ = 0;
write_index_ = 0;
}
addToBuffer
在缓冲区末尾追加写入,如果不够则扩容。
getContentAsString
将缓冲区中的内容以字符串的形式读出,支持定长读取和全部读取。
std::string MyBuffer::getContentAsString(size_t len)
{
if (len > getUnreadSize())
{
len = getUnreadSize();
}
std::string s(buffer_.data() + cur_read_index_, len);
cur_read_index_ += len;
if (cur_read_index_ == cur_write_index_)
{
cleanBuffer();
}
return s;
}
cleanBuffer
通过设置读写指针的方式实现对于缓冲区的复用。
void MyBuffer::cleanBuffer()
{
cur_read_index_ = 0;
cur_write_index_ = 0;
}
读写逻辑
在server类中处理连接的读写数据时通过对缓冲区的操作来实现。
从输入缓冲区拿到本次读到的数据(在 MyConnection 的 Channel 回调中已读入):
if (in.getUnreadSize() > 0)
{
std::string msg = in.getContentAsString();
printf("message from client fd %d: %s\n", c_sockfd, msg.c_str());
}
将一个简单http的header和body写入缓冲区后发送:
out.addToBuffer(header, hdrLen);
out.addToBuffer(kBody, bodyLen);
if (out.writeFromBuffer(c_sockfd) < 0)
{
if (errno != EAGAIN)
{
// 其他错误,关闭连接
printf("write error on fd %d, closing connection\n", c_sockfd);
delete conn;
connections_.erase(it);
return;
}
// EAGAIN 情况下,输出缓冲区数据未写完,等待下一次可写事件
}
优化方向
- 使用 iovec中的writev和readv优化读写操作
- 使用零拷贝相关来读取长文件或者长string
小结
完成了Connection类中基础缓冲区的实现,下一步将要对Connection类中相关回调函数、业务逻辑尽心重构。
4877

被折叠的 条评论
为什么被折叠?



