muduo中的buffer

muduo在应用层上加了一个缓存机制,用户发送接受数据只需要与buffer打交道就好,底层的网络通信由buffer来实现。

为什么non-blocking网络编程中应用层buffer是必须的

non-blocking IO的核心思想就是避免阻塞在read()或write()或其他的IO系统调用上,这样可以最大限度的复用thread-of-control,让一个线程能服务于多个sockets链接。IO线程只能阻塞在IO multiplexing函数上,如select/poll/epoll_wait.这样一来,应用层的缓存是必须的。每一个TCP socket都要有stateful的input buffer和output buffer。

TCPConnection必须要有output buffer

考虑一个常见的场景:程序想要通过TCP联系发送100KB的数据,但是write()调用中,操作系统只接受80KB的,此时如果直接让应用程序发送的话,那么剩下的20KB数据就发不出去,应用程序可以等到发送缓冲区有空间的时候继续发送,但是不知道什么时候有空间,此时将会阻塞,如果不等待的话,应用程序需要记录剩下的20KB,等到下一次发,然后继续循环上面的操作。但是应用程序只是负责生产数据,对于数据什么时候发送,以怎样的大小发送,应用程序并不关心,这些事情应该都由网络库来实现,所以需要有output buffer。

TcpConnection必须要有input buffer

TCP是一个无边界的字节流协议,接受方必须要处理“收到的数据尚不构成一条完整的消息”和“一次性收到两条消息的数据”等情况。网络库在处理socket可读事件的时候,必须一次性吧socket里的数据读完,否则会反复触发POLLIN事件,造成busy-loop。那么网络库就需要应对数据不完整的情况,需要将收到的数据放在input buffer中,等构成一条完整的消息后,在通知应用程序。也就是应用层的消息分包

Buffer的类图

buffer内部数据存储结构为vector,支持类似于队列的先进先出操作。具体的类图如下:

Buffer::readFD()

在非阻塞网络编程中,对于缓冲去大小的设定需要根据不同应用的场景来决定。为了减少系统调用,一次性读的数据越多约好,但是需要一个比较大的缓冲区。另一方面,为了减少系统的内存占用,应该将缓冲区设的小一点比较好,不然的话,如果一个连接的缓冲区大小为1M,那么1000个连接就需要21G的内存,但是在大多数情况下,buffer的使用率很低,此时大的buffer会造成内存的浪费。

在muduo里面,readFD函数的实现为:在栈上准备一个65536字节的extrabuf,然后利用readv()来读取数据,iovec有两块,一个指向input buffer的writable字节,一个指向extrabuf。如果读入的数据很多,那么可以放在extrabuf里面,然后程序再把extrabuf里面的数据append到buffer中,如果读取数据比较少,那么数据直接存在buffer里面,不需要额外的操作。

buffer数据结构

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

数据结构的初始化如下:

在数据的读取过程中,readIndex和writeIndex随着读取操作的进行而改变。
readIndex和writeIndex随着读取的进行而改变

自动增长和内部腾挪

当当前的可写空间不够的时候,那么需要分配一个更大的内存,在分配好内存进行数据的复制时,顺便进行碎片回收,将所有数据负载到新内存的kCheapPrependable个字节后面。由于vector重新分配了内存,原有的指向其元素的指针会失效,所以buffer中采用的是readIndex和writeIndex来表示可读和可写的其实下标。

当前的内存状态

写入1000字节后,内存的状态

具体的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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());
}
}

前方添加

prependable的作用就是实现高效率的前方添加。预留一定的小空间,但需要在数据的前面添加数据时,就可以直接的添加了,否则的话,需要对整个buffer进行操作。
例如:程序以固定的4字节表示数据的长度,但需要序列化一个消息的时候,不知道该消息的长度,那么可以一直append直到序列化完成为止,然后就可以在前面添加最终的长度。