zoukankan      html  css  js  c++  java
  • Socket网络编程--epoll小结

      以前使用的用于I/O多路复用为了方便就使用select函数,但select这个函数是有缺陷的。因为它所支持的并发连接数是有限的(一般小于1024),因为用户处理的数组是使用硬编码的。这个最大值为FD_SETSIZE,这是在<sys/select.h>中的一个常量,它说明了最大的描述符数。但是对于大多数应用程序而言,这个数是够用的,而且有可能还是太大的,多数应用程序只使用3~10个描述符。而如今的网络服务器小小的都有几万的连接,虽然可以使用多线程多进程(也就有N*1024个)。但是这样处理起来既不方面,性能又低。

      同时期有I/O多路复用的还有一个poll函数,这个函数类似于select,但是其应用程序接口有所不用。原型如下

      #include <poll.h>
      int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);

      //返回值: 准备就绪的描述符数,若超时则返回0,出错返回-1

      如果只是考虑性能的话,poll()也是不合适的,尽管它可以支持较高的TCP并发连接数,但是由于其采用“轮询”机制(遍历数组而已),但并发数较高时,其运行效率相当低(如果有10k个连接,单用于轮询的时间就需要1~10ms了),同时还可能存在I/O事件分配不均,导致部分TCP连接上的I/O出现“饥饿”现象。基于种种原因在Linux 2.5.44版本后poll被epoll取代。
      支持一个进程打开最大数目的 socket 描述符(FD)。select 最不能忍受的是一个进程所打开的FD 是有一定限制的,由 FD_SETSIZE 设置,默认值是 2048。对于那些需要支持的上万连接数目的 IM 服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache 方案Process Per Connection,TPC方案 Thread Per Connection),不过虽然 linux 上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。不过 epoll 则没有这个限制,它所支持的 FD 上限是最大可以打开文件的数目,这个数字一般远大于 2048,举个例子,在 1GB 内存的机器上大约是 10 万左右,具体数目可以 cat /proc/sys/fs/file-max 察看,一般来说这个数目和系统内存关系很大。
    由于epoll这个函数是后增加上的,造成现在很少有资料提及到,我看了APUE,UNPv1等书都没有找到相关的函数原型。所以我只能从网络上抄一些函数原型过来了。

       epoll用到的所有函数都是在头文件sys/epoll.h中声明,有什么地方不明白或函数忘记了可以去看一下或者man epoll。epoll和select相比,最大不同在于:

      epoll返回时已经明确的知道哪个sokcet fd发生了事件,不用再一个个比对(轮询)。这样就提高了效率。select的FD_SETSIZE是有限制的,而epoll是没有限制的只与系统资源有关。

      epoll_create函数

    1 /* Creates an epoll instance.  Returns an fd for the new instance.
    2    The "size" parameter is a hint specifying the number of file
    3    descriptors to be associated with the new instance.  The fd
    4    returned by epoll_create() should be closed with close().  */
    5 extern int epoll_create (int __size) __THROW;

      该函数生成一个epoll专用的文件描述符。它其实是在内核申请空间,用来存放你想关注的socket fd上是否发生以及发生了什么事件。size就是你在这个epoll fd上能关注的最大socket fd数。这个数没有select的1024约束。

      epoll_ctl函数

     1 /* Manipulate an epoll instance "epfd". Returns 0 in case of success,
     2    -1 in case of error ( the "errno" variable will contain the
     3    specific error code ) The "op" parameter is one of the EPOLL_CTL_*
     4    constants defined above. The "fd" parameter is the target of the
     5    operation. The "event" parameter describes which events the caller
     6    is interested in and any associated user data.  */
     7 extern int epoll_ctl (int __epfd, int __op, int __fd,
     8                       struct epoll_event *__event) __THROW;
     9 /* Valid opcodes ( "op" parameter ) to issue to epoll_ctl(). */
    10 #define EPOLL_CTL_ADD 1 /* Add a file descriptor to the interface. */
    11 #define EPOLL_CTL_DEL 2 /* Remove a file descriptor from the interface. */
    12 #define EPOLL_CTL_MOD 3 /* Change file descriptor epoll_event structure. */

      该函数用于控制某个epoll文件描述符上的事件,可以注册事件,修改事件,删除事件。 

      参数: epfd:由 epoll_create 生成的epoll专用的文件描述符;
          op:要进行的操作例如注册事件,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修 改、EPOLL_CTL_DEL 删除

          fd:关联的文件描述符;
          event:指向epoll_event的指针;
          返回值:如果调用成功返回0,不成功返回-1

      用到的数据结构

     1 typedef union epoll_data
     2 {
     3   void *ptr;
     4   int fd;
     5   uint32_t u32;
     6   uint64_t u64;
     7 } epoll_data_t;
     8 
     9 struct epoll_event
    10 {
    11   uint32_t events;      /* Epoll events */
    12   epoll_data_t data;    /* User data variable */
    13 };

      设置实例

     1 struct epoll_event ev;
     2 //设置与要处理的事件相关的文件描述符
     3 ev.data.fd=listenfd;
     4 //设置要处理的事件类型
     5 ev.events=EPOLLIN|EPOLLET;
     6 //注册epoll事件
     7 epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
     8 //常用的事件类型:
     9 EPOLLIN :表示对应的文件描述符可以读;
    10 EPOLLOUT:表示对应的文件描述符可以写;
    11 EPOLLPRI:表示对应的文件描述符有紧急的数据可读
    12 EPOLLERR:表示对应的文件描述符发生错误;
    13 EPOLLHUP:表示对应的文件描述符被挂断;
    14 EPOLLET:表示对应的文件描述符有事件发生;
    15 //具体可以看<sys/epoll.h>

      epoll_wait函数

     1 /* Wait for events on an epoll instance "epfd". Returns the number of
     2    triggered events returned in "events" buffer. Or -1 in case of
     3    error with the "errno" variable set to the specific error code. The
     4    "events" parameter is a buffer that will contain triggered
     5    events. The "maxevents" is the maximum number of events to be
     6    returned ( usually size of "events" ). The "timeout" parameter
     7    specifies the maximum wait time in milliseconds (-1 == infinite).
     8    This function is a cancellation point and therefore not marked with
     9    __THROW.  */
    10 extern int epoll_wait (int __epfd, struct epoll_event *__events,
    11                        int __maxevents, int __timeout);
    12 
    13 
    14 /* Same as epoll_wait, but the thread's signal mask is temporarily
    15    and atomically replaced with the one provided as parameter.
    16    This function is a cancellation point and therefore not marked with
    17    __THROW.  */
    18 extern int epoll_pwait (int __epfd, struct epoll_event *__events,
    19                         int __maxevents, int __timeout,
    20                         __const __sigset_t *__ss);

      该函数用于轮询I/O事件的发生;
      参数: epfd:由epoll_create 生成的epoll专用的文件描述符;
          epoll_event:用于回传代处理事件的数组;
          maxevents:每次能处理的事件数;
          timeout:等待I/O事件发生的超时值(单位应该是ms);-1相当于阻塞,0相当于非阻塞。一般用-1即可

          返回值:返回发生事件数。如出错则返回-1。

       下面给一个man手册里面的例子

     1 #define MAX_EVENTS 10
     2 struct epoll_event ev, events[MAX_EVENTS];
     3 int listen_sock, conn_sock, nfds, epollfd;
     4 
     5 /* Set up listening socket, 'listen_sock' (socket(),
     6    bind(), listen()) */
     7 
     8 epollfd = epoll_create(10);
     9 if (epollfd == -1) {
    10     perror("epoll_create");
    11     exit(EXIT_FAILURE);
    12 }
    13 
    14 ev.events = EPOLLIN;
    15 ev.data.fd = listen_sock;
    16 if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
    17     perror("epoll_ctl: listen_sock");
    18     exit(EXIT_FAILURE);
    19 }
    20 
    21 for (;;) {
    22     nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
    23     if (nfds == -1) {
    24         perror("epoll_pwait");
    25         exit(EXIT_FAILURE);
    26     }
    27 
    28     for (n = 0; n < nfds; ++n) {
    29         if (events[n].data.fd == listen_sock) {
    30             conn_sock = accept(listen_sock,
    31                     (struct sockaddr *) &local, &addrlen);
    32             if (conn_sock == -1) {
    33                 perror("accept");
    34                 exit(EXIT_FAILURE);
    35             }
    36             setnonblocking(conn_sock);
    37             ev.events = EPOLLIN | EPOLLET;
    38             ev.data.fd = conn_sock;
    39             if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
    40                         &ev) == -1) {
    41                 perror("epoll_ctl: conn_sock");
    42                 exit(EXIT_FAILURE);
    43             }
    44         } else {
    45             do_use_fd(events[n].data.fd);
    46         }
    47     }
    48 }

      下面这个是引用mmz_xiaokong

    epoll函数
      常用模型的缺点
        如果不摆出来其他模型的缺点,怎么能对比出 Epoll 的优点呢。
      PPC/TPC 模型
      这两种模型思想类似,就是让每一个到来的连接一边自己做事去,别再来烦我 。只是 PPC 是为它开了一个进程,而 TPC 开了一个线程。可是别烦我是有代价的,它要时间和空间啊,连接多了之后,那么多的进程 / 线程切换,这开销就上来了;因此这类模型能接受的最大连接数都不会高,一般在几百个左右。
      select 模型
      1. 最大并发数限制,因为一个进程所打开的 FD (文件描述符)是有限制的,由 FD_SETSIZE 设置,默认值是 1024/2048 ,因此 Select 模型的最大并发数就被相应限制了。自己改改这个 FD_SETSIZE ?想法虽好,可是先看看下面吧 …
      2. 效率问题, select 每次调用都会线性扫描全部的 FD 集合,这样效率就会呈现线性下降,把 FD_SETSIZE 改大的后果就是,大家都慢慢来,什么?都超时了??!!
      3. 内核 / 用户空间 内存拷贝问题,如何让内核把 FD 消息通知给用户空间呢?在这个问题上 select 采取了内存拷贝方法。
      poll 模型
    基本上效率和 select 是相同的, select 缺点的 2 和 3 它都没有改掉。
      Epoll 的提升
      把其他模型逐个批判了一下,再来看看 Epoll 的改进之处吧,其实把 select 的缺点反过来那就是 Epoll 的优点了。
      1. Epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于 2048, 一般来说这个数目和系统内存关系很大 ,具体数目可以 cat /proc/sys/fs/file-max 察看。
      2. 效率提升, Epoll 最大的优点就在于它只管你“活跃”的连接 ,而跟连接总数无关,因此在实际的网络环境中, Epoll 的效率就会远远高于 select 和 poll 。
      3. 内存拷贝, Epoll 在这点上使用了“共享内存 ”,这个内存拷贝也省略了。
      Epoll 为什么高效
      Epoll 的高效和其数据结构的设计是密不可分的(以空间换时间),这个下面就会提到。
      首先回忆一下 select 模型,当有 I/O 事件到来时, select 通知应用程序有事件到了快去处理,而应用程序必须轮询所有的 FD 集合,测试每个 FD 是否有事件发生,并处理事件;代码像下面这样:

     1 int res = select(maxfd+1, &readfds, NULL, NULL, 120);
     2 if (res > 0)
     3 {
     4     for (int i = 0; i < MAX_CONNECTION; i++)
     5     {
     6         if (FD_ISSET(allConnection[i], &readfds))
     7         {
     8             handleEvent(allConnection[i]);
     9         }
    10     }
    11 }
    12 // if(res == 0) handle timeout, res < 0 handle error    

      Epoll 不仅会告诉应用程序有I/0 事件到来,还会告诉应用程序相关的信息,这些信息是应用程序填充的,因此根据这些信息应用程序就能直接定位到事件,而不必遍历整个FD 集合。

    1 int res = epoll_wait(epfd, events, 20, 120);
    2 for (int i = 0; i < res;i++)
    3 {
    4      handleEvent(events[n]);
    5 }

       下面用一个实例来说明

      client.cpp

     1 #include <stdio.h>
     2 #include <stdlib.h>
     3 #include <string.h>
     4 #include <netinet/in.h>
     5 #include <sys/types.h>
     6 #include <sys/socket.h>
     7 #include <netdb.h>
     8 #include <unistd.h>
     9 
    10 #define MAX_DATA_SIZE 4096
    11 #define SERVER_PORT 12138
    12 
    13 
    14 int main(int argc,char *argv[])
    15 {
    16     int sockfd;
    17     struct hostent * host;
    18     struct sockaddr_in servAddr;
    19     int pid;
    20     char sendBuf[MAX_DATA_SIZE],recvBuf[MAX_DATA_SIZE];
    21     int sendSize,recvSize;
    22 
    23     host=gethostbyname(argv[1]);
    24     if(host==NULL)
    25     {
    26         perror("get host error");
    27         exit(-1);
    28     }
    29 
    30     sockfd=socket(AF_INET,SOCK_STREAM,0);
    31     if(sockfd==-1)
    32     {
    33         perror("创建socket失败");
    34         exit(-1);
    35     }
    36 
    37     servAddr.sin_family=AF_INET;
    38     servAddr.sin_port=htons(SERVER_PORT);
    39     servAddr.sin_addr=*((struct in_addr *)host->h_addr);
    40     bzero(&(servAddr.sin_zero),8);
    41 
    42     if(connect(sockfd,(struct sockaddr *)&servAddr,sizeof(struct sockaddr_in))==-1)
    43     {
    44         perror("connect 失败");
    45         exit(-1);
    46     }
    47 
    48     if((pid=fork())<0)
    49     {
    50         perror("fork error");
    51     }
    52     else if(pid>0)
    53     {
    54         while(1)
    55         {
    56             fgets(sendBuf,MAX_DATA_SIZE,stdin);
    57             sendSize=send(sockfd,sendBuf,MAX_DATA_SIZE,0);
    58             if(sendSize<0)
    59                 perror("send error");
    60             memset(sendBuf,0,sizeof(sendBuf));
    61         }
    62     }
    63     else
    64     {
    65         while(1)
    66         {
    67             recvSize=recv(sockfd,recvBuf,MAX_DATA_SIZE,0);
    68             if(recvSize<0)
    69                 perror("recv error");
    70             printf("接收到的信息:%s",recvBuf);
    71             memset(recvBuf,0,sizeof(recvBuf));
    72         }
    73     }
    74     return 0;
    75 }

      server.cpp

      1 #include <stdio.h>
      2 #include <stdlib.h>
      3 #include <sys/socket.h>
      4 #include <sys/epoll.h>
      5 #include <string.h>
      6 #include <netinet/in.h>
      7 #include <string.h>
      8 #include <netdb.h>
      9 #include <arpa/inet.h>
     10 #include <unistd.h>
     11 
     12 #define SERVER_PORT 12138
     13 #define CON_QUEUE 20
     14 #define MAX_DATA_SIZE 4096
     15 #define MAX_EVENTS 500
     16 
     17 void AcceptConn(int sockfd,int epollfd);
     18 void Handle(int clientfd);
     19 
     20 int main(int argc,char *argv[])
     21 {
     22     struct sockaddr_in serverSockaddr;
     23     int sockfd;
     24 
     25     //创建socket
     26     if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
     27     {
     28         perror("创建socket失败");
     29         exit(-1);
     30     }
     31     serverSockaddr.sin_family=AF_INET;
     32     serverSockaddr.sin_port=htons(SERVER_PORT);
     33     serverSockaddr.sin_addr.s_addr=htonl(INADDR_ANY);
     34     bzero(&(serverSockaddr.sin_zero),8);
     35 
     36     int on=0;
     37     setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
     38 
     39     if(bind(sockfd,(struct sockaddr *)&serverSockaddr,sizeof(struct sockaddr))==-1)
     40     {
     41         perror("绑定失败");
     42         exit(-1);
     43     }
     44 
     45     if(listen(sockfd,CON_QUEUE)==-1)
     46     {
     47         perror("监听失败");
     48         exit(-1);
     49     }
     50 
     51     //epoll初始化
     52     int epollfd;//epoll描述符
     53     struct epoll_event eventList[MAX_EVENTS];
     54     epollfd=epoll_create(MAX_EVENTS);
     55     struct epoll_event event;
     56     event.events=EPOLLIN|EPOLLET;
     57     event.data.fd=sockfd;//把server socket fd封装进events里面
     58 
     59     //epoll_ctl设置属性,注册事件
     60     if(epoll_ctl(epollfd,EPOLL_CTL_ADD,sockfd,&event)<0)
     61     {
     62         printf("epoll 加入失败 fd:%d
    ",sockfd);
     63         exit(-1);
     64     }
     65 
     66     while(1)
     67     {
     68         int timeout=300;//设置超时;在select中使用的是timeval结构体
     69         //epoll_wait epoll处理
     70         //ret会返回在规定的时间内获取到IO数据的个数,并把获取到的event保存在eventList中,注意在每次执行该函数时eventList都会清空,由epoll_wait函数填写。
     71         //而不清除已经EPOLL_CTL_ADD到epollfd描述符的其他加入的文件描述符。这一点与select不同,select每次都要进行FD_SET,具体可看我的select讲解。
     72         //epoll里面的文件描述符要手动通过EPOLL_CTL_DEL进行删除。
     73         int ret=epoll_wait(epollfd,eventList,MAX_EVENTS,timeout);
     74 
     75         if(ret<0)
     76         {
     77             perror("epoll error
    ");
     78             break;
     79         }
     80         else if(ret==0)
     81         {
     82             //超时
     83             continue;
     84         }
     85 
     86         //直接获取了事件数量,给出了活动的流,这里就是跟selec,poll区别的关键 //select要用遍历整个数组才知道是那个文件描述符有事件。而epoll直接就把有事件的文件描述符按顺序保存在eventList中
     87         for(int i=0;i<ret;i++)
     88         {
     89             //错误输出
     90             if((eventList[i].events & EPOLLERR) || (eventList[i].events & EPOLLHUP) || !(eventList[i].events & EPOLLIN))
     91             {
     92                 printf("epoll error
    ");
     93                 close(eventList[i].data.fd);
     94                 exit(-1);
     95             }
     96 
     97             if(eventList[i].data.fd==sockfd)
     98             {
     99                 //这个是判断sockfd的,主要是用于接收客户端的连接accept
    100                 AcceptConn(sockfd,epollfd);
    101             }
    102             else //里面可以通过判断eventList[i].events&EPOLLIN 或者 eventList[i].events&EPOLLOUT 来区分当前描述符的连接是对应recv还是send
    103             {
    104                 //其他所有与客户端连接的clientfd文件描述符
    105                 //获取数据等操作
    106                 //如需不接收客户端发来的数据,但是不关闭连接。
    107                 //epoll_ctl(epollfd, EPOLL_CTL_DEL,eventList[i].data.fd,eventList[i]);
    108                 //Handle对各个客户端发送的数据进行处理
    109                 Handle(eventList[i].data.fd);
    110             }
    111         }
    112     }
    113 
    114     close(epollfd);
    115     close(sockfd);
    116     return 0;
    117 }
    118 
    119 void AcceptConn(int sockfd,int epollfd)
    120 {
    121     struct sockaddr_in sin;
    122     socklen_t len=sizeof(struct sockaddr_in);
    123     bzero(&sin,len);
    124 
    125     int confd=accept(sockfd,(struct sockaddr *)&sin,&len);
    126 
    127     if(confd<0)
    128     {
    129         perror("connect error
    ");
    130         exit(-1);
    131     }
    132 
    133     //把客户端新建立的连接添加到EPOLL的监听中
    134     struct epoll_event event;
    135     event.data.fd=confd;
    136     event.events=EPOLLIN|EPOLLET;
    137     epoll_ctl(epollfd,EPOLL_CTL_ADD,confd,&event);
    138     return ;
    139 }
    140 
    141 void Handle(int clientfd)
    142 {
    143     int recvLen=0;
    144     char recvBuf[MAX_DATA_SIZE];
    145     memset(recvBuf,0,sizeof(recvBuf));
    146     recvLen=recv(clientfd,(char *)recvBuf,MAX_DATA_SIZE,0);
    147     if(recvLen==0)
    148         return ;
    149     else if(recvLen<0)
    150     {
    151         perror("recv Error");
    152         exit(-1);
    153     }
    154     //各种处理
    155     printf("接收到的数据:%s 
    ",recvBuf);
    156     return ;
    157 }


      epoll参考资料

      http://blog.csdn.net/mmz_xiaokong/article/details/8704988
      http://blog.csdn.net/mmz_xiaokong/article/details/8704455
      http://www.cppblog.com/converse/archive/2008/10/13/63928.html
      http://blog.csdn.net/haoahua/article/details/2037704
      https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c/

      epoll为什么这么快: http://www.cppblog.com/converse/archive/2008/10/12/63836.html

      本文地址: http://www.cnblogs.com/wunaozai/p/3895860.html

  • 相关阅读:
    csrf 跨站请求伪造
    apache 根据端口访问配置
    对于表单中单双引号问题
    svn常用命令
    urlencode和rawurlencode的区别
    php 显示某一个时间相当于当前时间在多少秒前,多少分钟前,多少小时前
    javaweb基础(24)_jsp一般的标签开发
    javaweb基础(23)_jsp自定义标签
    javaweb基础(22)_Servlet+JSP+JavaBean实战登陆
    javaweb基础(21)_两种开发模式
  • 原文地址:https://www.cnblogs.com/wunaozai/p/3895860.html
Copyright © 2011-2022 走看看