陈硕 (giantchen_AT_gmail)
Blog.csdn.net/Solstice t.sina.com.cn/giantchen
Muduo 全系列文章列表: http://blog.csdn.net/Solstice/category/779646.aspx
本文介绍 Muduo 中输入输出缓冲区的设计与实现。
本文中 buffer 指一般的应用层缓冲区、缓冲技术,Buffer 特指 muduo::net::Buffer class。
本文前两节的内容已事先发表在 muduo 英文博客 http://muduo.chenshuo.com/2011/04/essentials-of-non-blocking-tcp-network.html 。
如果文中的图形看不清楚,请移步 http://www.cnblogs.com/Solstice/archive/2011/04/17/2018801.html 。
Muduo 的 IO 模型
UNPv1 第 6.2 节总结了 Unix/Linux 上的五种 IO 模型:阻塞(blocking)、非阻塞(non-blocking)、IO 复用(IO multiplexing)、信号驱动(signal-driven)、异步(asynchronous)。这些都是单线程下的 IO 模型。
C10k 问题的页面介绍了五种 IO 策略,把线程也纳入考量。(现在 C10k 已经不是什么问题,C100k 也不是大问题,C1000k 才算得上挑战)。
在这个多核时代,线程是不可避免的。那么服务端网络编程该如何选择线程模型呢?我赞同 libev 作者的观点:one loop per thread is usually a good model。之前我也不止一次表述过这个观点,见《多线程服务器的常用编程模型》《多线程服务器的适用场合》。
如果采用 one loop per thread 的模型,多线程服务端编程的问题就简化为如何设计一个高效且易于使用的 event loop,然后每个线程 run 一个 event loop 就行了(当然、同步和互斥是不可或缺的)。在“高效”这方面已经有了很多成熟的范例(libev、libevent、memcached、varnish、lighttpd、nginx),在“易于使用”方面我希望 muduo 能有所作为。(muduo 可算是用现代 C++ 实现了 Reactor 模式,比起原始的 Reactor 来说要好用得多。)
event loop 是 non-blocking 网络编程的核心,在现实生活中,non-blocking 几乎总是和 IO-multiplexing 一起使用,原因有两点:
- 没有人真的会用轮询 (busy-pooling) 来检查某个 non-blocking IO 操作是否完成,这样太浪费 CPU cycles。
- IO-multiplex 一般不能和 blocking IO 用在一起,因为 blocking IO 中 read()/write()/accept()/connect() 都有可能阻塞当前线程,这样线程就没办法处理其他 socket 上的 IO 事件了。见 UNPv1 第 16.6 节“nonblocking accept”的例子。
所以,当我提到 non-blocking 的时候,实际上指的是 non-blocking + IO-muleiplexing,单用其中任何一个是不现实的。另外,本文所有的“连接”均指 TCP 连接,socket 和 connection 在文中可互换使用。
当然,non-blocking 编程比 blocking 难得多,见陈硕在《Muduo 网络编程示例之零:前言》中“TCP 网络编程本质论”一节列举的难点。基于 event loop 的网络编程跟直接用 C/C++ 编写单线程 Windows 程序颇为相像:程序不能阻塞,否则窗口就失去响应了;在 event handler 中,程序要尽快交出控制权,返回窗口的事件循环。
为什么 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。
TcpConnection 必须要有 output buffer
考虑一个常见场景:程序想通过 TCP 连接发送 100k 字节的数据,但是在 write() 调用中,操作系统只接受了 80k 字节(受 TCP advertised window 的控制,细节见 TCPv1),你肯定不想在原地等待,因为不知道会等多久(取决于对方什么时候接受数据,然后滑动 TCP 窗口)。程序应该尽快交出控制权,返回 event loop。在这种情况下,剩余的 20k 字节数据怎么办?
对于应用程序而言,它只管生成数据,它不应该关心到底数据是一次性发送还是分成几次发送,这些应该由网络库来操心,程序只要调用 TcpConnection::send() 就行了,网络库会负责到底。网络库应该接管这剩余的 20k 字节数据,把它保存在该 TCP connection 的 output buffer 里,然后注册 POLLOUT 事件,一旦 socket 变得可写就立刻发送数据。当然,这第二次 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() 之后他就认为数据迟早会发出去),那么这时候网络库不能立刻关闭连接,而要等数据发送完毕,见我在《为什么 muduo 的 shutdown() 没有直接关闭 TCP 连接?》一文中的讲解。
综上,要让程序在 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。(Again, Muduo EventLoop 采用的是 epoll level trigger,这么做的具体原因我以后再说。)
那么网络库必然要应