7.4 Buffer 类的设计与使用
muduo EventLoop 才用的是epoll LT
水平触发,而不是 ET 边沿触发:
- 为了与传统的
poll
兼容,因为在文件描述符数目较少,活动文件描述符比例较高时,epoll
不一定比poll
更高效,必要时可以在进程启动时切换 Poller。 - 水平触发编程更容易,以往
select
、poll
的经验都可以继续用,不会漏掉事件的 bug。 - 读写时不需要等候出现
EAGAIN
,可以节省i系统调用次数,降低延迟。
所有 muduo 中的 IO,都是带有缓冲的 IO(buffered IO),不会自己去read()
和write()
某个 socket,而是操作TCPConnection
的输入、输出缓冲——在onMessage()
回调中读取 input buffer,调用TcpConnection::send()
间接操作 output buffer。
7.4.3 Buffer 的功能需求
muduo Buffer 设计要点:
- 对外表现为一块连续的内存
(char* p, int len)
,方便客户代码编写。 - 其
**size()**
可以自动增长,以适应不同大小的消息。 - 内部以
std::vector<char>
来保存数据,并提供相应的访问函数。
TcpConnection 有两个 Buffer 成员:
- input buffer:TcpConnection 会从 socket 读取数据,然后写入 input buffer(通过
Buffer::readFd()
完成),客户代码从 input buffer 读取数据。 - output buffer:客户代码吧数据写入 output buffer(通过
TcpConnection::send()
完成),从output buffer 读取数据并写入 socket。
Buffer::readFd()
在非阻塞网络编程中设计缓冲区,一方面希望减少系统调用、一次读的数据越多越划算,那么应该准备一个大的缓冲区;另一方面希望减少内存占用,如果有1w并发连接,每个连接一建立就分配50kB的读写缓冲区,将会占用 1GB 内存,而大多情况下这些缓冲区使用率很低。
muduo 使用**readv()**
结合栈空间解决:
在栈上准备一个 65536 字节的 extrabuf,然后利用readv()
读取数据,iovec
有两块:
- 第一块指向 muduo Buffer 中的 writable 字节
- 另一块指向栈上的 extrabuf。
如此,若读入的数据不多,就全部读到 Buffer 中,如果长度超过 Buffer 的 writable 字节数,就会读到栈上的 extrabuf 中,然后程序再把 extrabuf 的数据append()
到 Buffer 中。
通过利用临时栈空间,避免每个连接的初始 Buffer 过大造成内存浪费,也避免了反复调用**read()**
的系统开销。 而且由于采用水平触发,因此不会反复调用read()
直到其返回EAGAIN
,从而降低消息处理的延迟。
muduo::net::Buffer 不是线程安全的:
- 对于 input buffer,
**onMessage()**
回调始终发生在该 TcpConnection 所属的那个 IO 线程,应用程序应该在onMessage()
完成对 input buffer 的操作,并且不要把 input buffer 暴露给其他线程。这样对 input buffer 的所有操作都在同一个线程中。 - 对于 output buffer,应用程序不会直接操作它,而是调用
TcpConnection::send()
来发送数据,后者是线程安全的。
如果TcpConnection::send()
调用发生在TcpConnection
所属的那个线程,它转而调用TcpConnection::sendInLoop()
,在当前线程操作 output buffer;如果发生在别的线程,那么不会调用sendInLoop()
,而是通过EventLoop::runInLoop()
把sndInLoop()
调用转移到 IO 线程,这样还是会在 IO 线程中操作 output buffer。
7.4.4 Buffer 的数据结构
Buffer 的内部是一个std::vector<char>
,还有两个int
数据成员,指向 vector 中的元素,不是char*
是为了应对迭代器失效。
7.4.5 Buffer 的操作
向 Buffer 中写入 200 字节:
读取 50 字节后:
又写入 350 字节:
一次性读取 350 字节,由于全部数据读完了,readIndex 和 writeIndex 返回原位以备新一轮使用:
自动增长
muduo Buffer 可以自动增长,如果下标使用指针的话,就会失效,所以采用int
。
内存腾挪
有时经过若干次读写,readIndex 移动到了较靠后的位置。留下了巨大的 prependable 空间:
此时如果想写入 300 字节,而 writable 只有 200 字节,不重新分配空间,而要将已有的数据移到前面去
前方添加(prepend)
程序以固定的 4 个字节表示消息的长度。在不知道具体消息有多长时,可以通过预留一块空间,待消息长度确定的时候,在预留的空间进行记录。
7.5 Protobuf
7.6 Protobuf 编解码与消息分发
后面再看。
7.8 定时器
方案如下:
- 计时采用
gettimeofday()
来获取当前时间(微秒):在 x86-64 平台上,**gettimeofday()**
不是系统调用,而是在用户态实现的,所以没有上下文切换和陷入内核的开销。 - 定时采用
timerfd_
处理定时任务:sleep()``alarm()``usleep()
在实现时可能用了SIGALRM
信号,在多线程程序中处理信号很棘手,应避免。timerfd_create()
把时间变成了一个文件描述符,该“文件”在定时器超时的那一刻变得可读,这样可以方便融入 select/poll 框架中,用统一的方式处理 IO 事件和超时事件。