7.4 Buffer 类的设计与使用

muduo EventLoop 才用的是epoll LT水平触发,而不是 ET 边沿触发:

  • 为了与传统的poll兼容,因为在文件描述符数目较少,活动文件描述符比例较高时epoll不一定比poll更高效,必要时可以在进程启动时切换 Poller。
  • 水平触发编程更容易,以往selectpoll的经验都可以继续用,不会漏掉事件的 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。

image.png
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*是为了应对迭代器失效
image.png
image.png

7.4.5 Buffer 的操作

向 Buffer 中写入 200 字节:
image.png
读取 50 字节后:
image.png
又写入 350 字节:
image.png
一次性读取 350 字节,由于全部数据读完了,readIndex 和 writeIndex 返回原位以备新一轮使用:
image.png

自动增长
muduo Buffer 可以自动增长,如果下标使用指针的话,就会失效,所以采用int

内存腾挪
有时经过若干次读写,readIndex 移动到了较靠后的位置。留下了巨大的 prependable 空间:
image.png
此时如果想写入 300 字节,而 writable 只有 200 字节,不重新分配空间,而要将已有的数据移到前面去
image.png
前方添加(prepend)
程序以固定的 4 个字节表示消息的长度。在不知道具体消息有多长时,可以通过预留一块空间,待消息长度确定的时候,在预留的空间进行记录。image.png

7.5 Protobuf

7.6 Protobuf 编解码与消息分发

后面再看。

7.8 定时器

方案如下:

  • 计时采用gettimeofday()来获取当前时间(微秒):在 x86-64 平台上,**gettimeofday()**不是系统调用,而是在用户态实现的,所以没有上下文切换和陷入内核的开销。
  • 定时采用timerfd_处理定时任务:sleep()``alarm()``usleep()在实现时可能用了SIGALRM信号,在多线程程序中处理信号很棘手,应避免。timerfd_create()把时间变成了一个文件描述符,该“文件”在定时器超时的那一刻变得可读,这样可以方便融入 select/poll 框架中,用统一的方式处理 IO 事件和超时事件。