muduo源码分析之Buffer缓冲区

相关文件

muduo/net/Buffer.h
muduo/net/Buffer.cc

功能

为了让一个线程能服务于多个socket连接,IO线程只能阻塞在IO多路复用函数(如epoll_wait/poll),所以read/write等IO系统调用需设置为非阻塞non-blocking。因此每个TCP的socket连接都要有输入/输出缓冲区。
输入缓冲区input buffer: Tcpconnection从socket读数据,然后写入输入缓冲区,用户代码从输入缓冲区读取数据。
输出缓冲区output buffer: 用户把数据写入output buffer。TcpConnection从输出缓冲区读取数据并写入socket。

使用

每个TcpConnection有一个输入缓冲区和一个输出缓冲区。
用户代码不会直接操作Buffer,只需要调用发送和取数据的接口函数,read/write由muduo库完成。

这里只放用户代码:

#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
#include <muduo/net/InetAddress.h>

#include <boost/bind.hpp>

#include <stdio.h>

using namespace muduo;
using namespace muduo::net;

class TestServer
{
 public:
  TestServer(EventLoop* loop,
             const InetAddress& listenAddr)
    : loop_(loop),
      server_(loop, listenAddr, "TestServer")
  {
    server_.setConnectionCallback(
        boost::bind(&TestServer::onConnection, this, _1));
    server_.setMessageCallback(
        boost::bind(&TestServer::onMessage, this, _1, _2, _3));
  }

  void start()
  {
	  server_.start();
  }

 private:
  void onConnection(const TcpConnectionPtr& conn)
  {
    if (conn->connected())
    {
      printf("onConnection(): new connection [%s] from %s\n",
             conn->name().c_str(),
             conn->peerAddress().toIpPort().c_str());
    }
    else
    {
      printf("onConnection(): connection [%s] is down\n",
             conn->name().c_str());
    }
  }

  //消息到来回调函数
  void onMessage(const TcpConnectionPtr& conn,
                 Buffer* buf,
                 Timestamp receiveTime)
  {
    string msg(buf->retrieveAllAsString());//从缓冲区取数据
    printf("onMessage(): received %zd bytes from connection [%s] at %s\n",
           msg.size(),
           conn->name().c_str(),
           receiveTime.toFormattedString().c_str());
    conn->send(msg);//发送
  }

  EventLoop* loop_;
  TcpServer server_;  //包含TcpServer,基于对象编程
};


int main()
{
  printf("main(): pid = %d\n", getpid());

  InetAddress listenAddr(8888);
  EventLoop loop;

  TestServer server(&loop, listenAddr);
  server.start();

  loop.loop();
}

主要看onMessage()回调函数,使用buf->retrieveAllAsString()从缓冲区取数据,使用 conn->send(msg)发送数据。

实际当可读事件触发,先调用TcpConnection::handleRead(),其中调用inputBuffer_.readFd()和onMessage()回调函数。

当用户代码调用conn->send(msg),如果输出缓冲区没有数据,则直接write(msg),否则将msg添加到输出缓冲区,监听可写事件。当可写事件触发,调用TcpConnection::handleWrite()发送outputBuffer_中的数据,最后调用writeCompleteCallback_回调函数。

Buffer源码分析

Buffer类

Buffer内部以vector作为数据容器。
image
readIndex和writerIndex两个下标将数组分为三块
image
Buffer就像一个队列queue,从头部读数据,从尾部写数据。
prependable部分是头部预留字节,可方便地在头部追加数据长度等信息。
readable部分数有效数据部分,起始时readIndex等于writerIndex,有效载荷为空。
writable是空白可写部分。

添加数据

  1. readFd()接收数据。
    read()是系统调用,需要减少系统调用的次数,每次调用读的数据越多越划算。这样初始缓冲区越大越好,但往往使用率很低,这样又会浪费内存。
    muduo中使用站上空间划分了第二块缓冲区。体做法是,在栈上准备一个65536字节的extrabuf,然后利用readv()来读取数据,iovec有两块,第一块指向muduo Buffer中的writable
    字节,另一块指向栈上的extrabuf。这样如果读入的数据不多,那么全部都读到Buffer中去了;如果长度超过Buffer的writable字节数,就会读到栈上的extrabuf里,然后程序再把extrabuf里的数据append()到Buffer中。

这么做利用了临时栈上空间 ,避免每个连接的初始Buffer过大造成的内存浪费,也避免反复调用read()的系统开销(由于缓冲区足够大,通常一次readv()系统调用就能读完全部数据)。

  1. 调用append()往缓冲区添加数据。
    先检查剩余可写空间是否足够。
    不够的话,有两种方式,一是使用vector的resize()重新分配空间,一种是将readable部分往前挪(因为经过多次读写,readIndex可能到了比较靠后的位置),增大可写空间,不需要重新分配空间。
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<size_t>(n) <= writable)
  {
    writerIndex_ += n;
  }
  else//当前缓冲区不够容纳,数据被接收到了第二块缓冲区extrabuf,将其append至buffer
  {
    writerIndex_ = buffer_.size();
    append(extrabuf, n - writable);
  }
  // if (n == writable + sizeof extrabuf)
  // {
  //   goto line_30;
  // }
  return n;
}


  void append(const char* /*restrict*/ data, size_t len)
  {
    ensureWritableBytes(len);  //确保剩余空间足够
    std::copy(data, data+len, beginWrite());// 复制到buffer,beginWrite()返回writeIndex所在指针
    hasWritten(len); //更新writeIndex位置  +len
  }


 void ensureWritableBytes(size_t len)
  {
    if (writableBytes() < len)
    {
      makeSpace(len); //空间不足,腾空间
    }
    assert(writableBytes() >= 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即回到初始位置
      readerIndex_ = kCheapPrepend;
      writerIndex_ = readerIndex_ + readable;
      assert(readable == readableBytes());
    }
  }

取数据

上面的用户代码中,使用retrieveAllAsString()将缓冲区的数据作为string全部取出。

  string retrieveAllAsString()
  {
    return retrieveAsString(readableBytes());
  }

  string retrieveAsString(size_t len)
  {
    assert(len <= readableBytes());
    string result(peek(), len); //peek()返回readerIndex所在指针。
    retrieve(len);
    return result;
  }

  const char* peek() const
  { return begin() + readerIndex_; }

  void retrieve(size_t len)
  {
    assert(len <= readableBytes());
    if (len < readableBytes())
    {
      readerIndex_ += len; //更新readInder位置
    }
    else
    {
      retrieveAll();//重置到起始位置
    }
  }

   void retrieveAll()
  {
    readerIndex_ = kCheapPrepend;
    writerIndex_ = kCheapPrepend;
  }
posted @ 2021-05-30 14:15  零十  阅读(315)  评论(0编辑  收藏  举报