<aside> 💡 从发送数据来说,项目使用非阻塞I/O模型,一次发送数据不一定会发送完(因为内核写缓冲区大小是有限的),没发送完的数据需要一个容器进行接收,因此要实现应用层写缓冲区。另一方面,从读数据来说,接收方必须要处理“收到的数据尚不构成一条完整的消息”和“一次收到两条消息的数据”等情况。因此这些数据应该先暂存在某个地方,以保证收到了一条完整的消息。
</aside>
prependable域可以用于存消息的长度,这可以用于解决TCP的粘包问题;readable域表示接收缓冲区,writable域表示发送缓冲区。
一方面,我们希望减少系统调用,一次读的数据越多越划算,那么似乎应该准备一个大的缓冲区。但另一方面我们又希望减少内存占用。如果有上千万个并发连接,这样读写缓冲区占用的内存空间是巨大的。
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造成的系统开销。
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());
}
}
Buffer并没有设计为线程安全的,这是因为muduo网络库会保证数据的接收和发送动作都会在Buffer对应的IO线程中进行,代码中会用EventLoop::assertLoopThread()保证以上承诺站