muduo -- Buffer分析

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

/*
自己设计的可变缓冲区,成员变量vector、readIndex、writeIndex,同时处理粘包问题。Buffer::readFd()中的extraBuffer通过堆上和栈上空间的结合,避免了内存资源的巨额开销。先加入栈空间再扩充和直接扩充的区别就是明确知道多少数据,避免巨大的buffer浪费并且减少read系统调用。

主要就是利用两个指针readerIndex,writerIndex分别记录着缓冲区中数据的起点和终点,写入数据的时候追加到writeIndex后面,读出数据时从readerIndex开始读。在readerIndex前面预留了8字节大小的空间,方便日后为数据追加头部信息。缓冲区在使用的过程中会动态调整readerIndex和writerIndex的位置,初始缓冲区为空,readerIndex == writerIndex
 * */
/*
 *
 *   缓冲区的设计方法,muduo采用vector连续内存作为缓冲区,libevent则是分块内存
 *      1.相比之下,采用vector连续内存更容易管理,同时利用std::vector自带的内存
 *        增长方式,可以减少扩充的次数(capacity和size一般不同)
 *      2.记录缓冲区数据起始位置和结束位置,写入时写到已有数据的后面,读出时从
 *        数据起始位置读出
 *      3.起始/结束位置如上图的readerIndex/writeIndex,其中readerIndex为缓冲区
 *        数据的起始索引下标,writeIndex为结束位置下标。采用下标而不是迭代器的
 *        原因是删除(erase)数据时迭代器可能失效
 *      4.开头部分(readerIndex以前)是预留空间,通常只有8个字节的大小,可以用来
 *        写入数据的长度,解决粘包问题
 *      5.读出和写入数据时会动态调整readerIndex/writeIndex,如果没有数据,二者
 *        相等
 */

static const size_t kCheapPrepend = 8;

static const size_t kInitialSize = 1024;

如图:

muduo -- Buffer分析_第1张图片

muduo -- Buffer分析_第2张图片

读写游标把vector分成几块区域:

prependable = readIndex
readable = writeIndex - readIndex
writable = size() - writeIndex

muduo作者将Buffer前添加了一段8字节的预留空间(称之为prependable)。这样做的好处,比如recv buffer收到来自网络的消息,并计算它的长度后,可以将这个长度值写入到prependable区域。output buffer同理。

一. 不用迭代器

原因是:如果vector缓冲区不够大,扩容,迭代器会失效。

二. 收数据Buffer:

        readFd内部,在栈上准备了一个65536字节的extrabuf,使用readv(接受io数据,iovec有两块,第一块指向Muuod Buffer中的writable字节,另一块指向栈上extrabuf。如果读入的数据不多,那么Buffer可以存储;如果长度超过Buffer的writable字节数,就会读到栈上的extrabuf里,然后程序再把extrabuf里的数据append()到Buffer内。
        通过增加栈上缓冲区,增加muduo吞吐量。利用临时栈上空间,避免每个连接对象初始化时new Buffer过大长度内存,造成内存浪费,也避免了反复调用read()的系统开销(由于接受数据缓冲区较大,一次readv()调用就能读完用户数据)。

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;
    // 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);
...
}

三. 读Buffer是否线程安全:

 对于读 recv buffer,接收客户端消息函数onMessage()始终发生在该TcpConnnection所属那个IO线程,在onMessage()完成对input buffer的操作,且不要把recv buffer暴露给其他线程。这样所有对recv buffer的操作都在同一个线程。

四. 写Buffer是否线程安全:

   如果TcpConnection::send()调用发生在该TcpConnection所属的那个IO线程,那么它会调用TcpConnection::sendInLoop,sendInLoop()会在当前线程(也就是eventloop线程)操作output buffer;

void TcpConnection::send(Buffer* buf)
{
    if (state_ == kConnected)
    {
        if (loop_->isInLoopThread())
...

   如果TcpConnection::send()调用发生在别的线程,他不会在当前线程调用sendInLoop(),而是通过EventLoop::runInLoop()把sendInLoop函数调用转移到IO线程,这样sendInLoop还是会在IO线程操作output buffer,不会有线程安全问题。跨线程的函数转移调用涉及函数参数的跨线程传递,一种简单的做法是把数据拷一份,线程安全,就是性能稍有损失。

void TcpConnection::send(Buffer* buf)
{
    if (state_ == kConnected)
    {
        if (loop_->isInLoopThread())// 在当前io线程调用 send 发送数据
        {
            sendInLoop(buf->peek(), buf->readableBytes());
            buf->retrieveAll();
        }
        else// 在其他线程调用 send 发送数据
        {
            loop_->runInLoop(
                        boost::bind(&TcpConnection::sendInLoop,
                                    this,     // FIXME
                                    buf->retrieveAllAsString()));
            //std::forward(message)));
        }
    }
}

五. 压缩:

初始readerIndex==writerIndex==8, 没有任何数据写入。

一旦开始要写入数据的话,那么writerIndex+=size(要写入的字节数)这个buffer会动态地增长。

readerIndex标记的是我们可以读的游标,如果readerIndex==writerIndex就表示所有写入数据都已经被上层应用读完。

这个Buffer并不是无限增长,在makeSpace函数里,会有压缩策略。

 

你可能感兴趣的:(muduo)