重写muduo网络库:应用层Buffer缓冲区及其重要性

应用层Buffer缓冲区及其重要性

event loop 是 non-blocking 网络编程的核心,在现实生活中,non-blocking 几乎总是和 IO-multiplexing 一起使用,原因有两点:

  1. 没有人真的会用轮询 (busy-pooling) 来检查某个 non-blocking IO 操作是否完成,这样太浪费 CPU资源了。
  2. IO-multiplex 一般不能和 blocking IO 用在一起,因为 blocking IO read()/write()/accept()/connect() 都有可能阻塞当前线程,这样线程就没办法处理其他 socket上的 IO 事件了。

所以,当我们提到 non-blocking 的时候,实际上指的是 non-blocking + IO-multiplexing,单用其中任何一个都没有办法很好的实现功能。

在non-blocking网络编程中,应用层buffer是必须的。
non-blocking IO的核心思想是避免阻塞在read()或write()或者其他IO系统调用上,这样可以最大限度地复用thread-of-control,让一个线程能服务于多个socket连接,IO线程只能阻塞在IO multiplexing函数上,如select/poll/epoll_wait。
这样一来,应用层的缓冲是必须的,每个TCP socket都要有stateful的input buffer和output buffer。

output buffer的必要性

考虑一个场景:程序想通过TCP连接发送100KB的数据,但是在write()调用中,操作系统只接受了80KB(受TCP advertised window的控制),此时肯定不想原地等待,因为不知道会等多久(取决于对方什么时候接收数据,然后滑动TCP窗口)。程序应该金珂交出程序控制权,返回event loop。在这种情况下,剩余的20KB数据怎么办?
对于应用程序而言,它只管生成数据,它不应该关心到底数据是一次性发送还是分成几次发送,这些应该由网络库来操心,程序只要调用TcpConnection: :send()就行了,网络库会负责到底。网络库应该接管这剩余的20kB数据,把它保存在该TCPconnection的output buffer里,然后注册POLLOUT事件,一旦socket变得可写就立刻发送数据。当然,这第二次write()也不一定能完全写人20kB,如果还有剩余,网络库应该继续关注POLLOUT事件;如果写完了20kB,网络库应该停止关注POLLOUT,以免造成busy loop。
如果程序又写人了50kB,而这时候output buffer里还有待发送的20kB数据,那么网络库不应该直接调用write(),而应该把这50kB数据append在那20kB数据之后,等socket变得可写的时候再一并写人。
如果outputbuffer里还有待发送的数据,而程序又想关闭连接(对程序而言,调用TcpConnection: :send()之后他就认为数据迟早会发出去),那么这时候网络库不能立刻关闭连接,而要等数据发送完毕。

input buffer的必要性

TCP是一个无边界的字节流协议,接收方必须要处理“收到的数据尚不构成一条完整的消息”和“一次收到两条消息的数据"等情况。一个常见的场景是,发送方send()了两条1kB的消息(共2kB),接收方收到数据的情况可能是:

  • 一次性收到2kB数据;
  • 分两次收到,第一次600B, 第二次1400B;
  • 分两次收到,第一次1400B, 第二次600B;
  • 分两次收到,第一次1kB,第二次1kB;
  • 分三次收到,第一次600B, 第二次800B,第三次600B;
  • 其他任何可能。一般而言,长度为n字节的消息分块到达的可能性有2^n-1种。

网络库在处理"socket 可读”事件的时候,必须一次性把socket 里的数据读完(从操作系统buffer搬到应用层buffer),否则会反复触发POLLIN事件,造成busy-loop。那么网络库必然要应对“数据不完整”的情况,收到的数据先放到input buffer里,等构成一条完整的消息再通知程序的业务逻辑。

muduo的应用层缓冲区Buffer

Buffer的内部是一个 std::vector,它是一块连续的内存。此外,Buffer有两个data member,指向该vector中的元素。这两个index的类型是int,不是char*,目的是应对迭代器失效。
muduo Buffer的数据结构如下图(摘自《Linux多线程服务端编程:使用muduoC++网络库》):
在这里插入图片描述
两个index把vector的内容分为三块: prependable, readable、 writable, 各块的大小:

  • prependable = readIndex
  • readable = writeIndex - readIndex
  • writable = size() - writeIndex
  • 0 ≤ readIndex ≤ writeIndex≤size()

muduo Buffer里有两个常数kCheapPrepend和kInitialSize,定义了prepend-able的初始大小和writable的初始大小,readable 的初始大小为0。在初始化之后,Buffer的数据结构如下图:
在这里插入图片描述

其中括号里的数字是该变量或常量的值。
重写Buffer.h:

#pragma once

#include <vector>
#include <string>
#include <algorithm>

class Buffer
{
public:
    static const size_t KCheapPrepend = 8;
    static const size_t KInitialSize = 1024;

    explicit Buffer(size_t initialSize = KInitialSize)
        :buffer_(KCheapPrepend + initialSize)
        ,readerIndex_(KCheapPrepend)
        ,writerIndex_(KCheapPrepend)
    {}
    
    size_t readableBytes()const
    {
        return writerIndex_ - readerIndex_;
    }

    size_t writableBytes()const
    {
        return buffer_.size() - writerIndex_;
    }

    size_t prependableBytes()const
    {
        return readerIndex_;
    }

    //返回缓冲区中可读数据的起始地址
    const char* peek() const
    {
        return begin() + readerIndex_;
    }

    //onMessage string <- Buffer
    void retrieve(size_t len)
    {
        if(len < readableBytes())
        {
            readerIndex_ += len;  //应用只读取了刻度缓冲区数据的一部分,就是len,还剩下的readerIndex_+=len-》writerIndex_
        }
        else
        {
            retrieveAll();
        }
    }

    void retrieveAll()
    {
        readerIndex_ = writerIndex_ = KCheapPrepend;
    }

    //把onMessage函数上报的Buffer数据,转成string类型的数据返回
    std::string retrieveAllAsString()
    {
        return retrieveAsString(readableBytes());
    }

    std::string retrieveAsString(size_t len)
    {
        std::string result(peek(), len);
        retrieve(len); //上面一句把缓冲区中可读可读的数据,已经读取出来,这里肯定要对缓冲区进行复位操作
        return result;
    }

    //buffer_.size() - writerIndex_    len  确保可写缓冲区能容纳可写数据
    void ensureWriteableBytes(size_t len)
    {
        if(writableBytes() < len)
        {
            makeSpace(len); //扩容函数
        }
    }

    //把【data,data+len】的数据添加到writable缓冲区当中
    void append(const char* data, size_t len)
    {
        ensureWriteableBytes(len);
        std::copy(data, data + len, beginWrite());
        writerIndex_ += len;
    }

    char* beginWrite()
    {
        return begin() + writerIndex_;
    }

    const char* beginWrite() const
    {
        return begin() + writerIndex_;
    }

    //从fd上读取数据
    ssize_t readFd(int fd, int* savaErrno);
    //通过fd发送数据
    ssize_t writeFd(int fd, int* saveErrno);
private:
    char* begin()
    {
        return &*buffer_.begin();
    }
    const char* begin() const
    {
        return &*buffer_.begin();
    }

    void makeSpace(size_t len)
    {
        //可写空间大小 + readerIndex < len + KCheapPrepend
        if(writableBytes() + prependableBytes() < len + KCheapPrepend)
        {
            buffer_.resize(writerIndex_ + len);
        }
        else
        {
            size_t readable = readableBytes();
            std::copy(begin() + readerIndex_, begin() + writerIndex_, begin() + KCheapPrepend);
            readerIndex_ = KCheapPrepend;
            writerIndex_ = readerIndex_ + readable;
        }
    }

    std::vector<char> buffer_;
    size_t readerIndex_;
    size_t writerIndex_;
};

重写Buffer.cc:

#include "Buffer.h"

#include <errno.h>
#include <sys/uio.h>
#include <unistd.h>

/**
 * 从fd上读数据, Poller工作在LT模式
 * Buffer缓冲区是有大小的,但是从fd上读数据的时候,却不知道tcp数据最终大小
 */

 //从fd上读取数据
ssize_t Buffer::readFd(int fd, int* savaErrno)
{
    char extrabuf[65536] = {0}; //栈内存空间
    struct iovec vec[2];
    const size_t writable = writableBytes(); //这是Buffer底层缓冲区剩余的可写空间大小
    vec[0].iov_base = begin() + writerIndex_;
    vec[0].iov_len = writable;

    vec[1].iov_base = extrabuf;
    vec[1].iov_len = sizeof extrabuf;

    const int iovcnt = (writable < sizeof extrabuf) ? 2 : 1;
    const ssize_t n = ::readv(fd, vec, iovcnt);
    if(n < 0)
    {
        *savaErrno = errno;
    }
    else if(n <= writable) //buffer可写缓冲区已经够存储读出来的数据了
    {
        writerIndex_ += n;
    }
    else //extrabuf 里面也写入了数据
    {
        writerIndex_ = buffer_.size();
        append(extrabuf, n - writable); //writerIndex_ 开始写n-writable大小的数据
    }
    return n;
}


//通过fd发送数据
ssize_t Buffer::writeFd(int fd, int* saveErrno)
{
    ssize_t n = ::write(fd, peek(), readableBytes());
    if(n < 0)
    {
        *saveErrno = errno;
    }
    return n;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_200_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值