考虑一个常见场景:
程序想通过TCP连接向对方发送100K字节的数据,但是write()调用中,操作系统只接收了80K字节(受TCP advertised window的控制,细节见TCPv1),你肯定不想在原地等待,因为不知道会等多久(取决于对方什么时候能够接收数据,然后滑动TCP窗口)。程序应该尽快交出控制权,返回eventloop。在这种情况下,剩余的20K字节数据怎么处理?
对于应用程序而言,它只管生成数据,它不应该关心到底数据是一次性发送还是分成几次发送,这些应该由网络库来操心
,程序只要调用TcpConnection::send()就行了,网络库会负责到底。那么,网络库是怎么做的呢?(请看①②③④⑤的描述)
① 还有20K字节的数据没有发送,此时,网络库会接管这剩余的20K字节数据,把它保存在该Tcp connection的output buffer中,然后注册POLLOUT事件,一旦socket变得可写,就立即将20K数据的一部分写入socket。
② 当然,这第二次write()也不一定能将20K字节全部写入到socket,如果还有剩余,网络库应该继续关注POLLOUT事件;
③ 当20K字节数据全部写完后,网络库将停止关注POLLOUT事件,以免造成busy loop。(Muduo EventLoop采用的就是epoll level trigger,LT模式当缓冲区可读可写时,将一直触发读写事件,这么做的具体原因,我以后再说。)
④ 如果程序又写入了50K字节,而这时候output buffer里还有待发送的20K数据,那么网络库不应该直接调用write(),而应该把这50K数据append在那20K数据之后,等socket变得可写的时候,再一并写入。
⑤ 如果output buffer里还有待发送的数据,而程序又想关闭连接(对于程序而言,调用TcpConnection::send()之后,就认为数据迟早会发送出去),那么这时候网络库不能立即关闭连接,而要等待数据发送完毕
后,才去断开连接(参考“为什么muduo的shutdown()没有直接关闭TCP连接”)。
综上,要让程序在write操作上不阻塞,网络库必须要给每个tcp connection配置output buffer。
TCP是一个无边界的字节流协议,接收方必须要处理“收到的数据尚不构成一条完整的消息”和“一次收到两条消息的数据”等等粘包情况。
网络库在处理“socket可读”事件的时候,必须一次性把socket里的数据读完(即将数据从操作系统buffer搬到应用层buffer),否则会反复触发POLLIN事件,造成busy-loop。(再说一遍:Muduo EventLoop采用的就是epoll level trigger,这么做的具体原因,我以后再说。)
那么网络库必然要应对“数据不完整”的情况,收到的数据先放到input buffer里,等构成一条完整的消息再通知程序的业务逻辑。这通常是code的职责,间第2.3节“Boost.Asio聊天服务器”一文中“TCP分包”的论述和代码。
所以,在tcp网络编程中,网络库必须要给每一个tcp connection配置input buffer。
当input buffer中有数据时,就会通知上层应用程序,onMessage(Buffer*)回调,根据上层应用层协议判定是否是一个完整的包,即:如果不是一条完整的消息,就不会取走数据,也不会进行相应的处理;如果是一条完整的消息,将取走消息并进行相应的处理。
① 与poll兼容
② LT模式不会发生漏掉事件的BUG,但POLLOUT事件不能一开始就关注,否则会出现busy loop,而应该在write无法完全写入内核缓冲区的时候才关注,将未写入内核缓冲区的数据添加到应用层output buffer,直到应用层output buffer写完,写完后停止关注POLLOUT事件
③ 读写的时候不必等候EAGIN,可以节约系统调用的次数,降低延迟。(注:如果用ET模式,读的时候读到EAGAIN,写的时候直到output buffer写完或者EAGAIN)
muduo buffer的设计考虑了常见的网络编程需求,我试图在易用性和性能之间找一个平衡点,目前这个平衡点更偏向于易用性。
muduo buffer的设计要点:
(1) 对外表现为一块连续的内存(char* ,len),以方便客户代码的编写
(2) 其size()可以自动增长,以适应不同大小的消息。他不是一个fixed size array(即char buf[8192])
(3) 内部以vector of char 来保存数据,并提供相应的访问函数
Buffer其实像一个queue,从末尾写入数据,从头部读出数据。即内部维护了下标,分别是:读入位置、写入位置。
TcpConnection会有两个Buffer成员, Buffer inputBuffer_ 、Buffer outputBuffer_;
其实,input、output是针对于客户端代码而言,客户代码从input读,往output写。TcpConnection的读写正好相反。
Buffer的作用就是暂时存储数据。当向Buffer写入数据后,Buffer可写入空间writeable减小,可读空间readable增大;取走数据后变化相反。
muduo中的Buffer为封装了的vector。vector为一块连续空间,且其本身具有自动增长的性质,它的迭代器为原始指针,使用起来较为方便。
①Buffer类只有3个成员变量,分别是buffer_、readerIndex_、writerIndex_
std::vector<char> buffer_; //vector用来替代固定大小的数字
size_t readerIndex_; //读位置
size_t writerIndex_; //写位置
kCRLF是一个柔性数组,存放“\r\n”,可以用来搜索缓冲区数据中的”\r\n“
②其中,readerIndex_、writerIndex_将整个buffer_分成了3个区域,分别是prependable、writable、readable ,Buffer的结构如下图:
③readeIndex_是可读部分的首位置,writerIndex_是可写部分的首位置。两个中间就是实际可读(readable)的元素。前部预留的Prepend部分可以用来向头部添加内容而不需要内存挪动的消耗。下面三个函数能获得这3个区域的大小:
size_t readableBytes() const //可读空间,里面放有缓存的数据
{ return writerIndex_ - readerIndex_; }
size_t writableBytes() const //可写空间大小
{ return buffer_.size() - writerIndex_; }
size_t prependableBytes() const //预留空间大小
{ return readerIndex_; }
① 构造函数
static const size_t kCheapPrepend = 8; //prependable的大小
static const size_t kInitialSize = 1024; //writable的大小
explicit Buffer(size_t initialSize = kInitialSize)
: buffer_(kCheapPrepend + initialSize), // 8+1024
readerIndex_(kCheapPrepend), //8
writerIndex_(kCheapPrepend) //8
{
assert(readableBytes() == 0);
assert(writableBytes() == initialSize);
assert(prependableBytes() == kCheapPrepend);
}
② 获取readIndex、writerIndex位置
//图中的readIndex位置
const char* peek() const
{ return begin() + readerIndex_; }
//图中writerIndex位置
char* beginWrite()
{ return begin() + writerIndex_; }
const char* beginWrite() const
{ return begin() + writerIndex_; }
基本的read-write操作
①当有足够的空间读写时,每次read走、write入数据后,都要将readIndex、writeIndex指针下标后移
②当将数据全部read空时,readIndex、writeIndex指针下标设置为初始位置,即kCheapPrepend(8)
内部腾挪
Buffer没有封装为一个环,所以当Buffer使用一段时间后,其真正的可写入空间不一定是size() - writeIndex,因为readIndex和prependable之间可能还有空间。因此当可写如空间不够时,有时可以通过把CONTENT向“前移”来达到目的。
void shrink(size_t reserve)
{
Buffer other; //生成临时对象other(大小为readableBytes()+reserve),交换之后,临时对象析构掉
//两种情况
//1.空间不够resize空间
//2.空间足够内部腾挪
other.ensureWritableBytes(readableBytes()+reserve);
//将当前空间中readable中的数据全部添加到other空间中,然后再交换swap
other.append(toStringPiece());
swap(other);
}
3. 扩增可写空间writeable
当writeable不能够容纳足够多的数据时,要进行动态的缓冲区扩充,即:①重新申请一块更大的缓冲区 ②旧空间上的数据拷贝到新缓冲区上 ③释放旧空间
[1] 从readable区域中,读走数据
//读取len个字节的数据
void retrieve(size_t len)
{
assert(len <= readableBytes());
if (len < readableBytes())//if 要读字节数 < 可读字节数
{
readerIndex_ += len; //只是readerIndex_后移len
}
else //if 要读字节数 > 可读字节数,即将数据全部读走
{
retrieveAll(); //readerIndex_、writerIndex_位置重置为初始位置(8)
}
}
void retrieveAll()
{
readerIndex_ = kCheapPrepend;
writerIndex_ = kCheapPrepend;
}
//取出[readerIndex_,end]之间的数据,end为手动指定的
void retrieveUntil(const char* end);
//返回值:void
void retrieveInt64();
void retrieveInt32();
void retrieveInt16();
void retrieveInt8();
//返回值:intXX_t,即读到的值
int64_t readInt64();
int32_t readInt32();
int16_t readInt16();
int8_t readInt8();
//取出readable中len字节的数据,并转换成string类型,再返回
string retrieveAsString(size_t len);
//取出readable中所有的数据,并转换成string类型,再返回
string retrieveAllAsString();
//取出readable中所有的数据,并转换成StringPiece类型,再返回
StringPiece toStringPiece() const;
[2] 将数据写入到writable区域中
append():把数据追加到writeable区域中,如果空间不够,会进行makespace
void append(const char* data, size_t len)
{
ensureWritableBytes(len); //确保有可写空间,空间不够,就自动分配
std::copy(data, data+len, beginWrite());
hasWritten(len);
}
void ensureWritableBytes(size_t len)
{
if (writableBytes() < len) //如果可写空间不够
{
makeSpace(len); //resize或移动数据,使Buffer能容下len大数据
}
assert(writableBytes() >= len);
}
void makeSpace(size_t len) //resize或移动数据,使Buffer能容下len大数据
{
//确保空间是真的不够,而不是挪动就可以腾出空间
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());
}
}
void append(const StringPiece& str)
{
append(str.data(), str.size());
}
void append(const void* data, size_t len)
{
append(static_cast<const char*>(data), len);
}
void appendInt64(int64_t x)
{
int64_t be64 = sockets::hostToNetwork64(x);
append(&be64, sizeof be64);
}
void appendInt32(int32_t x);
void appendInt16(int16_t x);
void appendInt8(int8_t x);
[3] 向从Prepend区域中,放入数据的函数API:
void prependInt64(int64_t x); //放入x,x大小为8字节
void prependInt32(int32_t x);
void prependInt16(int16_t x);
void prependInt8(int8_t x);
void prepend(const void* data, size_t len);
实现功能:从socket中读取数据,存放到Buffer中的writeable中
实现细节:因为不知道一次性可以读多少,因此先在栈上开辟了65536字节的空间extrabuf,使用readv读至Buffer_中。①如果Buffer中wriable足够存放从fd读到的数据,则读取完毕;②否则剩余数据先读到extrabuf中然后以append的方式追加入Buffer_。
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();
//第一块缓冲区,指向可写空间begin()+writerIndex_
vec[0].iov_base = begin()+writerIndex_;
vec[0].iov_len = writable;
//第二块缓冲区,指向栈上空间extrabuf
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
//返回值n:表示readv读到的字节数
const ssize_t n = sockets::readv(fd, vec, iovcnt); //iovcnt=2
if (n < 0)
{
*savedErrno = errno;
}
else if (implicit_cast<size_t>(n) <= writable)//writable足够容纳
{
writerIndex_ += n; //writerIndex_后移n
}
else //writable不足够容纳 ==> 数据被接受到了第二块缓冲区extrabuf,将其append至buffer
{
writerIndex_ = buffer_.size();//更新writerIndex到buffer.size()位置
append(extrabuf, n - writable);//将extrabuf中的数据,再追加到buffer中
}
return n;
}