I/O多路复用详解

转自:http://konglingchun.is-programmer.com/posts/12146.html

 

要想完全理解I/O多路复用,需先要了解I/O模型:

一、五种I/O模型

1、阻塞I/O模型

     最流行的I/O模型是阻塞I/O模型,缺省情形下,所有套接口都是阻塞的。我们以数据报套接口为例来讲解此模型(我们使用UDP而不是TCP作为例子的原因在于就UDP而言,数据准备好读取的概念比较简单:要么整个数据报已经收到,要么还没有。然而对于TCP来说,诸如套接口低潮标记等额外变量开始活动,导致这个概念变得复杂)。

     进程调用recvfrom,其系统调用直到数据报到达且被拷贝到应用进程的缓冲区中或者发生错误才返回,期间一直在等待。我们就说进程在从调用recvfrom开始到它返回的整段时间内是被阻塞的。

2、非阻塞I/O模型

      进程把一个套接口设置成非阻塞是在通知内核:当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。也就是说当数据没有到达时并不等待,而是以一个错误返回。

3、I/O复用模型

     调用select或poll,在这两个系统调用中的某一个上阻塞,而不是阻塞于真正I/O系统调用。 阻塞于select调用,等待数据报套接口可读。当select返回套接口可读条件时,调用recevfrom将数据报拷贝到应用缓冲区中。

4、信号驱动I/O模型

     首先开启套接口信号驱动I/O功能, 并通过系统调用sigaction安装一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据报准备好被读时,就为该进程生成一个 SIGIO信号。随即可以在信号处理程序中调用recvfrom来读数据报,井通知主循环数据已准备好被处理中。也可以通知主循环,让它来读数据报。

5、异步I/O模型

     告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核拷贝到用户自己的缓冲区)通知我们。这种模型与信号驱动模型的主要区别是:
           信号驱动I/O:由内核通知我们何时可以启动一个I/O操作,
           异步I/O模型:由内核通知我们I/O操作何时完成。

 

二、I/O复用的典型应用场合:

 1、当客户处理多个描述字(通常是交互式输入和网络套接口)时,必须使用I/O复用。

 2、如果一个服务器要处理多个服务或者多个协议(例如既要处理TCP,又要处理UDP),一般就要使用I/O复用。

 

三、支持I/O复用的系统调用

     目前支持I/O复用的系统调用有select、pselect、poll、epoll:

1、select函数

     该函数允许进程指示内核等待多个事件中的任何一个发生,并仅在有一个或多个事件发生或经历一段指定的时间后才唤醒它。

格式为:

1 #include <sys/select.h>
2 #include <sys/time.h>
3  
4 int select( int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
5             
6           返回:就绪描述字的正数目,0-超时,-1-出错

    我们从该函数的最后一个参数开始介绍,它告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。

         struct timeval{

                   long tv_sec;   //seconds

                   long tv_usec;  //microseconds

       };

这个参数有三种可能:

(1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,我们把该参数设置为空指针。

(2)等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。

(3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。

    中间的三个参数readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述字。如果我们对某一个的条件不感兴趣, 就可以把它设为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:

          void FD_ZERO(fd_set *fdset);           //清空集合

          void FD_SET(int fd, fd_set *fdset);   //将一个给定的文件描述符加入集合之中

          void FD_CLR(int fd, fd_set *fdset);   //将一个给定的文件描述符从集合中删除

          int FD_ISSET(int fd, fd_set *fdset);   // 检查集合中指定的文件描述符是否可以读写 ?

目前支持的异常条件只有两个:

(1)某个套接口的带外数据的到达。

(2)某个已置为分组方式的伪终端存在可从其主端读取的控制状态信息。

    第一个参数maxfdp1指定待测试的描述字个数,它的值是待测试的最大描述字加1(因此我们把该参数命名为maxfdp1),描述字0、1、2...maxfdp1-1均将被测试。

 一个应用select的例子:

001 /**
002    *TCP回射服务器客户端程序
003    */
004 #include <stdio.h>
005 #include <stdlib.h>
006 #include <unistd.h>
007 #include <sys/socket.h>
008 #include <sys/types.h>
009 #include <netinet/in.h>
010 #include <netdb.h>
011 #include <string.h>
012 #include <math.h>
013 #include <sys/select.h>
014 #include <sys/time.h>
015  
016 #define SERVER_PORT 3333  //服务器端口号
017  
018 void str_cli( FILE *fp, int sockfd)
019 {
020      int maxfdp1, stdineof;
021      fd_set rset;
022      char buf[BUFSIZ];
023      int n;
024  
025      stdineof = 0;
026      FD_ZERO(&rset);
027  
028      while (1)
029      {
030          if ( stdineof == 0 )
031              FD_SET(fileno(fp),&rset);
032          FD_SET(sockfd, &rset);
033  
034          maxfdp1 = ((fileno(fp) > sockfd) ? fileno(fp) : sockfd) + 1;
035  
036          select(maxfdp1, &rset, NULL, NULL, NULL);
037  
038          if ( FD_ISSET(sockfd, &rset) )
039          {
040              if ( (n = read(sockfd, buf, BUFSIZ)) == 0 )
041                  if ( stdineof == 1 )
042                      return ;
043                  else
044                      perror ( "server terminated prematurely" );
045              write(fileno(stdout), buf, n);
046          }
047  
048          if ( FD_ISSET(fileno(fp), &rset))
049          {
050              if ( (n = read(fileno(fp), buf, BUFSIZ)) == 0 )
051              {
052                  stdineof = 1;
053                  shutdown(sockfd, SHUT_WR);
054                  FD_CLR(fileno(fp), &rset);
055                  continue ;
056              }
057              write(sockfd, buf, n);
058          }
059      }
060 }
061  
062 int main( int argc, char *argv[])
063 {
064      int sockfd[5];
065      struct sockaddr_in servaddr;
066      struct hostent  *hp;
067      char buf[BUFSIZ];
068  
069      if ( argc != 2 )
070      {
071          printf ( "Please input %s <hostname>/n" , argv[0]);
072          exit (1);
073      }
074       
075      int i;
076      for (i = 0; i < 5; ++i)
077      {
078  
079          //创建socket
080          if ( (sockfd[i] = socket(AF_INET, SOCK_STREAM,0)) < 0 )
081          {
082              printf ( "Create socket error!/n" );
083              exit (1);
084          }
085  
086          //设置服务器地址结构
087          bzero(&servaddr, sizeof (servaddr));
088          servaddr.sin_family = AF_INET;
089          if ( (hp = gethostbyname(argv[1])) != NULL )
090          {
091              bcopy(hp->h_addr, ( struct sockaddr*)&servaddr.sin_addr, hp->h_length);
092          }
093          else if (inet_aton(argv[1], &servaddr.sin_addr) < 0 )
094          {
095              printf ( "Input Server IP error!/n" );
096              exit (1);
097          }
098          servaddr.sin_port = htons(SERVER_PORT);
099  
100          //连接服务器
101          if ( connect(sockfd[i],( struct sockaddr*)&servaddr, sizeof (servaddr)) < 0 )
102          {
103              printf ( "Connect server failure!/n" );
104              exit (1);
105          }
106      }
107      str_cli(stdin, sockfd[0]);
108  
109      exit (0);
110 }

 

注:本章内容摘自<Unix 网络编程>第六章。

 2、pselect函数

     pselect函数是由POSIX发明的,如今许多Unix变种都支持它。 

1 #include <sys/select.h>
2 #include <signal.h>
3 #include <time.h>
4  
5 int pselect( int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timespec *timeout, const sigset_t *sigmask);
6                 返回:就绪描述字的个数,0-超时,-1-出错

pselect相对于通常的select有两个变化:

1、pselect使用timespec结构,而不使用timeval结构。timespec结构是POSIX的又一个发明。

     struct timespec{

            time_t tv_sec;     //seconds

            long    tv_nsec;    //nanoseconds

     };

     这两个结构的区别在于第二个成员:新结构的该成员tv_nsec指定纳秒数,而旧结构的该成员tv_usec指定微秒数。

2、pselect函数增加了第六个参数:一个指向信号掩码的指针。该参数允许程序先禁止递交某些信号,再测试由这些当前被禁止的信号处理函数设置的全局变量,然后调用pselect,告诉它重新设置信号掩码。

      关于第二点,考虑下面的例子,这个程序的SIGINT信号处理函数仅仅设置全局变量intr_flag并返回。如果我们的进程阻塞于select调用,那 么从信号处理函数的返回将导致select返回EINTR错误。然而调用select时,代码看起来大体如下:

01 if ( intr_flag )
02     handle_intr();
03  
04 if ( (nready = select(...)) < 0 )
05 {
06     if ( errno == EINTR )
07     {
08        if ( intr_flag )
09           handle_intr();
10     }
11     ...
12 }

    问题是在测试intr_flag和调用select之间如果有信号发生,那么要是select永远阻塞,该信号就会丢失。有了pselect后,我们可以如下可靠地编写这个例子的代码:

01 sigset_t newmask, oldmask, zeromask;
02  
03 sigemptyset(&zeromask);
04 sigemptyset(&newmask);
05 sigaddset(&newmask, SIGINT);
06  
07 sigprocmask(SIG_BLOCK, &newmask, &oldmask); //block SIGINT
08 if (intr_flag)
09     handle_intr();
10  
11 if ( (nready = pselect(...,&zeromask)) < 0 )
12 {
13      if ( errno == EINTR)
14      {
15         if (intr_flag)
16             handle_intr();
17      }
18      ...
19 }

       在测试intr_flag变量之前,我们阻塞SIGINT。当pselect被调用时,它先以空集(zeromask)取代进程的信号掩码,再检查描述 字,并可能进入睡眠。然而当pselect函数返回时,进程的信号掩码又被重置为调用pselect之前的值(即SIGINT被阻塞)。

 

 3、poll函数

      poll函数起源于SVR3,最初局限于流设备。SVR4取消了这种限制,允许poll工作在任何描述字上。poll提供的功能与select类似,不过在处理流设备时,它能够提供额外的信息。

1 #include <poll.h>
2  
3 int poll( struct pollfd *fdarray, unsigned long nfds, int timeout);
4                 返回:就绪描述字的个数,0-超时,-1-出错

      第一个参数是指向一个结构数组第一个元素的指针。每个数组元素都是一个pollfd结构,用于指定测试某个给定描述字fd的条件。

           struct pollfd{

                    int fd;              //descriptor to check

                    short events;    //events of interest on fd

                    short revents;   //events that occurred on fd 

            };

       要测试的条件由events成员指定,而返回的结果则在revents中存储。常用条件及含意说明如下:

poll函数可用的测试值
常量 说明
POLLIN普通或优先级带数据可读
POLLRDNORM普通数据可读
POLLRDBAND优先级带数据可读
POLLPRI高优先级数据可读
POLLOUT普通数据可写
POLLWRNORM普通数据可写
POLLWRBAND优先级带数据可写
POLLERR发生错误
POLLHUP发生挂起
POLLNVAL描述字不是一个打开的文件

 注意:后三个只能作为描述字的返回结果存储在revents中,而不能作为测试条件用于events中。

 

     第二个参数nfds是用来指定数组fdarray的长度。

     最后一个参数timeout是指定poll函数返回前等待多长时间。它的取值如下:

timeout值 说明
INFTIM永远等待
0立即返回,不阻塞进程
>0等待指定数目的毫秒数

    一个使用poll的网络程序例子:

001 /**
002    *TCP回射服务器的服务端程序
003    */
004 #include <stdio.h>
005 #include <stdlib.h>
006 #include <unistd.h>
007 #include <sys/socket.h>
008 #include <sys/types.h>
009 #include <netinet/in.h>
010 #include <netdb.h>
011 #include <string.h>
012 #include <errno.h>
013 #include <poll.h>   //for poll
014  
015 #define LISTENQ 1024
016 #define MAXLINE 1024
017 #define OPEN_MAX 50000
018 #define SERVER_PORT 3333 
019  
020 #ifndef INFTIM     /*按照书上解释:POSIX规范要求INFTIM在头文件<poll.h>中定义,不过*/
021 #define INFTIM -1  /*许多系统仍然把它定义在头文件<sys/stropts.h>中,但是经过我的测试*/
022 #endif             /*即使都包含这两个文件,编译器也找不到,不知何解。索性自己定义了。*/
023  
024 int main( int argc, char *argv[])
025 {
026      int i, maxi, listenfd, connfd, sockfd;
027      int nready;
028      ssize_t n;
029      socklen_t clilen;
030      struct sockaddr_in servaddr, cliaddr;
031      struct hostent  *hp;
032      char buf[BUFSIZ];
033      struct pollfd client[OPEN_MAX]; /*用于poll函数第一个参数的数组*/
034  
035      if ( argc != 2 )
036      {
037          printf ( "Please input %s <hostname>/n" , argv[0]);
038          exit (1);
039      }
040       
041      //创建socket
042      if ( (listenfd = socket(AF_INET, SOCK_STREAM,0)) < 0 )
043      {
044          printf ( "Create socket error!/n" );
045          exit (1);
046      }
047  
048      //设置服务器地址结构
049      bzero(&servaddr, sizeof (servaddr));
050      servaddr.sin_family = AF_INET;
051      if ( (hp = gethostbyname(argv[1])) != NULL )
052      {
053          bcopy(hp->h_addr, ( struct sockaddr*)&servaddr.sin_addr, hp->h_length);
054      }
055      else if (inet_aton(argv[1], &servaddr.sin_addr) < 0 )
056      {
057          printf ( "Input Server IP error!/n" );
058          exit (1);
059      }
060      servaddr.sin_port = htons(SERVER_PORT);
061  
062      //绑定地址
063      if ( bind(listenfd, ( struct sockaddr*)&servaddr, sizeof (servaddr)) < 0 )
064      {
065          printf ( "IPaddress bound failure!/n" );
066          exit (1);
067      }
068       
069      //开始监听
070      listen(listenfd, LISTENQ);
071  
072      client[0].fd = listenfd;         /*将数组中的第一个元素设置成监听描述字*/
073  
074      client[0].events = POLLIN;  /*将测试条件设置成普通或优先级带数据可读,此处书中为POLLRDNORM,
075                                        但是怎么也编译不过去 ,编译器就是找不到,所以就临时改成了POLLIN这个条件,
076                                        希望以后能弄清楚。
077                                       */
078  
079      for (i = 1;i < OPEN_MAX; ++i)     /*数组中的其它元素将暂时设置成不可用*/
080          client[i].fd = -1;
081      maxi = 0;
082  
083      while (1)
084      {
085          nready = poll(client, maxi+1,INFTIM); //将进程阻塞在poll上
086          if ( client[0].revents & POLLIN /*POLLRDNORM*/ ) /*先测试监听描述字*/
087          {
088              connfd = accept(listenfd,( struct sockaddr*)&servaddr, &clilen);
089              for (i = 1; i < OPEN_MAX; ++i)
090                  if ( client[i].fd < 0 )
091                  {
092                      client[i].fd = connfd;  /*将新连接加入到测试数组中*/
093                      client[i].events = POLLIN; //POLLRDNORM; /*测试条件普通数据可读*/
094                      break ;
095                  }
096              if ( i == OPEN_MAX )
097              {
098                  printf ( "too many clients" ); //连接的客户端太多了,都达到最大值了
099                  exit (1);
100              }
101  
102              if ( i > maxi )
103                  maxi = i;  //maxi记录的是数组元素的个数
104  
105              if ( --nready <= 0 )
106                  continue ;   //如果没有可读的描述符了,就重新监听连接
107          }
108  
109          for (i = 1; i <= maxi; i++)  /*测试除监听描述字以后的其它连接描述字*/
110          {
111              if ( (sockfd = client[i].fd) < 0) /*如果当前描述字不可用,就测试下一个*/
112                  continue ;
113  
114              if (client[i].revents & (POLLIN /*POLLRDNORM*/ | POLLERR)) /*如果当前描述字返回的是普通数据可读或出错条件*/
115              {
116                  if ( (n = read(sockfd, buf, MAXLINE)) < 0) //从套接口中读数据
117                  {
118                      if ( errno == ECONNRESET) //如果连接断开,就关闭连接,并设当前描述符不可用
119                      {
120                          close(sockfd);
121                          client[i].fd = -1;
122                      }
123                      else
124                          perror ( "read error" );
125                  }
126                  else if (n == 0) //如果数据读取完毕,关闭连接,设置当前描述符不可用
127                  {
128                      close(sockfd);
129                      client[i].fd = -1;
130                  }
131                  else
132                      write(sockfd, buf, n); //打印数据
133  
134                  if (--nready <= 0)
135                      break ;
136                   
137              }
138          }
139      }
140  
141      exit (0);
142 }

 

 注:本章内容摘自<Unix 网络编程>第六章。

 4、epoll 

    在linux的网络编程中,很长的一段时间都在使用select来做事件触发。然而select逐渐暴露出了一些缺陷,使得linux不得不在新的内核中 寻找出替代方案,那就是epoll。其实,epoll与select原理类似,只不过,epoll作出了一些重大改进,即:

      a、当它们所监听的集合中有状态发生改变时,select需要循环检查整个集合,才能确定那个文件描述符状态发生改变,进而进行操作;而epoll在添加 文件描述符到集合时,已经绑定了该文件描述符的对应函数,因此,当该文件描述符状态改变时,不需要循环查询整个集合,因而将复杂度由0(n)将为o (1),性能得到几何量级的提高,尤其是在大量连接的情况下。

       b、再有,select所监听的描述字最大数目是有一定限制的,由FD_SETSIZE设置,通常是1024。对于那些需要支持的上万连接数目的web服 务器来说显然是太少了,尽管可以通过修改头文件再重编译内核来扩大这个数目,不过资料也同时指出这样会带来网络效率的下降。(详细请参考:epoll 相对于poll的优点

epoll的接口非常简单,一共就三个函数:

(1)、int epoll_create(int size);

     创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。需要注意的是,当创建好epoll句柄后,epoll本身就占用一个fd值,所以用完后必须调用close()关闭,以防止fd被耗尽。


(2)、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

     epoll的事件注册函数,第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:

            EPOLL_CTL_ADD:注册新的fd到epfd中;

            EPOLL_CTL_MOD:修改已经注册的fd的监听事件;

            EPOLL_CTL_DEL:从epfd中删除一个fd;

第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事件,struct epoll_event结构如下:

            struct epoll_event{

                      __uint32_t events; //epoll events

                     epoll_data_t data;   //user data variable

            };

events可以是以下几个宏的集合:

     EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭);

     EPOLLOUT:表示对应的文件描述符可以写;

     EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);

     EPOLLERR:表示对应的文件描述符发生错误;

     EPOLLHUP:表示对应的文件描述符被挂断;

     EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。

     EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加

                                入到EPOLL队列里。

(3)、int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

     等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有 说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

      令人高兴的是,2. 6内核的epoll比其2. 5开发版本的/ dev/ epoll简洁了许多,所以,大部分情况下,强大的东西往往是简单的。唯一有点麻烦是epoll有2种工作方式: LT和ET。

       LT( level triggered) 是缺省的工作方式,并且同时支持block和no- block socket. 在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/ poll都是这种模型的代表.

       ET ( edge- triggered) 是高速工作方式,只支持no- block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了( 比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作( 从而导致它再次变成未就绪) ,内核不会发送更多的通知( only once) , 不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。

注:以上参考epoll精髓 一文。

一个epoll的例子:

001 #include <stdio.h>
002 #include <stdlib.h>
003 #include <errno.h>
004 #include <string.h>
005 #include <sys/types.h>
006 #include <netinet/in.h>
007 #include <sys/socket.h>
008 #include <sys/wait.h>
009 #include <unistd.h>
010 #include <arpa/inet.h>
011 #include <openssl/ssl.h>
012 #include <openssl/err.h>
013 #include <fcntl.h>
014 #include <sys/epoll.h>
015 #include <sys/time.h>
016 #include <sys/resource.h>
017  
018  
019 #define MAXBUF 1024
020 #define MAXEPOLLSIZE 10000
021  
022 /*
023 setnonblocking - 设置句柄为非阻塞方式
024 */
025 int setnonblocking( int sockfd)
026 {
027      if (fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)|O_NONBLOCK) == -1) {
028          return -1;
029      }
030      return 0;
031 }
032  
033 /*
034 handle_message - 处理每个 socket 上的消息收发
035 */
036 int handle_message( int new_fd)
037 {
038      char buf[MAXBUF + 1];
039      int len;
040      /* 开始处理每个新连接上的数据收发 */
041      bzero(buf, MAXBUF + 1);
042      /* 接收客户端的消息 */
043      len = recv(new_fd, buf, MAXBUF, 0);
044      if (len > 0)
045          printf
046              ( "%d接收消息成功:'%s',共%d个字节的数据/n" ,
047               new_fd, buf, len);
048      else {
049          if (len < 0)
050              printf
051                  ( "消息接收失败!错误代码是%d,错误信息是'%s'/n" ,
052                   errno , strerror ( errno ));
053          close(new_fd);
054          return -1;
055      }
056      /* 处理每个新连接上的数据收发结束 */
057      return len;
058 }
059 /************关于本文档********************************************
060 *filename: epoll-server.c
061 *purpose: 演示epoll处理海量socket连接的方法
062 *wrote by: zhoulifa(zhoulifa@163.com) 周立发(http://zhoulifa.bokee.com )
063 Linux爱好者 Linux知识传播者 SOHO族 开发者 最擅长C语言
064 *date time:2007-01-31 21:00
065 *Note: 任何人可以任意复制代码并运用这些文档,当然包括你的商业用途
066 * 但请遵循GPL
067 *Thanks to:Google
068 *Hope:希望越来越多的人贡献自己的力量,为科学技术发展出力
069 * 科技站在巨人的肩膀上进步更快!感谢有开源前辈的贡献!
070 *********************************************************************/
071 int main( int argc, char **argv)
072 {
073      int listener, new_fd, kdpfd, nfds, n, ret, curfds;
074      socklen_t len;
075      struct sockaddr_in my_addr, their_addr;
076      unsigned int myport, lisnum;
077      struct epoll_event ev;
078      struct epoll_event events[MAXEPOLLSIZE];
079      struct rlimit rt;
080  
081      if (argv[1])
082          myport = atoi (argv[1]);
083      else
084          myport = 7838;
085  
086      if (argv[2])
087          lisnum = atoi (argv[2]);
088      else
089          lisnum = 2;
090  
091      /* 设置每个进程允许打开的最大文件数 */
092      rt.rlim_max = rt.rlim_cur = MAXEPOLLSIZE;
093      if (setrlimit(RLIMIT_NOFILE, &rt) == -1) {
094          perror ( "setrlimit" );
095          exit (1);
096      }
097      else printf ( "设置系统资源参数成功!/n" );
098  
099      /* 开启 socket 监听 */
100      if ((listener = socket(PF_INET, SOCK_STREAM, 0)) == -1) {
101          perror ( "socket" );
102          exit (1);
103      } else
104          printf ( "socket 创建成功!/n" );
105  
106      setnonblocking(listener);
107  
108      bzero(&my_addr, sizeof (my_addr));
109      my_addr.sin_family = PF_INET;
110      my_addr.sin_port = htons(myport);
111      if (argv[3])
112          my_addr.sin_addr.s_addr = inet_addr(argv[3]);
113      else
114          my_addr.sin_addr.s_addr = INADDR_ANY;
115  
116      if (bind
117          (listener, ( struct sockaddr *) &my_addr, sizeof ( struct sockaddr))
118          == -1) {
119          perror ( "bind" );
120          exit (1);
121      } else
122          printf ( "IP 地址和端口绑定成功/n" );
123  
124      if (listen(listener, lisnum) == -1) {
125          perror ( "listen" );
126          exit (1);
127      } else
128          printf ( "开启服务成功!/n" );
129  
130      /* 创建 epoll 句柄,把监听 socket 加入到 epoll 集合里 */
131      kdpfd = epoll_create(MAXEPOLLSIZE);
132      len = sizeof ( struct sockaddr_in);
133      ev.events = EPOLLIN | EPOLLET;
134      ev.data.fd = listener;
135      if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, listener, &ev) < 0) {
136          fprintf (stderr, "epoll set insertion error: fd=%d/n" , listener);
137          return -1;
138      } else
139          printf ( "监听 socket 加入 epoll 成功!/n" );
140      curfds = 1;
141      while (1) {
142          /* 等待有事件发生 */
143          nfds = epoll_wait(kdpfd, events, curfds, -1);
144          if (nfds == -1) {
145              perror ( "epoll_wait" );
146              break ;
147          }
148          /* 处理所有事件 */
149          for (n = 0; n < nfds; ++n) {
150              if (events[n].data.fd == listener) {
151                  new_fd = accept(listener, ( struct sockaddr *) &their_addr,
152                                  &len);
153                  if (new_fd < 0) {
154                      perror ( "accept" );
155                      continue ;
156                  } else
157                      printf ( "有连接来自于: %s:%d, 分配的 socket 为:%d/n" , inet_ntoa(their_addr.sin_addr), ntohs(their_addr.sin_port), new_fd);
158  
159                  setnonblocking(new_fd);
160                  ev.events = EPOLLIN | EPOLLET;
161                  ev.data.fd = new_fd;
162                  if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, new_fd, &ev) < 0) {
163                      fprintf (stderr, "把 socket '%d' 加入 epoll 失败!%s/n" ,
164                              new_fd, strerror ( errno ));
165                      return -1;
166                  }
167                  curfds++;
168              } else {
169                  ret = handle_message(events[n].data.fd);
170                  if (ret < 1 && errno != 11) {
171                      epoll_ctl(kdpfd, EPOLL_CTL_DEL, events[n].data.fd,
172                                &ev);
173                      curfds--;
174                  }
175              }
176          }
177      }
178      close(listener);
179      return 0;
180 }

编译此程序用命令:

gcc -Wall epoll-server.c -o server

运行此程序需要具有管理员权限!

sudo ./server 7838 1

通过测试这一个服务器可能同时处理10000 -3 = 9997 个连接!

如果这是一个在线服务系统,那么它可以支持9997人同时在线,比如游戏、聊天等。

 

参考网址:http://zhoulifa.bokee.com/6081520.html

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值