应用层Buffer缓冲区及其重要性
event loop 是 non-blocking 网络编程的核心,在现实生活中,non-blocking 几乎总是和 IO-multiplexing 一起使用,原因有两点:
- 没有人真的会用轮询 (busy-pooling) 来检查某个 non-blocking IO 操作是否完成,这样太浪费 CPU资源了。
- IO-multiplex 一般不能和 blocking IO 用在一起,因为 blocking IO read()/write()/accept()/connect() 都有可能阻塞当前线程,这样线程就没办法处理其他 socket上的 IO 事件了。
所以,当我们提到 non-blocking 的时候,实际上指的是 non-blocking + IO-multiplexing,单用其中任何一个都没有办法很好的实现功能。
在non-blocking网络编程中,应用层buffer是必须的。
non-blocking IO的核心思想是避免阻塞在read()或write()或者其他IO系统调用上,这样可以最大限度地复用thread-of-control,让一个线程能服务于多个socket连接,IO线程只能阻塞在IO multiplexing函数上,如select/poll/epoll_wait。
这样一来,应用层的缓冲是必须的,每个TCP socket都要有stateful的input buffer和output buffer。
output buffer的必要性
考虑一个场景:程序想通过TCP连接发送100KB的数据,但是在write()调用中,操作系统只接受了80KB(受TCP advertised window的控制),此时肯定不想原地等待,因为不知道会等多久(取决于对方什么时候接收数据,然后滑动TCP窗口)。程序应该金珂交出程序控制权,返回event loop。在这种情况下,剩余的20KB数据怎么办?
对于应用程序而言,它只管生成数据,它不应该关心到底数据是一次性发送还是分成几次发送,这些应该由网络库来操心,程序只要调用TcpConnection: :send()就行了,网络库会负责到底。网络库应该接管这剩余的20kB数据,把它保存在该TCPconnection的output buffer里,然后注册POLLOUT事件,一旦socket变得可写就立刻发送数据。当然,这第二次write()也不一定能完全写人20kB,如果还有剩余,网络库应该继续关注POLLOUT事件;如果写完了20kB,网络库应该停止关注POLLOUT,以免造成busy loop。
如果程序又写人了50kB,而这时候output buffer里还有待发送的20kB数据,那么网络库不应该直接调用write(),而应该把这50kB数据append在那20kB数据之后,等socket变得可写的时候再一并写人。
如果outputbuffer里还有待发送的数据,而程序又想关闭连接(对程序而言,调用TcpConnection: :send()之后他就认为数据迟早会发出去),那么这时候网络库不能立刻关闭连接,而要等数据发送完毕。
input buffer的必要性
TCP是一个无边界的字节流协议,接收方必须要处理“收到的数据尚不构成一条完整的消息”和“一次收到两条消息的数据"等情况。一个常见的场景是,发送方send()了两条1kB的消息(共2kB),接收方收到数据的情况可能是:
- 一次性收到2kB数据;
- 分两次收到,第一次600B, 第二次1400B;
- 分两次收到,第一次1400B, 第二次600B;
- 分两次收到,第一次1kB,第二次1kB;
- 分三次收到,第一次600B, 第二次800B,第三次600B;
- 其他任何可能。一般而言,长度为n字节的消息分块到达的可能性有2^n-1种。
网络库在处理"socket 可读”事件的时候,必须一次性把socket 里的数据读完(从操作系统buffer搬到应用层buffer),否则会反复触发POLLIN事件,造成busy-loop。那么网络库必然要应对“数据不完整”的情况,收到的数据先放到input buffer里,等构成一条完整的消息再通知程序的业务逻辑。
muduo的应用层缓冲区Buffer
Buffer的内部是一个 std::vector,它是一块连续的内存。此外,Buffer有两个data member,指向该vector中的元素。这两个index的类型是int,不是char*,目的是应对迭代器失效。
muduo Buffer的数据结构如下图(摘自《Linux多线程服务端编程:使用muduoC++网络库》):
两个index把vector的内容分为三块: prependable, readable、 writable, 各块的大小:
- prependable = readIndex
- readable = writeIndex - readIndex
- writable = size() - writeIndex
- 0 ≤ readIndex ≤ writeIndex≤size()
muduo Buffer里有两个常数kCheapPrepend和kInitialSize,定义了prepend-able的初始大小和writable的初始大小,readable 的初始大小为0。在初始化之后,Buffer的数据结构如下图:
其中括号里的数字是该变量或常量的值。
重写Buffer.h:
#pragma once
#include <vector>
#include <string>
#include <algorithm>
class Buffer
{
public:
static const size_t KCheapPrepend = 8;
static const size_t KInitialSize = 1024;
explicit Buffer(size_t initialSize = KInitialSize)
:buffer_(KCheapPrepend + initialSize)
,readerIndex_(KCheapPrepend)
,writerIndex_(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_;
}
//onMessage string <- Buffer
void retrieve(size_t len)
{
if(len < readableBytes())
{
readerIndex_ += len; //应用只读取了刻度缓冲区数据的一部分,就是len,还剩下的readerIndex_+=len-》writerIndex_
}
else
{
retrieveAll();
}
}
void retrieveAll()
{
readerIndex_ = writerIndex_ = KCheapPrepend;
}
//把onMessage函数上报的Buffer数据,转成string类型的数据返回
std::string retrieveAllAsString()
{
return retrieveAsString(readableBytes());
}
std::string retrieveAsString(size_t len)
{
std::string result(peek(), len);
retrieve(len); //上面一句把缓冲区中可读可读的数据,已经读取出来,这里肯定要对缓冲区进行复位操作
return result;
}
//buffer_.size() - writerIndex_ len 确保可写缓冲区能容纳可写数据
void ensureWriteableBytes(size_t len)
{
if(writableBytes() < len)
{
makeSpace(len); //扩容函数
}
}
//把【data,data+len】的数据添加到writable缓冲区当中
void append(const char* data, size_t len)
{
ensureWriteableBytes(len);
std::copy(data, data + len, beginWrite());
writerIndex_ += len;
}
char* beginWrite()
{
return begin() + writerIndex_;
}
const char* beginWrite() const
{
return begin() + writerIndex_;
}
//从fd上读取数据
ssize_t readFd(int fd, int* savaErrno);
//通过fd发送数据
ssize_t writeFd(int fd, int* saveErrno);
private:
char* begin()
{
return &*buffer_.begin();
}
const char* begin() const
{
return &*buffer_.begin();
}
void makeSpace(size_t len)
{
//可写空间大小 + readerIndex < len + KCheapPrepend
if(writableBytes() + prependableBytes() < len + KCheapPrepend)
{
buffer_.resize(writerIndex_ + len);
}
else
{
size_t readable = readableBytes();
std::copy(begin() + readerIndex_, begin() + writerIndex_, begin() + KCheapPrepend);
readerIndex_ = KCheapPrepend;
writerIndex_ = readerIndex_ + readable;
}
}
std::vector<char> buffer_;
size_t readerIndex_;
size_t writerIndex_;
};
重写Buffer.cc:
#include "Buffer.h"
#include <errno.h>
#include <sys/uio.h>
#include <unistd.h>
/**
* 从fd上读数据, Poller工作在LT模式
* Buffer缓冲区是有大小的,但是从fd上读数据的时候,却不知道tcp数据最终大小
*/
//从fd上读取数据
ssize_t Buffer::readFd(int fd, int* savaErrno)
{
char extrabuf[65536] = {0}; //栈内存空间
struct iovec vec[2];
const size_t writable = writableBytes(); //这是Buffer底层缓冲区剩余的可写空间大小
vec[0].iov_base = begin() + writerIndex_;
vec[0].iov_len = writable;
vec[1].iov_base = extrabuf;
vec[1].iov_len = sizeof extrabuf;
const int iovcnt = (writable < sizeof extrabuf) ? 2 : 1;
const ssize_t n = ::readv(fd, vec, iovcnt);
if(n < 0)
{
*savaErrno = errno;
}
else if(n <= writable) //buffer可写缓冲区已经够存储读出来的数据了
{
writerIndex_ += n;
}
else //extrabuf 里面也写入了数据
{
writerIndex_ = buffer_.size();
append(extrabuf, n - writable); //writerIndex_ 开始写n-writable大小的数据
}
return n;
}
//通过fd发送数据
ssize_t Buffer::writeFd(int fd, int* saveErrno)
{
ssize_t n = ::write(fd, peek(), readableBytes());
if(n < 0)
{
*saveErrno = errno;
}
return n;
}