epoll默认的模式是LT,要说ET不得不提到LT,LT与ET的区别可以用一句话概括: LT模式下只要socket处于可读状态(添加EPOLLIN事件时)或可写状态(添加EPOLLOUT事件时),就会一直返回其socket。 ET模式下在第一次返回socket后,只有当socket由不可写到可写(添加EPOLLIN事件时)或由不可读到可读(添加EPOLLOUT事件时),才会返回其socket。
进一步捋一下
由上可得,我们对ET操作必须要做到以下条件: 如果读:必须要将缓冲区的内容全部读出,即读到缓冲区空为止 如果写:一直写,知道需写的数据写完或是缓冲区满为止。 要做到以上要求,我们必须使用非阻塞套接字,否则socket会常常处在阻塞情况下,从而导致其他套接字饿死的情况发生。(何谓饿死,程序阻塞在当前套接字的操作上,而其他套接字根本没有机会进行操作)
TCP 是一个无边界的字节流协议,接收方必须要处理“收到的数据尚不构成一条完整的消息”和“一次收到两条消息的数据”等等情况。 如果是阻塞模式下,上面的情况根本不需要考虑,因为只要你recv,数据最终一定会过来,但代价就是你会一直阻塞在那里,我们编写服务器当然要尽可能的避免这种情况的出现,所以我们使用非阻塞套接字。 然而,非阻塞模式下,我们就必须要解决上面的情况。 那么,想想,如果给你一个内存空间,你读取数据时,无论取多少,都先放进这个空间,而要处理时从这个空间取数据即可。 有了这个空间,无论是消息不完整还是多个消息,我们都得以解决。 写也是一样的,当你向socket发送数据时,如果发送了一部分,缓冲区满了,此时我们该怎么办呢,只好一直阻塞,直至socket变回可写,再将剩余的数据全部写入。显然,代价还是阻塞,如果我们不想阻塞,仍然需要一个类似上面的空间。 这个空间,就是我们说的buffer,也可以叫做用户缓冲区。
首先,我们希望保证任何时候向buffer里面添加数据,都可以添加成功,也就是说,我们的buffer的空间需要足够大,但我们又不能确定一个固定的数值,因为如果数字设定的小了,还是会出现添加失败的情况,但如果设置的大了,又会导致大量空间的浪费。 所以我们将空间设置为可变的,用vector来保存,因为vector空间增长是以2的幂的形式扩展,很高效。 用两个指针或是变量来作为读标志和写标志,如下图所示。图片摘自Muduo 设计与实现之一:Buffer 类的设计) 这张图画的相当的清晰明了,一目了然,就不再具体描述了。 另外有两点优化,第一点是当Buffer内没有数据的时候(也就是readindex=writeindex时),要将两个标志全部归零,以免空间一直无限制增长下去,前面的空间反而浪费了。 第二点是,要添加数据时,如果剩余的空间不够(writeable),而加上前面空闲的空间(prependable+writeable)能够放下的话,将数据移动到buffer起始位置,以避免一次空间的增长。
参考博客 Muduo 设计与实现之一:Buffer 类的设计
