muduo的I/O模型采用非阻塞模式,避免阻塞在read()或write()或其他系统调用上。
TcpConnection必须要有output buffer 考虑一个常见场景:程序想通过TCP连接返送100k字节的数据,但在write()调用中,操作系统只接受了80k字节(受TCP advertised window的控制,细节见TCPv1),你肯定不想在原地等待,因为不知道会等待多久(取决于对方什么时候接收数据,然后滑动TCP窗口)。程序应该尽快交出控制权,返回eventloop。在这种情况下,剩余的20k字节数据怎么办?
对,答案就是网络库来接管。网络库把它保存在output buffer里,然后注册POLLOUT事件,一旦socket变得可写就立刻发送给数据。当然,这第二次write()也不一定能完全写入20字节,那就继续关注POLLOUT事件。如果写完,停止关注POLLOUT,防止busy loop。(因为epoll采用level trigger)。
如果output buffer里还有待发送的数据,而程序又想关闭连接(对程序而言,调用TcpConncetion之后就认为数据迟早会发送出去),这是不能立刻关闭连接,这就是muduo库的shutdown()没有立即关闭连接的原因。以后博客会写。
TcpConnection必须要有input buffer TCP是一个无边界的字节流协议,接收方必须要处理“收到的数据尚不构成一条完整的消息”和“一次收到两条消息的数据”等等情况。一个常见的场景是,发送方send了两条10k字节的消息(共20k),接收方收到数据的情况可能是:
class Buffer : public muduo::copyable
{
public:
static const size_t kCheapPrepend = 8; //默认预留8个字节
static const size_t kInitialSize = 1024; //初始大小
private:
std::vector buffer_; //vector用于替代固定数组
size_t readerIndex_; //读位置
size_t writerIndex_; //写位置
//const char Buffer::kCRLF[] = "\r\n";
static const char kCRLF[]; //'\r\n',使用柔性数组
};
mduo库Buffer设计的思路除了Buffer前部设计了预留(Prepend)的部分,其他都和libevent的buffer设计思路是一样的。
class Buffer : public muduo::copyable
{
public:
static const size_t kCheapPrepend = 8; //默认预留8个字节
static const size_t kInitialSize = 1024; //初始大小
explicit Buffer(size_t initialSize = kInitialSize)
: buffer_(kCheapPrepend + initialSize), //总大小为1032
readerIndex_(kCheapPrepend), //初始指向8
writerIndex_(kCheapPrepend) //初始指向8
{
assert(readableBytes() == 0);
assert(writableBytes() == initialSize);
assert(prependableBytes() == kCheapPrepend);
}
初始化了Buffer的大小,和Prepend部分的大小。
//可读大小
size_t readableBytes() const
{ return writerIndex_ - readerIndex_; }
//可写大小
size_t writableBytes() const
{ return buffer_.size() - writerIndex_; }
//预留大小
size_t prependableBytes() const
{ return readerIndex_; }
//读的下标
const char* peek() const
{ return begin() + readerIndex_; }
//结合栈上空间,避免内存使用过大,提高内存使用率
//如果有10K个连接,每个连接就分配64K缓冲区的话,将占用640M内存
//而大多数时候,这些缓冲区的使用率很低
ssize_t Buffer::readFd(int fd, int* savedErrno)
{
// saved an ioctl()/FIONREAD call to tell how much to read
//节省一次ioctl系统调用(获取当前有多少可读数据)
//为什么这么说?因为我们准备了足够大的extrabuf,那么我们就不需要使用ioctl取查看fd有多少可读字节数了
char extrabuf[65536];
//使用iovec分配两个连续的缓冲区
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; //writeable一般小于65536
const ssize_t n = sockets::readv(fd, vec, iovcnt); //iovcnt=2
if (n < 0)
{
*savedErrno = errno;
}
else if (implicit_cast(n) <= writable) //第一块缓冲区足够容纳
{
writerIndex_ += n; //直接加n
}
else //当前缓冲区,不够容纳,因而数据被接受到了第二块缓冲区extrabuf,将其append至buffer
{
writerIndex_ = buffer_.size(); //先更显当前writerIndex
append(extrabuf, n - writable); //然后追加剩余的再进入buffer当中
}
return n;
}
没错,这个函数是用来从套接字向缓冲区读取数据的。由于该操作在网络程序中执行的很频繁,为了避免每次使用系用调用ioctl查看可读字节数,所以muduo在这里使用了一个65536个字节足够大的缓冲区。但是这个缓冲区怎么用呢?要知道怎么用我们先来看一下iovec结构体:
#include
struct iovec {
ptr_t iov_base; /* Starting address */
size_t iov_len; /* Length in bytes */
};
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
#include
ssize_t readv(int fildes, const struct iovec *iov, int iovcnt);
ssize_t writev(int fildes, const struct iovec *iov, int iovcnt);
struct iovec {
void *iov_base /* 数据区的起始地址 */
size_t iov_len /* 数据区的大小 */
}
参数iovcnt指出数组iov的元素个数,元素个数至多不超过IOV_MAX。Linux中定义IOV_MAX的值为1024。
void append(const char* /*restrict*/ data, size_t len)
{
ensureWritableBytes(len); //确保缓冲区可写空间大于等于len,如果不足,需要扩充
std::copy(data, data+len, beginWrite()); //追加数据
hasWritten(len); //内部仅仅是写入后调整writeindex
}
void ensureWritableBytes(size_t len)
{
if (writableBytes() < len) //如果可写数据小于len
{
makeSpace(len); //增加空间
}
assert(writableBytes() >= len);
}
空间不够实际调用扩充空间函数makeSpace():
void makeSpace(size_t len) //vector增加空间
{
if (writableBytes() + prependableBytes() < len + kCheapPrepend) //确保空间是真的不够,而不是挪动就可以腾出空间
{
// FIXME: move readable data
buffer_.resize(writerIndex_+len);
}
else
{
//内部腾挪就足够append,那么就内部腾挪一下。
// move readable data to the front, make space inside buffer
assert(kCheapPrepend < readerIndex_);
size_t readable = readableBytes();
std::copy(begin()+readerIndex_, //原来的可读部分全部copy到Prepend位置,相当于向前挪动,为writeable留出空间
begin()+writerIndex_,
begin()+kCheapPrepend);
readerIndex_ = kCheapPrepend; //更新下标
writerIndex_ = readerIndex_ + readable;
assert(readable == readableBytes());
}
}
//收缩空间,保留reserver个字节,可能多次读写后buffer太大了,可以收缩
void shrink(size_t reserve)
{
// FIXME: use vector::shrink_to_fit() in C++ 11 if possible.
//为什么要使用Buffer类型的other来收缩空间呢?如果不用这种方式,我们可选的有使用resize(),把我们在resize()z和
Buffer other; //生成临时对像,保存readable内容,然后和自身交换,该临时对象再析构掉
//ensureWritableBytes()函数有两个功能,一个是空间不够resize空间,一个是空间足够内部腾挪,这里明显用的是后者。
other.ensureWritableBytes(readableBytes()+reserve); //确保有足够的空间,内部此时已经腾挪
other.append(toStringPiece()); //把当前数据先追加到other里面,然后再交换。
swap(other); //然后再交换
}
void swap(Buffer& rhs)
{
buffer_.swap(rhs.buffer_);
std::swap(readerIndex_, rhs.readerIndex_);
std::swap(writerIndex_, rhs.writerIndex_);
}
所以,现在缓冲区大小就缩小了,且readable数据并未受到影响。