逐步构建HTTP服务器(四)——设计并使用缓冲

逐步构建HTTP服务器(四)——设计并使用缓冲

设计缓冲

设计缓冲的目的:解决不能一次read和write全部数据、未及时将套接字接受缓冲区读出造成的反复触发读事件就绪(busy-loop)。

Linux多线程服务端编程 7.4.2 为什么non-blocking 网络编程中应用层buffer是必需的

简单想法:

  • 从缓冲读出n个字节:从optr开始读n个字节,并将optr往后移n个字节

  • 写入缓冲n个字节:从iptr开始写n个字节,并将iptr往后移n个字节

实现为:

  • 为了使缓冲空间足够灵活,可以使用向量
struct Buffer
{
    Buffer(size_t kBufferInitSize = 1024) : buffer_(kBufferInitSize), optr_(0), iptr_(0) {}

    std::vector<char> buffer_;
    size_t optr_;
    size_t iptr_;
};
  • 缓冲的要点在于: 每次可写空间放不下时,进行缓冲空间的扩充:

    1. 当整个(已读 + 可读 + 可写) < (可读 + 要写入的数据),将可读部分移到最前面即可
    2. 反之,循环将缓冲空间翻倍,直至能够放下要写的数据。同时在进行空间申请->数据复制过程中顺带将可读部分移到最前面
  • 对套接字read,无法预知大小问题:在栈上准备一个额外缓冲区,使用readv分别读到我们的缓冲和额外缓冲区。这样有两种情况:

    1. 我们的缓冲+额外缓冲区没有被写满,将额外缓冲区写入我们的缓冲即可。
    2. 我们的缓冲+额外缓冲区被写满,可能套接字接受缓冲区还有数据,就得等下个循环再去读取。我们当然是希望一次就能读完,所有就应该设一个适合大小的额外缓冲区,这里我们设65536个字节。
  • 例子

  1. 从缓冲区读出

    // 读出
    std::vector<char> ReadBuffer(Buffer *buf, int len = -1)
    {
        if (len == -1) // read all
        {
            len = buf->iptr_ - buf->optr_;
        }
        std::vector<char> result(buf->buffer_.begin() + buf->optr_,
                                buf->buffer_.begin() + buf->optr_ + len);
        buf->optr_ = buf->iptr_;
        return result;
    }
    

    读出较为简单,只需要拷贝数据,后移optr便可

  2. 写入缓冲区

    若单单是写入数据也较简单,也是拷贝数据,后移iptr。

    void AppendBuffer(Buffer *buf, std::vector<char>::iterator first,
                    std::vector<char>::iterator last)
    {
        std::copy(first, last, buf->buffer_.begin() + buf->iptr_);
        buf->iptr_ += last - first;
    }
    

    但是写入缓冲区,首先要判断缓冲区是否有足够空间可供写入。

    void TryAppendBuffer(Buffer *buf, std::vector<char> &data, int len)
    {
        if (len <= 0)
            return;
        int ndestdata = buf->iptr_ - buf->optr_;
        // 1.若buf->buffer_.size() >= buf->iptr_ + len
        // 即可写部分足够写下要写入数据,不做移动和扩充
        if (buf->buffer_.size() < buf->iptr_ + len)
        {
            // 2. 若new_size >= ndestdata + len
            // 即把可读部分移到最前便有足够空间写入要写入数据
            // 只需要完成移动,new_size等于原buffer的size
            int new_size = buf->buffer_.size();
            // 3. 存不下要写入数据,必须扩充空间
            while (new_size < ndestdata + len)
            {
                new_size <<= 1;
            }
            std::vector<char> new_buf(new_size);
            std::copy(buf->buffer_.begin() + buf->optr_,
                    buf->buffer_.begin() + buf->iptr_,
                    new_buf.begin());
            buf->buffer_ = new_buf;
            buf->iptr_ = buf->iptr_ - buf->optr_;
            buf->optr_ = 0;
        }
        // 写入Buffer
        AppendBuffer(buf, data.begin(), data.begin() + len);
    }
    
  3. 小总结

    定义了一个Buffer结构体,内有vector类型的数据data,用以存储缓冲区数据。还有读“指针”optr和写“指针”iptr

    对Buffer操作的三个函数:

    1. ReadBuffer(),以string形式从缓冲区读出数据
    2. AppendBuffer(),写入缓冲区
    3. TryAppendBuffer(),在必要时扩充缓冲区,而后调用AppendBuffer()写入缓冲区

使用缓冲

在使用epoll的echo服务器上进行修改:

  1. 对于可读事件就绪的处理:有个明显差别在于,我们读进数据后,不再直接读出,而是将读出来的数据放入缓冲,然后开始关注套接字的可写事件,当可写事件就绪后我们再调用write去写。

    // read available
    if (events[i].events & EPOLLIN)
    {
        // read available
        if (events[i].events & EPOLLIN)
        {
            // 使用介绍过的extrabuf
            size_t nwriteable = ibuf.buffer_.size() - ibuf.iptr_;
            std::vector<char> extrabuf(65536);
            struct iovec iov[2];
            iov[0].iov_base = &*(ibuf.buffer_.begin() + ibuf.iptr_);
            iov[0].iov_len = nwriteable;
            iov[1].iov_base = &*extrabuf.begin();
            iov[1].iov_len = 65536;
    
            int n = readv(events[i].data.fd, iov, 2);
    
            if (n < 0) // error
            {
                std::cout << "read error: " << errno << std::endl;
            }
            else if (n == 0) // close
            {
                close(events[i].data.fd);
                epoll_ctl(epollfd, EPOLL_CTL_DEL, events[i].data.fd, &events[i]);
            }
            else // n > 0,有从套接字读出写入ibuf缓冲
            {
                // 使用了extrabuf,将extrabuf写入缓冲
                if (n > nwriteable)
                {
                    TryAppendBuffer(&ibuf, extrabuf, n - nwriteable);
                }
                ibuf.iptr_ += n;
    
                // 从ibuf写到obuf
                // 是可以仅使用一个buffer,为了和以后的代码有个对应,这里不妨使用ibuf和
                // obuf,并根据echo服务器的需要,手动将ibuf的可读部分搬到obuf
    
                // 模拟从ibuf中读出
                std::vector<char> data = ReadBuffer(&ibuf);
    
                // 写入obuf Buffer
                // 调用TryAppendBuffer,当obuf可写空间不足时会进行扩充
                TryAppendBuffer(&obuf, data, data.size());
    
                // !!!
                // 开始关注可写事件
                epoll_event ev;
                ev.events = events[i].events | EPOLLOUT;
                ev.data.fd = events[i].data.fd;
                epoll_ctl(epollfd, EPOLL_CTL_MOD, events[i].data.fd, &ev);
            }
        }
    
    1. 处理可写事件:当缓冲区里没有数据了,也就是写完了,我们就可以取消关注可写事件
    // write available
    if (events[i].events & EPOLLOUT)
    {
        std::vector<char> data = ReadBuffer(&obuf);
        int n = write(events->data.fd, &*data.begin(), data.size());
        // 从Buffer读出的并不一定能够写完
        obuf.optr_ -= (data.size() - n);
    
        // 写完了,不再关注可写事件
        if (obuf.optr_ == obuf.iptr_)
        {
            epoll_event ev;
            ev.events = events[i].events & ~EPOLLOUT;
            ev.data.fd = events[i].data.fd;
            epoll_ctl(epollfd, EPOLL_CTL_MOD, events[i].data.fd, &ev);
        }
    }
    

遗憾

可以看到随着缓冲的引入,代码也进一步的复杂,是时候使用OOP(object-oriented programming)了,下节介绍Reactor模式。

posted @ 2021-08-16 22:58  ithepug  阅读(72)  评论(0编辑  收藏  举报