muduo Buffer类的设计与使用
muduo的IO模型
event loop
是non-blocking
网络编程的核心,non-blocking
总是和IO multiplexing
一起使用,原因如下:
- 不要使用轮询的方式来检查某个
non-blocking
IO操作是否完成,这样浪费CPU。 IO multiplexing
一般不能和blocking IO
一起使用,因为blocking IO
中read()/write()/accept()/connect()
都有可能阻塞当前线程,这样线程就没有办法处理其他socket
上的IO事件了。
为什么non-blocking网络编程中应用层buffer是必需的
non-blocking
IO的核心思想是避免阻塞在read()
或write()
或其他IO系统调用上,这样可以最大限度地复用thread-of-control
,让一个线程能服务于多个socket
连接。IO线程只能阻塞在IO multiplexing
函数上,如select/poll/epoll_wait
,这样一来,应用层的缓冲是必须的,每个TCPsocket
都要个stateful
的input buffer
和output buffer
。
TcpConnection
必须要有output buffer
,考虑一个常见场景:程序想通过TCP连接发送100KB的数据,但是在write()
调用中,操作系统只接受了80KB,这时肯定不想在原地等待,因为不知道等待多久,程序应该尽快交出控制权,返回event loop
,这种情况下,剩余的20KB应该怎么办?
对于应用程序而言,它只管生成数据,它不应该关心到底数据是一次性发送还是分成几次发送,这些应该由网络库接管,程序只要调用TcpConnection::send()
就可以了,网络库应该接管剩余的20KB,把它保存在该TCPconnection
的output buffer
里面,然后注册POLLOUT
事件,一旦socket
可写就立刻发送数据,当然,第二次write()
也不一定能完全写入20KB,如果还有剩余,网络库应该继续关注POLLOUT
事件,如果写完了20KB,网络库应该关闭POLLOUT
。
如果程序又写入了50KB,这时output buffer
里面还有待发送的20KB数据,那么网络库不应该直接调用write()
,而应该把这50KB数据append
在20KB数据之后,等socket
变得可写的时候再一并写入。
如果output buffer
里面还有待发送的数据,而程序又想关闭连接,那么这时候网络库不能立刻关闭连接,而要等数据发送完毕。
综上,要让程序在write
操作上不阻塞,网络库必需要给每个TCPconnection
配置output buffer
。
TcpConnection必需要有input buffer,TCP是一个无边界的字节流协议,接收方必须要处理”收到的数据尚不构成一条完整的消息“和”一次收到两条消息的数据“等情况。一个常见的场景是,发送方send()
了两条1KB的消息(共2KB),接收方收到数据的情况可能是:
- 一次性收到2KB数据
- 分两次收到,第一次600B,第二次1400B
- 等等
网络库在处理”socket
可读”事件的时候,必须一次性把socket
里的数据读完(从操作系统buffer
搬到应用层buffer
),否则会反复触发POLLIN
事件,造成busy-loop
,那么网络库必然要应对“数据不完整”的情况,收到的数据先放到input buffer
里面,等构成一条完整的消息再通知程序的业务逻辑。所以,在TCP网络编程中,网络库必须要给每个TCPconnection
配置input buffer
。
muduo EventLoop
采用的是epoll()
的level trigger
,而不是edge trigger
。一是为了与传统的poll()
兼容,因为在文件描述符数目较少,活动文件描述符比例较高时,epoll
不见得比poll
高效,必要时可以在进程启动时切换Poller
,二是level trigger
编程更容易。
所有muduo中的IO都是带缓冲的IO(buffered IO
),你不会自己去read()
或write()
某个socket
,只会操作TcpConnection
的input buffer
和output buffer
。更确切地说,是在onMessage()
回调里读取input buffer
;调用TcpConnection::send()
来间接操作output buffer
,一般不会直接操作output buffer
。
7.4.3 Buffer的功能需求
muduo Buffer
的设计要点:
- 对外表现为一块连续的内存
(char* p, int len)
,方便客户代码的编写。 - 其
size()
可以自动增长,以适应不同大小的消息。 - 内部以
std::vector<char>
来保存数据,并提供相应的访问函数。
TcpConnection
会有两个Buffer
成员,input buffer
与output buffer
。
input buffer
,TcpConnection
从socket
读取数据,然后写入input buffer
;客户代码从input buffer
读取。output buffer
,客户代码会把数据写入output buffer
;TcpConnection
从output buffer
读取数据并写入socket
。
具体做法,在栈上准备一个65536字节的extrabuf
,然后利用readv()
来读取数据,iovec
有两块,第一块指向muduo Buffer
中的writable
字节,另一块指向栈上extrabuf
。如果这样读入的数据不多,那么全部都读到了Buffer
中去了;如果长度超过Buffer
的writable
字节数,就会读到栈上的