muduo之Buffer解析

为什么采用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。

Tcpconnection必须要有output buffer ,考虑一个场景,程序想通过TCP连接发送100KB的数据,但是在write调用中,操作系统只接受了80KB,肯定不想在此等待,因为不知道会等多久(取决于对方什么时候接受数据,然后滑动TCP窗口)。程序需要尽快交出控制权,返回event loop。在这种情况下,剩余的20KB怎么办?

对于应用程序而言,它只管生成数据,它不关心到底数据是一次性发送还是分多少次发送的,这些由网络库关心,程序只需要调用TcpConnection::send()就行了,网络库会负责到底。网络库应该接管剩余的20KB数据,把它保存在该TCPconnection的output buffer里,然后注册pollout事件。 当然如果写完了,那么应该停止关注POLLOUT事件,以免造成busy loop,因为muduo采用的是level trigger。

首先我们来看buffer初始的时候的样子
muduo之Buffer解析_第1张图片

头部保留了8个字节。我们具体来看看Buffer类的实现,一些不是太重要的函数我们就不看了,来看具体的,首先是数据成员。

 private:
  std::vector buffer_;
  size_t readerIndex_;
  size_t writerIndex_;

  static const char kCRLF[];

其实很简单,就是使用vector来模拟buffer,然后有两个指针

readerIndex 代表可读的位置
writeIndex 代表可写位置

然后我们看看两个字段

static const size_t kCheapPrepend = 8;
static const size_t kInitialSize = 1024;

kCheapPrepend 代表头部预留的8个字节,而kInitialSize代表保存数据,最开始为1KB的大小。

然后我们看Buffer的构造函数:

 explicit Buffer(size_t initialSize = kInitialSize)
    : buffer_(kCheapPrepend + initialSize),
      readerIndex_(kCheapPrepend),
      writerIndex_(kCheapPrepend)
  {
    assert(readableBytes() == 0);
    assert(writableBytes() == initialSize);
    assert(prependableBytes() == 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_; }

返回第一个\r\n的位置

注意C++的头文件里 < algorithm > 里的seacrh函数,从从第一个容器里找到第二个容器第一个出现的位置

 const char* findCRLF() const
  {
    // FIXME: replace with memmem()?
    const char* crlf = std::search(peek(), beginWrite(), kCRLF, kCRLF+2);
    return crlf == beginWrite() ? NULL : crlf;
  }

然后就是从Buffer里读走len个字节的数据,代码

void retrieve(size_t len)
  {
    assert(len <= readableBytes());
    if (len < readableBytes())
    {
      readerIndex_ += len;
    }
    else
    {
      retrieveAll();
    }
  }

这个函数的具体意思是:

如果Buffer里可读字节数大于要读的字节,那么直接将readIndex_指针向后移动len个位置,否则若len大于可读的字节数,
那么直接读取完所有的字节。

接下来就是追加数据

 void append(const char* /*restrict*/ data, size_t len)
  {
    ensureWritableBytes(len);
    std::copy(data, data+len, beginWrite());
    hasWritten(len);
  }

我们来看看ensureWritableBytes()函数

void ensureWritableBytes(size_t len)
  {
    if (writableBytes() < len)
    {
      makeSpace(len);
    }
    assert(writableBytes() >= len);
  }

具体逻辑是如果可写字节数小于我们要写的字节数,那么我们就得增加空间。
然后我们就是利用copy函数来追加数据 std::copy(data, data+len, beginWrite()); 最后调用hasWritten(len)来更改writeIndex_的位置,即可写的位置。

然后我们看看当空间不够使,Buffer是怎么扩充数据和进行数据复制的。

void makeSpace(size_t len)
  {
    if (writableBytes() + prependableBytes() < len + kCheapPrepend)
    {
      // FIXME: move readable data
      buffer_.resize(writerIndex_+len); 
    }
    else
    {
      // move readable data to the front, make space inside buffer
      assert(kCheapPrepend < readerIndex_);
      size_t readable = readableBytes();
      std::copy(begin()+readerIndex_,
                begin()+writerIndex_,
                begin()+kCheapPrepend);
      readerIndex_ = kCheapPrepend;
      writerIndex_ = readerIndex_ + readable;
      assert(readable == readableBytes());
    }
  }
if (writableBytes() + prependableBytes() < len + kCheapPrepend) 首先要对这块代码进行解释,什么时候我们需要重新
分配空间呢?什么时候不需要重新分分配空间呢?
标准就是 如果头部剩下的空间和可写空间即 writableBytes() + prependableBytes()  小于 新加的数据长度+预留默认头部
长度8字节的时候,那么必须重新分配空间,如果大于的话,那么发生内部腾挪就行。 然后这个代码和书上的图画的不一样,
它是直接追加了len个字节,其实应该是将readIndex_移动到kCheapPrepend那里去,然后将writeIndex_
向前移动到readindex_ + readBytes()处。 还有要注意的当buffer_.resize(writerIndex_+len)的时候,会把以前的Buffer自动复制到
新的Buffer处。
assert(kCheapPrepend < readerIndex_);
size_t readable = readableBytes(); //得到可读位置
std::copy(begin()+readerIndex_, 
begin()+writerIndex_,
begin()+kCheapPrepend); //将可读取的那些字节移动到从第8字节处
readerIndex_ = kCheapPrepend; //然后将可读位置置为初始头部大小
writerIndex_ = readerIndex_ + readable;  //可写位置也要移动
assert(readable == readableBytes());

那么数据的追加就结束了。

最后就是数据的读取,然后追加到Buffer里面。
看代码

ssize_t Buffer::readFd(int fd, int* savedErrno)
{
  // saved an ioctl()/FIONREAD call to tell how much to read
  char extrabuf[65536];
  struct iovec vec[2];
  const size_t writable = writableBytes();
  vec[0].iov_base = begin()+writerIndex_;
  vec[0].iov_len = writable;
  vec[1].iov_base = extrabuf;
  vec[1].iov_len = sizeof extrabuf;
  // when there is enough space in this buffer, don't read into extrabuf.
  // when extrabuf is used, we read 128k-1 bytes at most.
  const int iovcnt = (writable < sizeof extrabuf) ? 2 : 1;
  const ssize_t n = sockets::readv(fd, vec, iovcnt);
  if (n < 0)
  {
    *savedErrno = errno;
  }
  else if (implicit_cast(n) <= writable)
  {
    writerIndex_ += n;
  }
  else
  {
    writerIndex_ = buffer_.size();
    append(extrabuf, n - writable);
  }
  // if (n == writable + sizeof extrabuf)
  // {
  //   goto line_30;
  // }
  return n;
}

这里有个小技巧,就是巧妙运用了栈的空间,如果读取的数据不多,没有超过Buffer的长度,那么直接读取到Buffer里,如果大于Buffer的可写长度,那么我们就先用栈保存起来, 这样不用把Buffer的长度设置的太大,免得避免浪费,只有在Buffer的长度不够用时,才会进行扩容。然后将栈里数据追加到Buffer里,最后这个局部变量栈空间在函数结束时,就自动释放了。

你可能感兴趣的:(muduo网络库)