1、概念
在讲清楚这两个模式的概念之前,我们应该明确一下他们都是epoll对文件描述符的操作模式。LT(电平触发)和ET(边沿触发)。LT是默认的工作模式。在LT模式下epoll就相当于一个效率比较高的poll。如果我们想要在ET模式下来操作该文件描述符的话,就往该文件描述符上注册EPOLLET事件。就是event.events = EPOLLEF
。
LT:
当epoll_wait检测到有事件发生并将此事件通知给应用程序过后,应用程序可以不立即处理该事件。这样当应用程序下一次调用epoll_wait的的时候,epoll_wait还会再次向应用程序通告此事件,直到该事件被处理ET:
当epoll_wait检测到有事件发生并将此事件通知给应用程序过后,应用程序必须立即处理该事件,因为后续的epoll_wait不会再次向此应用程序通知这一事件。
2、LT模式测试
在我们之前讲述的epoll.c的代码上做以下修改
修改接收数据的函数,每次只收一个字节:
即recv(fd,buff,5,0)
客户端代码还是不改变,运行两个客户端分别输入hello world 和this is linux。会出现下面这个结果:
从运行结果我们可以看到发送了一个大于5的数据,服务端每次只接受5个数据,那么epoll_wait()就会不断地提醒你,让你去处理,直至没有就绪事件。可以看到这种模式下epoll_wait()就被调用了多次,所以我们一般不使用它。
3、ET模式测试
我们往epoll内核事件表中注册一个文件描述符上的EPOLLET事件
event.events=EPOLLIN|EPOLLRDHUP|EPOLLET;
其他代码不变,运行结果如图所示:
从上面的运行结果我们可以看到,调用了两次一次是客户端连接,一次是处理hello。但是我们后面的wold数据去哪儿了呢?
首先可以肯定的是——肯定不在客户端缓冲区,因为send就表示发送过去了。所以在服务端的缓冲区中。
由此,我们接下来再发送this is linux的时候就输出了wold.
从上面的运行结果我们可以看到它继续读缓冲区的数据。
- 应用程序读数据的多少是由程序来决定的,读完了就完了。如果没有读完就存放在TCP的缓冲区中。
- 如果缓冲区满了则send会阻塞。但是我们一般都会采取拥塞控制和滑动窗口来控制。
那我们如何来解决一直不能读到实时信息,都是缓冲区的问题呢,也就是说让他做到提醒一次,一次性处理完所有数据?
由此我们引出了改进版的ET模式
4、改进ET模式
- 首先,需要
让它一次性把就绪事件都处理完
。那么我们在处理数据处加个循环,一直处理。但是如果hello world两次都读完之后recv就会阻塞退不出循环,除非客户端关闭,因为他没有办法退出,这时I/O复用已经没有作用。 - 为了让他处理完所有就绪事件可以退出,我们需要设计在获取事件描述符c后。将c设置成为非阻塞运行,这样recv就不会阻塞,则返回-1,并且会设置全局的errno。
如下,我们将c设置为非阻塞方式。关于fcntl函数的补充请参考博文fcntl
int oldoption= fcntl(c,F_GETFL);
int newoption=oldoption| O_NONBLOCK;//新的标志位设为非阻塞状态。
fcntl(c,F_SETFL,newoption);//设置新的状态
- 处理时要进行判断
if(errno==EAGAIN ||errno==EWOULDBLOCK)
如果 成立的话就表示没有数据可以读或者数据已经读取完毕,这时我们就可以退出循环。并且,只有读完了才给客户端发ok,不要读一次数据就发一次。
具体代码实现如下:
# define _GNU_SOURCE
# include<stdio.h>
# include<unistd.h>
# include<string.h>
# include<assert.h>
# include<sys/types.h>
# include<sys/socket.h>
# include<sys/epoll.h>
# include<arpa/inet.h>
# include<netinet/in.h>
# include<fcntl.h>
# include<errno.h>
# define MAXEVENTS 100
int InitSocket()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==