一、水平触发(LT)和边沿触发(ET)
在电路中的有水平触发和边沿触发的概念,在epoll读取事件下,水平触发可以理解为,蓝色那一部分,只要存在可读的情况,就会一直读取。而边沿触发,可以理解为红色箭头所指向,发生跳变的部分,就会触发一次。
在epoll中
events=EPOLLIN
为读取事件,LT模式
events=EPOLLIN|EPOLLET
为读取事件,ET模式
events=EPOLLOUT
为写事件,LT模式
events=EPOLLOUT|EPOLLET
为写事件,ET模式
- recv的时候
如果设置为LT,只要 接受缓冲 不为空,就会一直触发EPOLLIN,直到 接受缓冲 为空
如果设置为ET,只要 客户端 发送一次数据,就会触发一次EPOLLIN - send的时候
如果设置为LT,只要 发送缓冲 不满,就会一直触发EPOLLOUT
如果设置为ET,有注册EPOLLOUT事件,才会一次触发一次EPOLLOUT
ET模式 效率要比 LT模式高
小数据使用边沿触发,大数据使用水平触发
比如listenfd,接受缓冲区 可能存放多个客户端连接请求的信息,这时候要使用水平触发(LT),因为accept每次只能处理一个,需要多次触发。如果用边沿触发(ET)可能会漏掉一些连接。
二、例子
1.例子:水平触发(LT)
下面是epoll实现的简单tcp服务器,用LT的触发方式
完整代码
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<string.h> #include<sys/socket.h> #include<sys/epoll.h> #include<arpa/inet.h> #include<netinet/in.h> #include<fcntl.h> #define EPOLL_SIZE 1024 #define BUFFER_SIZE 4096 int main(int argc,char** argv){ int listenfd=socket(AF_INET,SOCK_STREAM,0); sockaddr_in serveraddr; memset(&serveraddr,0,sizeof(sockaddr_in)); serveraddr.sin_family=AF_INET; serveraddr.sin_port=htons(8888); serveraddr.sin_addr.s_addr=htonl(INADDR_ANY); bind(listenfd,(sockaddr*)&serveraddr,sizeof(sockaddr_in)); listen(listenfd,10); int epfd=epoll_create(1); epoll_event events[EPOLL_SIZE]={0}; epoll_event ev; ev.data.fd=listenfd; ev.events=EPOLLIN; epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); char buffer[BUFFER_SIZE]={0}; while(1){ int nready=epoll_wait(epfd,events,EPOLL_SIZE,5); if(nready==-1) continue; for(int i=0;i<nready;i++){ int fd=events[i].data.fd; if(fd==listenfd){ sockaddr_in clientaddr; memset(&clientaddr,0,sizeof(sockaddr_in)); socklen_t clientLen=sizeof(sockaddr_in); int clientfd=accept(listenfd,(sockaddr*)&clientaddr,&clientLen); ev.data.fd=clientfd; ev.events=EPOLLIN; epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev); } else { int n=recv(fd,buffer,BUFFER_SIZE,0); if(n==0){ ev.data.fd=fd; ev.events=EPOLLIN; epoll_ctl(epfd,EPOLL_CTL_DEL,fd,&ev); close(fd); break; } else if(n>0){ printf("Recv:%s\n",buffer); } } } } return 0; }
其中,只设置了EPOLLIN,就代表读取。并且默认为水平触发模式(LT)
现在利用 客户端向服务器发送这么一条数据,可以看到接受到了数据了
ev.events=EPOLLIN;
现在将其int n=recv(fd,buffer,BUFFER_SIZE,0);
修改为int n=recv(fd,buffer,5,0);
意思是现在每次只能读取长度为5的数据了
再来测试下,得到结果
可以发现,读取EPOLLIN这个事件,被多次触发,直至读完
因此LT模式,在EPOLLIN事件下,只要 读取缓冲 不为空 就会一直读取
2.例子:边沿触发(ET)
还是保持上一个例子,代码的基础上,每次只读取长度为5的数据
将clientfd的 事件ev的触发设置成 ET模式 ,ev.events=EPOLLIN|EPOLLET;
注意不是修改listenfd的触发模式
让客户端发送一条 数据,发现只有 长度为5的数据,剩下一部分,没有发出来
于是让客户端继续发送一条数据:“nihao”
结果客户端,没有输出“nihao”,而是把之前的未输出完的数据给输出了。
因此可以理解为,每当 接受缓冲 有新的数据时,就会触发一次。
完整代码
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<string.h> #include<sys/socket.h> #include<sys/epoll.h> #include<arpa/inet.h> #include<netinet/in.h> #include<fcntl.h> #define EPOLL_SIZE 1024 #define BUFFER_SIZE 4096 int main(int argc,char** argv){ int listenfd=socket(AF_INET,SOCK_STREAM,0); sockaddr_in serveraddr; memset(&serveraddr,0,sizeof(sockaddr_in)); serveraddr.sin_family=AF_INET; serveraddr.sin_port=htons(8888); serveraddr.sin_addr.s_addr=htonl(INADDR_ANY); bind(listenfd,(sockaddr*)&serveraddr,sizeof(sockaddr_in)); listen(listenfd,10); int epfd=epoll_create(1); epoll_event events[EPOLL_SIZE]={0}; epoll_event ev; ev.data.fd=listenfd; ev.events=EPOLLIN; epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); char buffer[BUFFER_SIZE]={0}; while(1){ int nready=epoll_wait(epfd,events,EPOLL_SIZE,5); if(nready==-1) continue; for(int i=0;i<nready;i++){ int fd=events[i].data.fd; if(fd==listenfd){ sockaddr_in clientaddr; memset(&clientaddr,0,sizeof(sockaddr_in)); socklen_t clientLen=sizeof(sockaddr_in); int clientfd=accept(listenfd,(sockaddr*)&clientaddr,&clientLen); ev.data.fd=clientfd; ev.events=EPOLLIN|EPOLLET; epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev); } else { int n=recv(fd,buffer,5,0); if(n==0){ ev.data.fd=fd; ev.events=EPOLLIN; epoll_ctl(epfd,EPOLL_CTL_DEL,fd,&ev); close(fd); break; } else if(n>0){ printf("Recv:%s\n",buffer); } } } } return 0; }
3.例子:边沿触发(ET)并设置非阻塞io
既然边沿触发,执行效率高,但是又不能读完数据该怎么办呢?
在ET模式下,一般会设置非阻塞io
当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符
所以ET所以循环处理,保证能将数据读取完毕,即同时要保证非阻塞IO,不然最后会被阻塞
也就是说,
在当前没有可读取数据的情况下
- 如果是阻塞io,recv()会阻塞
- 如果是非阻塞io,recv()会返回-1
现在 沿用例子2的代码
对新添加的客户端的clientfd设置为非阻塞
recv的外面设置了一层while(1)循环
在非阻塞情况下,如果recv没有收到数据就会返回-1,因此if(n<0) break,表明数据读取完成了。
然后运行测试,让客户端再次发送数据
可以发现完整的都运行出来了。
另外,如果不设置非阻塞(也就是 阻塞模式)
由于 recv不可读的时候会阻塞(而不会像非阻塞那样输出-1),导致下面死循环在while(1)内,虽然当前客户端可以再次发送数据,但是其他客户端就不能再连入服务器了。
完整代码
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<string.h> #include<sys/socket.h> #include<sys/epoll.h> #include<arpa/inet.h> #include<netinet/in.h> #include<fcntl.h> #define EPOLL_SIZE 1024 #define BUFFER_SIZE 4096 int main(int argc,char** argv){ int listenfd=socket(AF_INET,SOCK_STREAM,0); sockaddr_in serveraddr; memset(&serveraddr,0,sizeof(sockaddr_in)); serveraddr.sin_family=AF_INET; serveraddr.sin_port=htons(8888); serveraddr.sin_addr.s_addr=htonl(INADDR_ANY); bind(listenfd,(sockaddr*)&serveraddr,sizeof(sockaddr_in)); listen(listenfd,10); int epfd=epoll_create(1); epoll_event events[EPOLL_SIZE]={0}; epoll_event ev; ev.data.fd=listenfd; ev.events=EPOLLIN; epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); char buffer[BUFFER_SIZE]={0}; while(1){ int nready=epoll_wait(epfd,events,EPOLL_SIZE,5); if(nready==-1) continue; for(int i=0;i<nready;i++){ int fd=events[i].data.fd; if(fd==listenfd){ sockaddr_in clientaddr; memset(&clientaddr,0,sizeof(sockaddr_in)); socklen_t clientLen=sizeof(sockaddr_in); int clientfd=accept(listenfd,(sockaddr*)&clientaddr,&clientLen); fcntl(clientfd,F_SETFL,O_NONBLOCK);//设置非阻塞 ev.data.fd=clientfd; ev.events=EPOLLIN|EPOLLET; epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev); } else { while(1){ int n=recv(fd,buffer,5,0); if(n<0) break; if(n==0){ ev.data.fd=fd; ev.events=EPOLLIN; epoll_ctl(epfd,EPOLL_CTL_DEL,fd,&ev); close(fd); break; } else if(n>0){ printf("Recv:%s\n",buffer); } } } } } return 0; }