/// 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;
如图:
读写游标把vector
prependable = readIndex
readable = writeIndex - readIndex
writable = size() - writeIndex
muduo作者将Buffer前添加了一段8字节的预留空间(称之为prependable)。这样做的好处,比如recv buffer
收到来自网络的消息,并计算它的长度后,可以将这个长度值写入到prependable
区域。output buffer同理。
原因是:如果vector缓冲区不够大,扩容,迭代器会失效。
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);
...
}
对于读 recv buffer,接收客户端消息函数onMessage()始终发生在该TcpConnnection所属那个IO线程,在onMessage()完成对input buffer的操作,且不要把recv buffer暴露给其他线程。这样所有对recv 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函数里,会有压缩策略。