本章节主要针对于epoll进行一些补充,游双上很多部分并没有给出详尽的解释,并且有一点搬运文档的感觉,针对于部分进行补全;
关于epoll的两种触发方式:
最主要的问题是这问题,以及他们和阻塞的关系及异同;
epoll分为两种触发方式:TL水平触发以及EL边沿触发;
从总体上来看,两者无非是通知的功能不同:
- TL:当socket的缓冲区内只要有数据,就会一直发出提示请求;
- EL:只有当socket有数据写入的时候,才会发出提示请求;
总而言之,针对于TL来说,只要缓冲区里有数据没有取完,就会一直提示,直到取完为止,而EL是只提示一次,取不取完都不会再提示;
这里有两点会在后续理解和代码中会造成很大的理解困惑:
- 为什么EL要和非阻塞结合起来;
- 阻塞和epoll的关系到底是什么样的;
为什么EL要和非阻塞结合:
这里翻阅了基本的知乎以及相关博客,发现主要还是因为业务场景造成的;
假如有以下场景:
A给B发送一个100B的消息,但是后50B里面要求B回应A;
假若B采用EL模式,只读了50B,忘了还剩下50B在B的缓冲区里,由于EL只会提示一次,此时就不会得知自己还要回复A;
所以A就一直等,等到死,此时就发生了“死锁”,A陷入盲等;
因此,为了规避该风险,ET模式下会采用非阻塞IO,当得知一个socket可读后,就会采用非阻塞读,一直读,直到读到“EGAIN”错误;
阻塞和epoll的关系到底是怎么样的:
这个就是另外一个派生出来的点,为什么LT模式不用非阻塞呢;
原因是从宏观层面来上看,唯一需要阻塞的只有epoll_wait;
epoll_wait作为主线程一直阻塞,直到红黑树内有socket就绪打入就绪列表,才会唤醒主线程,对返回的socket列表进行操作;
对于LT模式下,阻塞与否并没有太大关系,如果没有取完,在下一次epoll_wait中也会可以再取;
而对于ET模式,则不阻塞很容易陷入client和server死锁盲等状态;
具体例子:
这里说一下游双书上的示例:
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<stdlib.h>
#include<sys/epoll.h>
#include<pthread.h>
#include<fcntl.h>
#define MAX_EVENT_MUMBER 1024
#define BUFFER_SIZE 10
int setnonblocking(int fd){
int old_option=fcntl(fd,F_GETFL);//Get the old statmengt of the file
int new_option=old_option|O_NONBLOCK;//The new sign;
fcntl(fd,F_SETFL,new_option);//Set the new statment to this file
return old_option;
}
void addfd(int epollfd,int fd,bool enable_et){
epoll_event event;
event.data.fd=fd;
event.events=EPOLLIN;
if(enable_et){
event.events|=EPOLLET;
}
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&event);
setnonblocking(fd);
}
void lt(epoll_event* events,int number,int epollfd,int listenfd){
char buf[BUFFER_SIZE];
for(int i=0;i<number;i++){
int sockfd=events[i].data.fd;
if(sockfd==listenfd){
struct sockaddr_in client_address;
socklen_t clinet_addrlength=sizeof(client_address);
int connfd=accept(listenfd,(struct sockaddr*)&client_address,&clinet_addrlength);
addfd(epollfd,connfd,false);//not ET,LT
}else if(events[i].events&EPOLLIN){
printf("event trigger once\n");
memset(buf,'\0',BUFFER_SIZE);
int ret=recv(sockfd,buf,BUFFER_SIZE-1,0);
if(ret<=0){
close(sockfd);
continue;
}
printf("get %d bytes of content:%s\n",ret,buf);
}else{
printf("something else happended\n");
}
}
}
void et(epoll_event* events,int number,int epollfd,int listenfd){
char buf[BUFFER_SIZE];
for(int i=0;i<number;i++){
int sockfd=events[i].data.fd;
if(sockfd==listenfd){
struct sockaddr_in client_address;
socklen_t clinet_addrlength=sizeof(client_address);
int connfd=accept(listenfd,(struct sockaddr*)&client_address,&clinet_addrlength);
addfd(epollfd,connfd,true);//ET mode
}else if(events[i].events&EPOLLIN){
printf("event trigger once\n");
while(1){
memset(buf,'\0',BUFFER_SIZE);
int ret=recv(sockfd,buf,BUFFER_SIZE-1,0);
if(ret<0){
if((errno==EAGAIN)||(errno==EWOULDBLOCK)){
//data is all
printf("read later\n");
break;
}
}else if(ret==0){
close(sockfd);
break;
}else{
printf("get %d bytes of content: %s\n",ret,buf);
}
}
//printf("get %d bytes of content:%s\n",ret,buf);
}else{
printf("something else happended\n");
}
}
}
int main(int argc,char* argv[]){
if(argc<=2){
printf("error argc");
return 1;
}
const char* ip=argv[1];
int port=atoi(argv[2]);
struct sockaddr_in address;
bzero(&address,sizeof(address));
address.sin_family=AF_INET;
inet_pton(AF_INET,ip,&address.sin_addr);
address.sin_port=htons(port);
int sock=socket(PF_INET,SOCK_STREAM,0);
assert(sock>=0);
int ret=bind(sock,(struct sockaddr*)&address,sizeof(address));
assert(ret!=-1);
ret=listen(sock,5);
assert(ret!=-1);
epoll_event events[MAX_EVENT_MUMBER];
int epollfd=epoll_create(5);
assert(epollfd!=-1);
addfd(epollfd,sock,true);
while(1){
int ret=epoll_wait(epollfd,events,MAX_EVENT_MUMBER,-1);
if(ret<0){
printf("epoll failure\n");
break;
}
lt(events,ret,epollfd,sock);
//et mode: et(events,ret,epollfd,sock);
}
return 0;
}
这里还有一个盲点,就是针对于socket接受的问题;
之前还迷惑为什么要进行判断:sockfd==listenfd
,后来发现自己对于socket创建理解有问题;
对于最初的bind操作,会将端口和listensocket进行绑定,作为监听socket;
当accept接受成功后,返回的其实是一个新的socket,用于标记已建立的连接;
但是对于新的连接,其实还是通过listensocket进行连接;
因此,将listensocket托管给epoll,之后就可以从内部判断是收到了新链接,还是已有socket收到了消息;