基于C++的HTTP WebServer实现--动态缓冲区

本系列主要记录自己在学习过程的思路、问题、解决方法。最后实现一个高性能的HTTP 服务器,用于学习cpp、网络编程、服务器相关的内容。
系列目录

  1. HTTPServer-动态缓冲区
  2. HTTPServer-Connection类逻辑优化
  3. HTTPServer-核心代码重构(1)
  4. git相关问题与解决方法
  5. Sourcetrail使用说明

本文记录了对于动态缓冲区的学习思路和设计、实现过程,供大家参考。

设计要求

如果不设置读缓冲区,每次都需要申请一个定长的数组,可能会导致资源浪费或内存越界,也不便于提交给上层进行进一步处理。
故本缓冲区希望让tcp连接能够动态管理收到的内容大小,做到一次性读取所有客户端数据,再统一发给客户端。

//不使用缓冲区
char buf[READ_BUFFER];
ssize_t bytes_read = read(sockfd, buf, sizeof(buf));

缓冲区类设计

  1. 底层以vector<char>作为储存结构,便于动态扩容。

相比于使用string,两者都采用指数扩容,但是vector更适合二进制数据(TCP/UDP 报文)的存储。

  1. 采用索引复用来管理读写空间,当数据被消费(应用层处理完),只移动 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 情况下,输出缓冲区数据未写完,等待下一次可写事件
        }

优化方向

  1. 使用 iovec中的writev和readv优化读写操作
  2. 使用零拷贝相关来读取长文件或者长string

小结

完成了Connection类中基础缓冲区的实现,下一步将要对Connection类中相关回调函数、业务逻辑尽心重构。

基于开源项目《30daysCppWebServer》进行学习、实现、优化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值