muduo网络库学习(六)缓冲区Buffer及TcpConnection的读写操作

tcp的通信过程中,内核其实为tcp维护着一个缓冲区

  • 当调用write/send时,会向内核缓冲区中写入数据,内核和tcp协议栈负责将缓冲区中的数据发送到指定<ip,port>的目标位置。
  • 当有数据到达内核的tcp缓冲区中,如果开启了对套接字可读事件的监听,那么内核会让套接字变为可读状态,从而从poll函数中返回,调用read/recv进行读操作。

但是,内核维护的tcp缓冲区通常都比较小

  • 如果调用write/send时,内核缓冲区已满,那么阻塞io将会阻塞在io函数上直到内核缓冲区有足够的空间容纳要写入的数据,非阻塞io将会返回错误,通常是EAGAIN/EWOULDBLOCK
  • 如果调用write/send时,内核缓冲区未满,但是不能容纳要写入的字节数,可用空间不足,那么只会写入能写入的那么多字节数,此时,仍然有一些数据没有发送,可是这些数据还非发送不可,就出现缓冲区已满的情况
  • 这就导致要不阻塞当前线程,要不无法正常写入数据,而如果采用判断返回值是否出错的方法,仍然是一直忙循环检测io写入状态,仍然是busy loop,仍然会阻塞当前线程

而且,io多路复用分水平触发和边缘触发两种,当内核tcp缓冲区中一直有数据时

  • 如果是水平触发,那么套接字会一直处于可读状态,io多路复用函数会一直认为这个套接字被激活,也就是说如果第一次触发后没有将tcp缓冲区中的数据全部读出,那么下次进行到poll函数时会立即返回,因为套接字一直是可读的。这会导致了busy loop问题
  • 如果是边缘触发,那么就只会触发一次,即使第一次触发没有将所有数据都读走,下次进行到poll也不会再触发套接字的可读状态,直到下次又有一批数据送至tcp缓冲区中,才会再次触发可读。所以有可能存在漏读数据的问题,万一不会再有数据到来呢,此时tcp缓冲区中仍然有数据,而应用程序却不知道

所以,设计应用层自己的缓冲区是很有必要的,也就是由应用程序来管理缓冲区问题

  • 应用层缓冲区通常很大,也可以初始很小,但可以通过动态调整改变大小(vector
  • 应用层缓冲区需要有读/写两个(缓冲区类只有一个,既可被用作读缓冲区,也可被用作写缓冲区)
  • 当用户想要调用write/send写入数据给对端,如果数据可以全部写入,那么写入就好了。如果写入了部分数据或者根本一点数据都写不进去,此时表明内核缓冲区已满,为了不阻塞当前线程,应用层写缓冲区会接管这些数据,等到内核缓冲区可以写入的时候自动帮用户写入。
  • 当有数据到达内核缓冲区,应用层的读缓冲区会自动将这些数据读到自己那里,当用户调用read/recv想要读取数据时,应用层读缓冲区将已经从内核缓冲区取出的数据返回给用户,实际上就是用户从应用层读缓冲区读取数据
  • 应用层缓冲区对用户而言是隐藏的,用户可能根本不知道有应用层缓冲区的存在,只需读/取数据,而且也不会阻塞当前线程

缓冲区Buffer的设计

muduo应用层缓冲区的设计采用std::vector数据结构,一方面内存是连续的方便管理,另一方面,vector自带的增长模式足以应对动态调整大小的任务
缓冲区Buffer的定义如下,只列出了一些重要部分

注释中写明了缓冲区的设计方法,主要就是利用两个指针readerIndexwriterIndex分别记录着缓冲区中数据的起点和终点,写入数据的时候追加到writeIndex后面,读出数据时从readerIndex开始读。在readerIndex前面预留了几个字节大小的空间,方便日后为数据追加头部信息。缓冲区在使用的过程中会动态调整readerIndexwriterIndex的位置,初始缓冲区为空,readerIndex == writerIndex
缓冲区默认大小为1KB,头部预留空间为8 bytes,如果使用过程中发现缓冲区大小不够,会增加缓冲区大小,方法见readFd函数

/// A buffer class modeled after org.jboss.netty.buffer.ChannelBuffer
///
/// @code
/// +-------------------+------------------+------------------+
/// | prependable bytes |  readable bytes  |  writable bytes  |
/// |                   |     (CONTENT)    |                  |
/// +-------------------+------------------+------------------+
/// |                   |                  |                  |
/// 0      <=      readerIndex   <=   writerIndex    <=     size
/// @endcode

/*
 *        
 *   缓冲区的设计方法,muduo采用vector连续内存作为缓冲区,libevent则是分块内存
 *      1.相比之下,采用vector连续内存更容易管理,同时利用std::vector自带的内存
 *        增长方式,可以减少扩充的次数(capacity和size一般不同)
 *      2.记录缓冲区数据起始位置和结束位置,写入时写到已有数据的后面,读出时从
 *        数据起始位置读出
 *      3.起始/结束位置如上图的readerIndex/writeIndex,其中readerIndex为缓冲区
 *        数据的起始索引下标,writeIndex为结束位置下标。采用下标而不是迭代器的
 *        原因是删除(erase)数据时迭代器可能失效
 *      4.开头部分(readerIndex以前)是预留空间,通常只有几个字节的大小,可以用来
 *        写入数据的长度,解决粘包问题
 *      5.读出和写入数据时会动态调整readerIndex/writeIndex,如果没有数据,二者
 *        相等
 */
class Buffer : public muduo::copyable
{
 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)
  {
    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_; }


  /// Read data directly into buffer.
  ///
  /// It may implement with readv(2)
  /// @return result of read(2), &
  • 9
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值