Buffer类的设计

应用层缓冲区Buffer设计

muduo的I/O模型是I/O复用,并且文件描述符设置为非阻塞模式,因为如果使用阻塞模式,I/O线程就有可能阻塞在read、write这些系统调用之上,这样一来,即使其它描述符的IO事件到来,IO线程也不能立刻去处理,也就不能最大程度的使用IO线程。 

为什么TcpConnection必须要有output buffer?

考虑一个常见场景:程序想通过 TCP 连接发送 100k 字节的数据,但是在 write() 调用中,操作系统只接受了 80k 字节, 我们知道write()的返回不代表消息已经到了对等方,仅仅表示数据从用户空间到了内核空间,还受 TCP advertised window 的控制,所以不一定着100k字节就到了内核的发送缓冲区中,你肯定不想在原地等待,因为不知道会等多久(取决于对方什么时候接受数据,然后滑动 TCP 窗口)。程序应该尽快交出控制权,返回 event loop。在这种情况下,剩余的 20k 字节数据怎么办?应该添加到应用层的发送缓冲区。    

对于应用程序而言,它只管生成数据,它不应该关心到底数据是一次性发送还是分成几次发送,这些应该由网络库来操心,程序只要调用 TcpConnection::send() 就行了,网络库会负责到底。网络库应该接管这剩余的 20k 字节数据,把它保存在该 TCP connection 的 output buffer 里,然后注册 POLLOUT 事件,一旦 socket 变得可写就立刻发送数据。就可以把应用层output buffer里面的数据拷贝到内核中的发送缓冲区 。当然,这第二次 write() 也不一定能完全写入 20k 字节,如果还有剩余,网络库应该继续关注 POLLOUT 事件;如果写完了 20k 字节,网络库应该停止关注 POLLOUT,以免造成 busy loop。(Muduo EventLoop 采用的是 epoll level trigger,这么做的具体原因我以后再说。)

如果程序又写入了 50k 字节,而这时候 output buffer 里还有待发送的 20k 数据,那么网络库不应该直接调用 write(),而应该把这 50k 数据 append 在那 20k 数据之后,等 socket 变得可写的时候再一并写入。

如果 output buffer 里还有待发送的数据,而程序又想关闭连接(对程序而言,调用 TcpConnection::send() 之后他就认为数据迟早会发出去),那么这时候网络库不能立刻关闭连接,而要等数据发送完毕.

综上,要让程序在 write 操作上不阻塞,网络库必须要给每个 tcp connection 配置 output buffer。

为什么TcpConnection必须要有input buffer?

TCP 是一个无边界的字节流协议,接收方必须要处理“收到的数据尚不构成一条完整的消息”和“一次收到两条消息的数据”等等情况。一个常见的场景是,发送方 send 了两条 10k 字节的消息(共 20k),接收方收到数据的情况可能是:

一次性收到 20k 数据
分两次收到,第一次 5k,第二次 15k
分两次收到,第一次 15k,第二次 5k
分两次收到,第一次 10k,第二次 10k
分三次收到,第一次 6k,第二次 8k,第三次 6k
其他任何可能

网络库在处理“socket 可读”事件的时候,必须一次性把 socket 里的数据读完(从操作系统 buffer 搬到应用层 buffer),否则会反复触发 POLLIN 事件,造成 busy-loop。

那么网络库必然要应对“数据不完整”的情况,收到的数据先放到 input buffer 里,等构成一条完整的消息再通知程序的业务逻辑。也就是说,接受到数据,存至input buffer,通知上层的应用程序,onMessage(Buffer *buff)回调,根据应用层协议判定是否是一个完整的包,如果不是一条完整的消息,不会取走数据,也不会相应的处理,如果是一条完整的消息,将取走这条消息,并进行相应的处理。怎么判断是完整的消息,就是上层应用程序的事了。

所以,在 tcp 网络编程中,网络库必须要给每个 tcp connection 配置 input buffer。

所有 muduo 中的 IO 都是带缓冲的 IO (buffered IO),你不会自己去 read() 或 write() 某个 socket,只会操作 TcpConnection 的 input buffer 和 output buffer。更确切的说,是在 onMessage() 回调里读取 input buffer;调用 TcpConnection::send() 来间接操作 output buffer,一般不会直接操作 output buffer。

谁会用 Buffer?谁写谁读?根据前文分析,TcpConnection 会有两个 Buffer 成员,input buffer 与 output buffer。一个是接受缓冲区,一个是发送缓冲区。 

input buffer,TcpConnection 会从 socket 读取数据,然后写入 input buffer(其实这一步是用 Buffer::readFd() 完成的);客户代码从 input buffer 读取数据。
也就是从内核缓冲区读取数据,写入到input buffer中。客户代码是从 input buffer 读取数据。对于网络库来说,是向input buffer 写入数据。readFd()从socket对应的内核缓冲区读取数据,拷贝到input buffer中。对于网络库来说,input buffer实际上是一个写操作。

output buffer,客户代码会把数据写入 output buffer (其实这一步是用 TcpConnection::send() 完成的);TcpConnection 从 output buffer 读取数据并写入socket。
对于网络库来说,是从output buffer读取数据,写入到socket的内核缓冲区中。

其实,input 和 output 是针对客户代码而言,客户代码从 input 读,往 output 写。TcpConnection 的读写正好相反。

对方发送了两条消息,50和350
接受方第一次接受了200字节,第二次接受了200字节

从socket读了200字节,写入到buffer中
从buffer中取回50字节
又从socket读了200字节,写入到buffer中
从buffer取回350字节

猜你喜欢

转载自blog.csdn.net/wk_bjut_edu_cn/article/details/80903213
今日推荐