non-blocking IO的核心思想是避免阻塞在read()或write()或其他IO系统调用上,这样可以最大限度地复用thread-of-control。让一个线程能服务于多个socket连接。IO线程只能阻塞在IO multiplexing函数上,如select/poll/epoll_wait。这样一来,应用层的缓冲是必需的,每个TCP socket都要有stateful的input buffer和output buffer。
Tcpconnection必须要有output buffer ,考虑一个场景,程序想通过TCP连接发送100KB的数据,但是在write调用中,操作系统只接受了80KB,肯定不想在此等待,因为不知道会等多久(取决于对方什么时候接受数据,然后滑动TCP窗口)。程序需要尽快交出控制权,返回event loop。在这种情况下,剩余的20KB怎么办?
对于应用程序而言,它只管生成数据,它不关心到底数据是一次性发送还是分多少次发送的,这些由网络库关心,程序只需要调用TcpConnection::send()就行了,网络库会负责到底。网络库应该接管剩余的20KB数据,把它保存在该TCPconnection的output buffer里,然后注册pollout事件。 当然如果写完了,那么应该停止关注POLLOUT事件,以免造成busy loop,因为muduo采用的是level trigger。
头部保留了8个字节。我们具体来看看Buffer类的实现,一些不是太重要的函数我们就不看了,来看具体的,首先是数据成员。
private:
std::vector buffer_;
size_t readerIndex_;
size_t writerIndex_;
static const char kCRLF[];
其实很简单,就是使用vector来模拟buffer,然后有两个指针
readerIndex 代表可读的位置
writeIndex 代表可写位置
然后我们看看两个字段
static const size_t kCheapPrepend = 8;
static const size_t kInitialSize = 1024;
kCheapPrepend 代表头部预留的8个字节,而kInitialSize代表保存数据,最开始为1KB的大小。
然后我们看Buffer的构造函数:
explicit Buffer(size_t initialSize = kInitialSize)
: buffer_(kCheapPrepend + initialSize),
readerIndex_(kCheapPrepend),
writerIndex_(kCheapPrepend)
{
assert(readableBytes() == 0);
assert(writableBytes() == initialSize);
assert(prependableBytes() == kCheapPrepend);
}
其实就是对各个字段进行确认。
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_; }
注意C++的头文件里 < algorithm > 里的seacrh函数,从从第一个容器里找到第二个容器第一个出现的位置
const char* findCRLF() const
{
// FIXME: replace with memmem()?
const char* crlf = std::search(peek(), beginWrite(), kCRLF, kCRLF+2);
return crlf == beginWrite() ? NULL : crlf;
}
然后就是从Buffer里读走len个字节的数据,代码
void retrieve(size_t len)
{
assert(len <= readableBytes());
if (len < readableBytes())
{
readerIndex_ += len;
}
else
{
retrieveAll();
}
}
这个函数的具体意思是:
如果Buffer里可读字节数大于要读的字节,那么直接将readIndex_指针向后移动len个位置,否则若len大于可读的字节数,
那么直接读取完所有的字节。
void append(const char* /*restrict*/ data, size_t len)
{
ensureWritableBytes(len);
std::copy(data, data+len, beginWrite());
hasWritten(len);
}
我们来看看ensureWritableBytes()函数
void ensureWritableBytes(size_t len)
{
if (writableBytes() < len)
{
makeSpace(len);
}
assert(writableBytes() >= len);
}
具体逻辑是如果可写字节数小于我们要写的字节数,那么我们就得增加空间。
然后我们就是利用copy函数来追加数据 std::copy(data, data+len, beginWrite());
最后调用hasWritten(len)
来更改writeIndex_的位置,即可写的位置。
然后我们看看当空间不够使,Buffer是怎么扩充数据和进行数据复制的。
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());
}
}
if (writableBytes() + prependableBytes() < len + kCheapPrepend) 首先要对这块代码进行解释,什么时候我们需要重新
分配空间呢?什么时候不需要重新分分配空间呢?
标准就是 如果头部剩下的空间和可写空间即 writableBytes() + prependableBytes() 小于 新加的数据长度+预留默认头部
长度8字节的时候,那么必须重新分配空间,如果大于的话,那么发生内部腾挪就行。 然后这个代码和书上的图画的不一样,
它是直接追加了len个字节,其实应该是将readIndex_移动到kCheapPrepend那里去,然后将writeIndex_
向前移动到readindex_ + readBytes()处。 还有要注意的当buffer_.resize(writerIndex_+len)的时候,会把以前的Buffer自动复制到
新的Buffer处。
assert(kCheapPrepend < readerIndex_);
size_t readable = readableBytes(); //得到可读位置
std::copy(begin()+readerIndex_,
begin()+writerIndex_,
begin()+kCheapPrepend); //将可读取的那些字节移动到从第8字节处
readerIndex_ = kCheapPrepend; //然后将可读位置置为初始头部大小
writerIndex_ = readerIndex_ + readable; //可写位置也要移动
assert(readable == readableBytes());
那么数据的追加就结束了。
最后就是数据的读取,然后追加到Buffer里面。
看代码
ssize_t Buffer::readFd(int fd, int* savedErrno)
{
// saved an ioctl()/FIONREAD call to tell how much to read
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);
if (n < 0)
{
*savedErrno = errno;
}
else if (implicit_cast(n) <= writable)
{
writerIndex_ += n;
}
else
{
writerIndex_ = buffer_.size();
append(extrabuf, n - writable);
}
// if (n == writable + sizeof extrabuf)
// {
// goto line_30;
// }
return n;
}
这里有个小技巧,就是巧妙运用了栈的空间,如果读取的数据不多,没有超过Buffer的长度,那么直接读取到Buffer里,如果大于Buffer的可写长度,那么我们就先用栈保存起来, 这样不用把Buffer的长度设置的太大,免得避免浪费,只有在Buffer的长度不够用时,才会进行扩容。然后将栈里数据追加到Buffer里,最后这个局部变量栈空间在函数结束时,就自动释放了。