我对muduo c++网络库的理解(三) - 木东驿站 - Powered by MoodBlog

CONTENT

我对muduo c++网络库的理解(三)

缓存设计

muduo中的具体事件处理器并不直接对套接字进行读写,而是通过缓存来进行数据的读写。这个缓存是一个非固定大小的vector<char>。使用readIndex,writeIndex对缓存区域进行标识。整个缓存区域是这个样子的:

0_1303014373FZQM.gif


prependable的区域就是提前准备好一块空间,这样我们可以方便的在待发送的数据前面增加一些数据(比方说数据长度),如果没有prependable区域,我们就得整体挪移数据腾出空间了。

可读空间大小 = writeIndex - readIndex

可写空间大小 = size() - writebleIndex

预准备空间大小 = readIndex

读缓存数据

muduo提供了对不同整型的读取方法,以读取64位整数为例:

int64_t readInt64()
{
    int64_t result = peekInt64();
    retrieveInt64();
    return result;
}
  int64_t peekInt64() const
  {
    assert(readableBytes() >= sizeof(int64_t));
    int64_t be64 = 0;
    ::memcpy(&be64, peek(), sizeof be64);
    return sockets::networkToHost64(be64);
  }
  
   void retrieveInt64()
  {
    retrieve(sizeof(int64_t));
  }
  
   void retrieve(size_t len)
  {
    assert(len <= readableBytes());
    if (len < readableBytes())
    {
      readerIndex_ += len;
    }
    else
    {
      retrieveAll();
    }
  }

第一行调用的方法是peekInt64(),很明显这个方法是从可读区域读取一个64位整数返回。(注意返回时进行了字节序转换,从网络字节序转换为主机字节序)这时候readIndex就应该变化了,使用retrieveInt64进行更新。

写缓存数据

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

(1)确保缓存拥有足够的空间存储新的数据

(2)拷贝数据到可写位置

(3)更新writeIndex位置

当剩余可写空间小于写入数据时,系统要对缓存进行扩容,执行了makeSpace这个方法,参数len是要写入数据的长度。

  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());
    }
  }

如果可写长度和预备空间长度小于数据长度加上kCheapPrepend(也就是说预备长度最低为kCheapPrepend),就使用vector::resize分配刚好足够使用新的空间。如果预备空间和可写长度够用呢,就无需分配新空间了,重利用旧空间即可,把可读空间前移到kCheapPrepend处,然后修改readIndex和writerIndex。

数据前面添加字节

有时候我们想在可读数据的前面加一些数据,这时prepend就起作用了。

void prependInt64(int64_t x)
{
    int64_t be64 = sockets::hostToNetwork64(x);
    prepend(&be64, sizeof be64);
}
void prepend(const void* /*restrict*/ data, size_t len)
{
    assert(len <= prependableBytes());
    readerIndex_ -= len;
    const char* d = static_cast<const char*>(data);
    std::copy(d, d+len, begin()+readerIndex_);
}

(1)先把数字从主机字节序转换为网络字节序

(2)前移readIndex,前移的大小为我们添加数据类型的字节数。

(3)拷贝数据到目标处。

由此看来,我们在数据头添加内容时,无需整体后移数据,只需要改变readIndex,利用prepend区域就能写入头部数据。

接收数据

接收数据到缓存比较麻烦,muduo采用了分散读。

ssize_t Buffer::readFd(int fd, int* savedErrno)
{
  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;
  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<size_t>(n) <= writable)
  {
    writerIndex_ += n;
  }
  else
  {
    writerIndex_ = buffer_.size();
    append(extrabuf, n - writable);
  }
  return n;
}

这两块存储区分别是临时的栈存储区和buffer的可写区。如果可写空间能够完全接收数据(即n<=writable),就无需后续操作了。反之,需要把放不下的数据(此时在extrabuf里)append到缓存,此时一定会转移可读数据或者进行扩容。之所以要这么设定,我觉得作者是想减少一次系统调用,避免该线程在系统调用上占用太多时间。

个快快 2018年11月14日 天气 晴

REMARKS

© 2018 MoodBlog 0.2 个快快 作品 | 参考主题: mathilda by fuzzz. | 鲁ICP备16047814号