一、epoll简介

epoll是select和poll的增强版,

epoll使用一个文件描述符管理多个文件描述符


二、有关函数

1.

IO多路复用之epoll

描述:用来创建一个epoll的句柄,该句柄创建成功后会占用一个fd,所以在使用完之后需要关闭

返回值:成功返回该fd的值,失败返回-1

参数:自从linux2.6.8之后,size参数是被忽略的

2.

IO多路复用之epoll

描述:它是epoll的事件注册函数,对描述符fd所进行操作,它和select的不同在于select是在监听的时候告诉内核需要监听什么事件,epoll则事先注册监听事件的类型

返回值:成功返回0,失败返回-1

参数: epfd:epoll_create()的返回值

    op:要对fd进行的操作,可用3个宏表示

    EPOLL_CTL_ADD  //注册新的fd到epfd中

    EPOLL_CTL_MOD  //修改已经注册到eofd中fd的监听事件

    EPOLL_CTL_DEL  //从epfd中删除fd

    fd:要监听的文件描述符

    evevt:用来描述fd所关心的事件并告知内核,如果op=EPOLL_CTL_DEL,忽略该参数设置为NULL

    epoll_event的结构如下:

     IO多路复用之epoll

   events可以是以下几个宏的集合:
            EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
            EPOLLOUT表示对应的文件描述符可以写

     EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的
            EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
            EPOLLERR:表示对应的文件描述符发生错误
            EPOLLHUP:表示对应的文件描述符被挂断
            EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

3.

IO多路复用之epoll

描述:等待被监听事件的发生,并分别保存在events结构体中,类似于select的调用

返回值:大于0   成功,返回值为绪文件描述符的个数

     等于0   超时,超过设定的时间timeout

     小于0   失败,返回-1

参数: epfd:epoll_create()的返回值

    events:保存从内核得到发生事件的集合

    maxevents:告知内核,events的大小

    timeout:timeout的值有三种情况,单位毫秒

           timeout>0,超时后返回0;

           timeout=0,立即返回,即使没有一个描述符上有事件发生也不等待;

           timeout<0,阻塞等待,会一直等下去,直到有一个文件描述符上的事件发生

三、epoll工作原理

epoll先创建一个epoll_fd文件描述符

epoll_wait()返回的是就绪的文件描述符的个数,而不是描述符的总数,且内核会按顺序一次把描述符放在所以我们只需要从epoll制定的一个结构体数组中一次取得相应数量的文件描述符即可


四、epoll的2种工作模式

epoll对文件描述符的操作有两种工作模式

水平触发(LT):当有事件发生时,内核会通知应用程序,应用程序可以不立即处理,下一次epoll_wait的时候内核会再次通知应用程序此事件发生

边缘触发(ET):当有事件发生时,内核会通知应用程序,应用程序必须立即处理,否则下一次epoll_wait的时候内核不会再次通知应用程序此事件发生


epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口:

i 基于非阻塞文件句柄

ii 只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read()时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成


ET与LT的区别在于,当一个新的事件到来时,ET模式下当然可以从epoll_wait调用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字中没有新的事件再次到来时,在ET模式下是无法再次从epoll_wait调用中获取这个事件的。而LT模式正好相反,只要一个事件对应的套接字缓冲区还有数据,就总能从epoll_wait中获取这个事件


因为epoll工作在et模式是必须使用非阻塞套接字接口,因此,我们可以使用fcntl()函数来把置描述符设置成非阻塞的

IO多路复用之epoll

IO多路复用之epoll

返回值:失败返回-1

F_GETFL 对应于当前文件状态标志作为函数值返回

F_SETFL 将文件状态标志设置为第三个参数的值 (取为整型值),可以更改的几个标志是:                                             O_APPEND,O_NONBLOCK,O_SYNC和O_ASYNC

五、代码展示

server_epoll.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>                                                          
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <assert.h>
 
#define _MAX_FD_NUM_ 64
#define _BLOCKLOG_ 6
 
typedef struct buf_ptr
{
    int fd; 
    char ptr[1024];
}buf_t,*buf_p;
 
static int set_non_block(int fd)
{   
    int OLD_FL=fcntl(fd,F_GETFL);
    if(OLD_FL<0){
        perror("fcntl");
        return -1;
    }
    if(fcntl(fd,F_SETFL,OLD_FL|O_NONBLOCK)<0){
        perror("fcntl");
        return -2;
    }
    return 0;
}   
    
static void usage(const char *porc)
{   
    assert(porc);                                                               
    printf("Usage:%s [ip] [port]\n",porc);
}  

static int read_data(int fd,char *buf,ssize_t size)
{   
    assert(buf);
    int index=0;
    ssize_t ret=0;
    while((ret=read(fd,buf+index,size-index))>0){
        index+=ret;
    }
    if(ret<0){
        if(errno==EAGAIN){
            return 0;
        }else{
            perror("read");
            return -1;
        }
    }else if(ret==0){                                                           
        printf("client closed...\n");
        return -2;
    }
}    

static int bind_sock(const char *ip,int port)
{   
    assert(ip);
    int listen_sock=socket(AF_INET,SOCK_STREAM,0);                              
    if(listen_sock<0){
        perror("socket");
        return -3;
    }
    //使服务器不会进入TIMEWAIT状态
    int opt=1;
    setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
    
    struct sockaddr_in local;
    local.sin_family=AF_INET;
    local.sin_port=htons(port);
    local.sin_addr.s_addr=inet_addr(ip);
    if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local))<0){
        perror("bind");
        return -4;
    }
      
    return listen_sock;
}

static int server_handler(int listen_sock)
{   
    //创建epoll描述符
    int epoll_fd=epoll_create(256);
    if(epoll_fd<0){
        perror("epoll_create");
        return -6;
    }
    
    struct epoll_event ev;
    ev.events=EPOLLIN|EPOLLET;
    ev.data.fd=listen_sock;
    //添加监听描述符事件
    if(epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_sock,&ev)<0){                    
        perror("epoll_ctl");
        return -7;
    }
    
    struct epoll_event ev_set[_MAX_FD_NUM_];
    int timeout=5000;
//  int timeout=-1;

    char buf[1024];
    int ready_num=-1;
    int i=0;
    int done=0;
    while(!done){
        //等待事件的发生
        if((ready_num=epoll_wait(epoll_fd,ev_set,_MAX_FD_NUM_,timeout))<0){
            perror("epoll_wait");
        }else if(ready_num==0){
            printf("epoll timeout...\n");
        }else{
            for(i=0;i<ready_num;++i){
                //printf("EPOLLIN=%d,ready_num=%d\n",EPOLLIN,ready_num);
                if(ev_set[i].data.fd==listen_sock && (ev_set[i].events&EPOLLIN)){                                                                               
                    struct sockaddr_in client;
                    socklen_t len=sizeof(client);
                    //连接请求
                    int new_sock=accept(listen_sock,(struct sockaddr*)&client,&len);
                    if(new_sock<0){
                        perror("accept");
                        continue;
                    }
                    //设置套接字为非阻塞
                    if(set_non_block(new_sock)<0){
                        printf("set noblock failed...\n");
                        return -8;
                    }
    
                    ev.data.fd=new_sock;
                    ev.events=EPOLLIN|EPOLLET;
                    //添加套接字到epoll_fd
                    if(epoll_ctl(epoll_fd,EPOLL_CTL_ADD,new_sock,&ev)<0){
                        perror("epoll_ctl");                                    
                        return -9;
                    }
                    printf("get a connection:%s,%d\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
                }else if(ev_set[i].events&(EPOLLIN|EPOLLET)){
                    int fd=ev_set[i].data.fd;
                    buf_p mem=(buf_p)malloc(sizeof(buf_t));
                    if(!mem){
                        perror("malloc");
                        continue;
                    }
                    mem->fd=fd;
    
                    int ret=read_data(fd,mem->ptr,sizeof(mem->ptr)-1);
                    if(ret==0){                                                 
                        printf("client# %s",mem->ptr);
                        ev.events=EPOLLOUT|EPOLLET;
                        ev.data.ptr=mem;
                        epoll_ctl(epoll_fd,EPOLL_CTL_MOD,mem->fd,&ev);
                        //ev_set[i].data.ptr=(void *)mem;//can't operator to ev_set[]***************
                    }else if(ret==-2){ 
                        epoll_ctl(epoll_fd,EPOLL_CTL_DEL,mem->fd,NULL);
                        close(mem->fd);
                        free(mem);
                    }else{
                        continue; 
                    }
                }else if((ev_set[i].events&(EPOLLOUT)|EPOLLET)){
                    buf_p mem=(buf_p)ev_set[i].data.ptr;
                    int fd=mem->fd;
                    char *buf=mem->ptr;
                    if(write(fd,buf,strlen(buf))<0){
                      perror("write");
                      return -10;
                    }
                    //char *msg="HTTP/1.0 200 ok\r\n\r\nhello world:)\r\n";
                    //write(fd,msg,strlen(msg));
                    epoll_ctl(epoll_fd,EPOLL_CTL_DEL,fd,NULL);
                    close(fd);
                    free(mem);                                                  
                }
            }
        }
    }
    
}   
int main(int argc,char *argv[])
{   
    //检查参数
    if(argc!=3){
        usage(argv[0]);
        return 1;
    }
    
    char *ip=argv[1];
    int port=atoi(argv[2]);                                                     
    //创建和绑定监听套接字
    int listen_fd=bind_sock(ip,port);
    if(listen_fd<0){
        printf("create or bind failed!\n");
        return 2;
    }
    //服务器进行处理
    if(server_handler(listen_fd)<0){
        printf("server_handler failed!\n");
    }
    
    close(listen_fd);
    return 0;                                                                   
}

我们使用telnet 来进行客户端登陆,运行结果如下:

上面为服务器端结果,下面为客户端结果

IO多路复用之epoll

当服务器端运行程序时,还没有客户端连接,因此服务器端超过设定的timeout时间后会输出超时信息,而当有连接到达时,服务器端收到客户端发送的消息,回显给客户端后关闭了连接


把程序中

else if((ev_set[i].events&(EPOLLOUT)|EPOLLET))

改为

else if((ev_set[i].events&(EPOLLOUT)|EPOLLET)){
                    buf_p mem=(buf_p)ev_set[i].data.ptr;
                    int fd=mem->fd;
                    char *msg="HTTP/1.0 200 ok\r\n\r\nhello world:)\r\n";
                    write(fd,msg,strlen(msg));
                    epoll_ctl(epoll_fd,EPOLL_CTL_DEL,fd,NULL);
                    close(fd);
                    free(mem);       
}

也可以用自己写的客户端进行测试:

client_epoll.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>                                                        
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <netinet/in.h>
 
void usage(char *proc)
{
    printf("%s [ip] [port]\n",proc);
}
 
int creat_socket()
{
    int fd=socket(AF_INET,SOCK_STREAM,0);
    if(fd<0){
        perror("socket");
        exit(1);
    }   
    
    return fd; 
}

int main(int argc,char* argv[])
{   
    if(argc!=3){
        usage(argv[0]);
        exit(1);
    }
    
    int fd=creat_socket();
    
    int _port=atoi(argv[2]);
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;
    addr.sin_port=htons(_port);
    inet_aton(argv[1],&addr.sin_addr);
    socklen_t addrlen=sizeof(addr);
    if(connect(fd,(struct sockaddr*)&addr,addrlen)<0){                         
        perror("connect");
        exit(2);
    }
    
    char buf[1024];
    while(1){
        memset(buf,'\0',sizeof(buf));
        printf("Please Enter:");
        printf("%s",fgets(buf,sizeof(buf)-1,stdin));
        //if(send(fd,buf,strlen(buf),0)<0){
        if(send(fd,buf,sizeof(buf)-1,0)<0){
               perror("send");
               continue;
        }
        ssize_t _size=recv(fd,buf,sizeof(buf)-1,0);
        if(_size>0){
             buf[_size]='\0';
             printf("echo->%s",buf);                                           
        }
    }
   
    return 0;
}


我们使用浏览器来连接服务器端,结果如下:

IO多路复用之epoll

服务器端会受到浏览器发的消息,

第一部分(第一行)为请求行,格式为:Method Request-URI HTTP-Version CRLF  

第二部分为请求消息报头

接下来有一个空行

第三部分为请求正文


服务器给浏览器发送HTTP响应,也是由三个部分组成分别是:

第一部分为状态行

第二部分为消息报头

第三部分为响应正文