1. 为什么需要Buffer类?

<aside> 💡 从发送数据来说,项目使用非阻塞I/O模型,一次发送数据不一定会发送完(因为内核写缓冲区大小是有限的),没发送完的数据需要一个容器进行接收,因此要实现应用层写缓冲区。另一方面,从读数据来说,接收方必须要处理“收到的数据尚不构成一条完整的消息”和“一次收到两条消息的数据”等情况。因此这些数据应该先暂存在某个地方,以保证收到了一条完整的消息。

</aside>

2. Buffer的设计

Untitled

prependable域可以用于存消息的长度,这可以用于解决TCP的粘包问题;readable域表示接收缓冲区,writable域表示发送缓冲区。

3. 如何合理地设计缓冲区的大小?

一方面,我们希望减少系统调用,一次读的数据越多越划算,那么似乎应该准备一个大的缓冲区。但另一方面我们又希望减少内存占用。如果有上千万个并发连接,这样读写缓冲区占用的内存空间是巨大的。

ssize_t Buffer::ReadFd(int fd, int *Errno) {
    char buff[65535];
    /*iovec结构体用于描述一个数据缓冲区。它通常与readv和writev
    系统调用一起使用,用于在一次系统调用中读取或写入多个缓冲区*/
    struct iovec iov[2];
    const size_t writableBytes = WritableBytes();
    /*分散读*/
    iov[0].iov_base = BeginPtr() + m_writeIdx;
    iov[0].iov_len = writableBytes;
    iov[1].iov_base = buff;
    iov[1].iov_len = sizeof(buff);
    /*fd是文件描述符,iov是iovec结构体数组,iovcnt是数组元素个数*/
    const ssize_t len = readv(fd, iov, 2);
    if (len < 0) {
        *Errno = errno;
    } else if (static_cast<size_t>(len) <= writableBytes) {
        m_writeIdx += len;
    } else {
        m_writeIdx = m_buffer.size();
        Append(buff, len - writableBytes);
    }
    return len;
}

readv结合栈上空间通过在栈上准备一个65536字节的extrabuf。如果数据不多,数据都写到Buffer中。如果数据长度超过Buffer的writable域的大小,就会将数据读到extrabuf中,然后程序再把extrabuf中的数据append到Buffer中。这样既避免了初始Buffer过大时造成的内存浪费,也避免了反复调用read造成的系统开销。

4. 缓冲区扩容

void Buffer::MakeSpace(size_t len) {
    if (WritableBytes() + PrependableBytes() < len) {
        /*如果prepend区和writable区的总空间也不足够,则需要为缓冲区
    resize空间,以实现自动增长。Buffer的空间增长之后,不会再回缩*/
        m_buffer.resize(m_writeIdx + len - 1);
    } else {
        /*如果prepend区和writable区的总空间足够写入数据,
        则可先将readable区的数据腾挪到前面,以腾出空间*/
        size_t readSize = ReadableBytes();
        std::copy(BeginPtr() + m_readIdx, BeginPtr() + m_writeIdx, BeginPtr());
        m_readIdx = 0;
        m_writeIdx = m_readIdx + readSize;
        assert(readSize == ReadableBytes());
    }
}

5. Buffer的线程安全性

Buffer并没有设计为线程安全的,这是因为muduo网络库会保证数据的接收和发送动作都会在Buffer对应的IO线程中进行,代码中会用EventLoop::assertLoopThread()保证以上承诺站