从epoll构建muduo-8 加入发送缓冲区和接收缓冲区

mini-muduo版本传送门
version 0.00 从epoll构建muduo-1 mini-muduo介绍
version 0.01 从epoll构建muduo-2 最简单的epoll
version 0.02 从epoll构建muduo-3 加入第一个类,顺便介绍reactor
version 0.03 从epoll构建muduo-4 加入Channel
version 0.04 从epoll构建muduo-5 加入Acceptor和TcpConnection
version 0.05 从epoll构建muduo-6 加入EventLoop和Epoll
version 0.06 从epoll构建muduo-7 加入IMuduoUser
version 0.07 从epoll构建muduo-8 加入发送缓冲区和接收缓冲区
version 0.08 从epoll构建muduo-9 加入onWriteComplate回调和Buffer
version 0.09 从epoll构建muduo-10 Timer定时器
version 0.11 从epoll构建muduo-11 单线程Reactor网络模型成型
version 0.12 从epoll构建muduo-12 多线程代码入场
version 0.13 从epoll构建muduo-13 Reactor + ThreadPool 成型

mini-muduo v0.07版本,这个版本是加入了发送缓冲区和接收缓冲区的初始版本,后续v0.08完善了缓冲区的实现。mini-muduo完整可运行的示例可从github下载,使用命令git checkout v0.07可切换到此版本,在线浏览此版本到这里

1 为什么要有发送缓冲区和接收缓冲区,muduo作者已经在<<7.4.2 为什么non-blocking网络编程中应用层buffer是必须的>>节中详细介绍过了。建议有条件的同学直接看书。我觉得这部分知识非常重要所以再摘录一遍书中的内容。我们分开对待这两个缓冲区。

首先,TcpConnection必须有输出缓冲区。

假设程序想通过TCP连接发送100k数据,但是在write系统调用中,操作系统只接受了80k,而作为用户你也不清楚什么时候你才有机会发送剩余的20k,你不得不一直保存着这20k数据,其实保存这20k数据应该是网络库的任务。库的用户应该只负责调用TcpConnection::send()来告诉网络库它要发100k数据,从此之后用户就不应该再操心这些数据了,每次发送了多少,还剩多少,下次发多少都应该由网络库来完成。

其次,TcpConnection必须有输入缓冲区。

TCP是一个无边界的字节流协议,接受方必须要处理"收到的数据尚不构成一天完整的消息"和"一次收到两条消息的数据"等情况。假设发送方发送了两条1k的消息(共2k),接收方收到的数据的情况可能是:

    1 一次性收到2k数据

    2 分两次收到,第一次0.6k,第二次1.4k

    3 分两次收到,第一次1.4k,第二次0.6k

    4 分两次收到,第一次1k,第二次1k

    5 分三次收到,第一次0.6k,第二次0.8k,第三次0.6k

    6 其他任何可能。一般而言,长度为n字节的消息分块到达的可能性有“2的n次方-1”种

所以网络库必须在上述情况下将消息都逐一接收下来,并且让用户有机会在单条消息到达时获得通知。

2 输出缓冲区的实现:TcpConnection添加了一个代表发送缓冲区的成员变量_outBuf。用户将100k数据通过TcpConnection::send送给网络库,网络库先检查输出缓冲区是否为空(_outBuf->empty()),如果为空说明现在没有等待发送的数据,就直接将这100k数据送给write系统调用,看下操作系统能接受多少,然后将剩余的没发送玩的数据添加到输出缓冲区的尾部。如果发送缓冲区不为空,说明之前已经堆积了一些数据则本次操作就不尝试write了,直接将100k数据附加在发送缓冲区后。注意在将数据加入发送缓冲区后会将EPOLLOUT事件注册到epoll文件描述符(TcpConnection.cc 98行)。只有这样才能接到epoll文件描述符的通知来继续发送剩余的数据。当操作系统发现自己的缓冲区有更多可用空间时,通过epoll_wait返回来告知网络库,可以继续发了,这时Channel::handleWrite被触发,Channel::handleWrite会调用到TcpConnection::handleWrite,后者将剩余的数据通过write继续发送,并且在全部数据发送完毕后取消关注EPOLLOUT事件(TcpConnection.cc 76行)。取消关注EPOLLOUT非常关键,如果没有这一部,epoll_wait将在每次调用epoll_wait()时都立刻返回EPOLLOUT,因为每当操作系统有更多的发送缓冲区可以被填满时都会通知网络库,而网络库因为没有更多要发送的数据送给操作系统导致其无休止的通知。

3 输入缓冲区的实现:TcpConnection添加了一个代表接收缓冲区的成员变量_inBuf,在每次handleRead()回调被调用时,使用read系统调用将数据读出来然后追加到输入缓冲区中,并通知用户。调节缓冲区长度的工作交给用户来处理了,相见下面的条目6里EchoServer的改动。

4 修改Epolle::update()方法,因为原来只需要关注EPOLLIN事件,所以epoll_ctl第二个参数只需要传入CLT_ADD,现在因为实现了发送缓冲区,所以要关注和取消关注EPOLLOUT了,所以epoll_ctl第二个参数某些时候要传入CLT_MOD。我们采用和原始muduo同样的处理方法,为Channel加入一个int _index成员变量,新建的Channel此值为kNew(值为-1),此时调用Epoll::update使用的是CTL_ADD,调用过之后将_index设置为_kAdded(值为1),后续调用update就是变成CTL_MOD了,这样正好用来添加和删除EPOLLOUT事件。

5 修改了IChannelCallback接口,添加了用于发送缓冲区的handleWrite回调,这个回调在epoll_wait返回EPOLLOUT事件时被触发。修改了几处命名规范问题。修改handleRead::handleWrite()方法的参数列表,删除了int socket参数,改为从channel里取,因为这个fd已经保存在Channel里了,显然这里没有必要再传入。

6 IMuduoUser和EchoServer都进行了改动,IMuduoUser的onMessage接口里的第二个参数从const string&改为string*。在之前的版本里,数据是单向传递,网络库告知用户有新数据到达,不管客户是否读取了这些数据,网路库都会在onMessage通知完用户后将数据丢弃(参看v0.06版本 TcpConnection.cc 56行,使用的局部变量会自动销毁),如果用户需要的一条消息要通过多次onMessage回调才能收完整,则用户必须自己缓存这部分数据。由于本版本改为由网络库来实现接收缓冲区,则用户必须告知网络库他读取了多少数据,这些数据库就可以丢弃了。在本版本中专门用作缓冲区的Buffer类还没有实现,于是我简单传递一个string*,由用户直接将接收缓冲区的数据舍弃掉。同时EchoServer在读取的时候使用了下面的代码

[cpp]  view plain copy print ?
  1. while(data->size() > MESSAGE_LENGTH)  
  2. {  
  3.     ...  
  4. }  
这里使用while而不是if的原因是如果一次收到n条完整的消息,if就只能处理第一条而忽略了后面的几条。EchoServer里添加的代码也叫做编解码器(codec),如果为了让程序更容易理解,可以将这块实现做成单独的类,作用就是每当一条完整的消息被收到时,通知用户程序。

7 就输入输出缓冲区的实现来说,还有几个明显的问题,首先,onWriteComplate回调还没有实现,用户不知道自己send的数据什么时候全部送给了操作系统。其次,没有实现Buffer类,而是用std::string进行了简单代替。这两个问题将在下一个版本,也就是v0.08版本得到解决。

你可能感兴趣的:(从epoll构建muduo-8 加入发送缓冲区和接收缓冲区)