1. 程式人生 > >Linux關於IO複用(epoll模型)

Linux關於IO複用(epoll模型)

在這篇開始之前,可以檢視前一篇對poll的概念的描述,這樣閱讀起這篇比較不困難。首先我們要知道,epoll模型和前面poll,select是有差別的,他實現的方法不大一樣,我們來看看下面的程式碼,為了和之前的poll,select進行區別,我們依舊採用C/S架構實現。伺服器程式碼不同,客戶端都是一樣的,好的,廢話不多說,先上程式碼,再進行分析。

伺服器程式碼:

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <sys/types.h>  
#include <sys/wait.h>
#include <sys/socket.h>  
#include <netinet/in.h> /* for struct sockaddr_in*/  
#include <sys/errno.h>  
#include <signal.h> 
#include <sys/select.h>
#include <sys/epoll.h>



//關於IO複用伺服器的epoll

#define LISTEN_SIZE 1024
void error_exit(char *name)
{
   perror(name);
   exit(-1);
}
int main(int argc,char *argv[])
{
   int sockfd=socket(AF_INET,SOCK_STREAM,0);
   if(sockfd<0)
   {
        error_exit("create error");
   }
   //繫結地址(ip和埠號)
   struct sockaddr_in svraddr;
   memset(&svraddr,0,sizeof(svraddr));
   svraddr.sin_family=AF_INET;
   svraddr.sin_addr.s_addr=INADDR_ANY;
   //svraddr.sin_addr.s_addr=inet_addr("127.0.0.1");第二種寫法
   svraddr.sin_port=htons(5555);
   int ret=bind(sockfd,(struct sockaddr*)&svraddr,sizeof(svraddr));
   if(ret<0)
   {
      error_exit("bind error");
   }
   //設定監聽引數back login 半連線數最大
   ret=listen(sockfd,1024);
   if(ret<0)
   {
      error_exit("listen error");
   }
    //建立epoll監聽集合
   int epfs=epoll_create(100);
   //新增svrfd
   struct epoll_event event;
   event.data.fd=sockfd;
   event.events=EPOLLIN;
   epoll_ctl(epfs,EPOLL_CTL_ADD,sockfd,&event);
   
   struct epoll_event ret_events[20]={0};
   int i=0;
   struct sockaddr_in removeaddr;
   memset(&removeaddr,0,sizeof(removeaddr));
   int addr_len=sizeof(removeaddr);
   char buf[1024]={0};
   while(1)
   {
        int nevent =epoll_wait(epfs,ret_events,20,-1);
        if(nevent<0)
        {
            error_exit("timeout\n");
        }
        else if(nevent==0)
        {
            printf("timeout\n");
            continue;
        }
        for(i=0;i<nevent;i++)
        {
            if(ret_events[i].data.fd== sockfd)
            {
                 int clientfd=accept(sockfd,(struct sockaddr*)&removeaddr,&addr_len);
                 if(clientfd<0)
                 {
                       error_exit("accept error");
                 }
                 printf("new connection fd %d\n",clientfd);
                 
                 event.data.fd=clientfd;
                 event.events=EPOLLIN;
                 epoll_ctl(epfs,EPOLL_CTL_ADD,clientfd,&event);
            }
            else if(ret_events[i].events&EPOLLIN)
            {
                 int rdsize=read(ret_events[i].data.fd,buf,1024);
                 if(rdsize<=0)
                 {
                     printf("close %d\n",ret_events[i].data.fd);
                     close(ret_events[i].data.fd);
                     event.data.fd=ret_events[i].data.fd;
                     event.events=EPOLLIN;
                     epoll_ctl(epfs,EPOLL_CTL_DEL,ret_events[i].data.fd,&event);      
                 }
                 else 
                 {
                     printf("read data %s\n",buf);
                 }
            }
        }
   }
   close(sockfd);
 }

客戶端依舊是前面一篇的程式碼:

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <sys/types.h>  
#include <sys/wait.h>
#include <sys/socket.h>  
#include <netinet/in.h> /* for struct sockaddr_in*/  
#include <sys/errno.h>  
#include <signal.h> 

//關於客戶端的socket
void error_exit(char *name)
{
   perror(name);
   exit(-1);
}
int main(int argc,char *argv[])
{
   if(argc<3)
   {
        printf("run program+ip+port\n");
        return-1;
   }
   int sockfd=socket(AF_INET,SOCK_STREAM,0);
   if(sockfd<0)
   {
        error_exit("create error");
   }
   //連線伺服器,設定伺服器的地址(ip和埠)
   struct sockaddr_in svraddr;
   memset(&svraddr,0,sizeof(svraddr));
   svraddr.sin_family=AF_INET;
   svraddr.sin_addr.s_addr= inet_addr(argv[1]);
   svraddr.sin_port=htons(atoi(argv[2]));
   int ret =connect(sockfd,(struct sockaddr *)&svraddr,sizeof(svraddr));
   if(ret<0)
   {
      error_exit("connect error");
   }

   write(sockfd,"hello",strlen("hello"));
   sleep(5); 
   close(sockfd);
   return 0;
}

先執行服務端,再執行客戶端程式碼:

以下是客戶端結果:

接下來檢視服務端結果:

epoll總結:epoll其實有兩種模式,EPOLLLT和EPOLLET,其實前面我們實現poll,select都是EPOLLLT模式,這篇也是。EPOLLLT為監聽模式,資料在緩衝區讀完,wait還是會觸發。而EPOLLET資料哪怕讀一點點,再次wait就不會觸發,因為ET會從不可讀變為可讀,也只有這一過程wait才會觸發,所以它只能觸發一次。

這時候我們會想,EPOLLET只觸發一次,那我們如何讀取它所有的資料,所以我們實現的時候必須一次性讀乾淨檔案,不然它的檔案描述符就不能用了。所以為了實現這樣的方法,就要對檔案設定為非阻塞。

下面為設定非阻塞程式碼:

void setnonblock(int fd)
{
   int flags = 0;
   fcntl(fd,F_GETFL,flags);
   flags |= O_NONBLOCK;
   fcntl(fd,F_SETFL,flags);
}

在設定非阻塞的時候,讀的時候的錯誤必須避免,我們進行判斷,以下是程式碼(errno為錯誤碼,EAGAIN為無資料讀的時候)

int n = 0;
while((nread = read(fd,buf+n,BUFSIZE-n))>0)
{
    n+=nread;
}
if(nread == -1 && errno != EAGAIN)
{
    perror("read error");
}

還有就是accept的錯誤,以下是程式碼

while(conn_sock=accept(listenfd,(struct sockaddr *)&removeaddr,(size_t *)addr_len))>0)
{
    handle_clinet(conn_sock);
}
if(conn_sock == -1)
{
    if(errno!=EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR)
              perror("error");
}

最後對select,poll,epoll三種實現方法進行總結:

select 監聽的檔案描述符為固定,哪怕產生一個,也要遍歷全部,效率不高

poll   雖然檔案描述符可以變大,但是是一個數組

epoll 每次讀,只返回讀的數量,效率高(而且ET效率比LT更高,不過ET編碼要求高,而且一次讀就要讀乾淨)