1.使用应用层buffer的原因
这个陈硕对应的书里面讲地很清楚,见书P205 7.4.2
分为两点
①为什么需要output buffer
②为什么需要input buffer
2.Class Buffer
class Buffer : public muduo::copyable
{
/*******Function Member***********/
//general use
char* peek();
char* beginWrite();
//clear the buffer
int32_t readInt();
int32_t peekInt();
void retrieve(size_t len);
//fill in the buffer
ssize_t readFd(int fd, int* savedErrno);
void append(const char* data, size_t len);
void hasWritten(size_t len);
//prepend the buffer
void prepend(const void* data, size_t len);
/********Data Member************/
std::vector<char> buffer;
size_t readerIndex;
size_t writerIndex;
};
3.具体分析
(1)数据成员
①buffer
采用vertor<char>作为整个Buffer的底层实现
好处是可以利用vector的一些特性进行一些方便的resize操作等等
②XXX Index
这个就是用来指示用的。
看完书中对应的7.4.4与7.4.5中的图,很好理解。
(2)通用函数
①peek
取名类似于c文件操作里面的peek,那里面是返回当前下标的位置。
这里Buffer::peek()返回的是内存中eadable段开始的位置。
因为readerIndex是一个相对地址(相对于buffer的首地址),所以peek就是通过简单返回buffer.begin()+readerIndex实现的。
②beginWrite
与peek相对,返回的是buffer.begin()+writerIndex
(3)从缓冲区取出数据
①peekInt
memcpy取走数据
②retrieve
移动下标
③readInt
调用①peekInt和②retrieve
(4)向缓冲区写入数据
①hasWriten
移动下标
②append
先调用std::copy复制数据
再调用①hasWriten
③readFd
为了解决7.4.3 P208提到的问题,并且从socket fd读数据
在append外面又包装了一层。
(5)前向写入
为了解决消息长度开始位置问题,以很低的代价在前面添加几个字节
它自己就完成了A。移动小标 B。用std::copy向里面写内容
4.缓冲区大小设置问题
在P208页提到了,我们既希望有一个大的缓冲区(一次处理的数据更多,更少地内存分配与转移),
又希望缓冲区足够小(这样即使一个线程有许多并发连接也不会暂用太多内存)
muduo库通过Buffer::readFd函数试图解决这个问题。
(1)基本思路
初始化时,每个connection的缓冲区为1K大小,
readFd中会有一个栈内存的64K大小的extrabuf,
每次通过readv从connfd那边读取数据,先放入buffer之中,多出来的再放入extrabuf之中。
n = sockets::readv(connfd, vec, invcnt);
①n<buffer大小,直接更新buffer的writerIndex即可
②n>buffer大小,多出的部分在extrabuf里面,这个时候就调用append,将多出的部分放入buffer,buffer也实现了扩容。
(2)append函数
①ensureWritableBytes
首先验证增加的长度vector buffer能不能装下,如果不能就调用vector::resize到能
②std::copy
将数据复制到buffer中
③hasWritten
改变WriterIndex
(3)Buffer::readFd的好处
使用这个函数,①保证了大部分connection都只是有一个很小的buffer空间
②但是每次读取,有一个extrabuf在那里保证了几乎每次只需要一次read就能将connfd上来的数据读完。
而这个extrabuf相当于是该线程所有connection共用的,所以开销很小
③每次如果buffer空间不够几乎是多处了多少,进行那么多大小的扩容
且扩容后不再减少,以应对后面需要,减少内存分配次数。
(4)为什么extrabuf是threadlocal
因为在每个thread中是一个loop,loop里面采用IO Multiplexing,这样其实是每次最多有一个connection在使用extrabuf,线程安全。
如果是多个线程使用一个extrabuf,存在并发可能,线程不安全,需要加锁。
但是这样带来了编码难度的加大,带来的提升确并不是很多,所以不值得采用。还不如一个thread一个extrabuf。
(5)编码细节
①iovcnt
位于Buffer::readFd中,
const int iovcnt = (writable < sizeof extrabuf) ? 2 : 1;
这里,iovcnt指示了readv用几块地方存放数据,
这句话保证了,一次读取的最大大小为writable = 64K - 1 < extrabuf, iovcnt = 2, 这个时候一次可以读取sizeof(writable + extrabuf) = 128K -1