概述

如果咱们要开发一个高并发的TCP程序。惯例的做法是:多过程或者多线程。即:应用其中一个线程或者过程去监听有没有客户端连贯上来,一旦有新客户端连贯,就新开一个线程,将其扔到线程(或过程)中去解决具体的读写操作等业务逻辑,主线程(过程)持续期待,监听其余的客户端。

这样操作往往存在很大的弊病。首先是浪费资源,要晓得,单个过程的最大虚拟内存是4G,单个线程的虚拟内存也有将近8M,那么,如果上万个客户端连贯上来,服务器将会承受不住。

其次是浪费时间,因为你必须始终等在accept那个中央,非常被动。

上述的网络模型,其实说白了,就是一个线程一路IO,在单个线程里只能解决一个IO。因而,也可称之为单路IO。而一路IO,就是一个并发。有多少个并发,就必须要开启多少个线程,因而,对资源的节约是显而易见的。

那么,有没有一种形式,能够在一个线程里,解决多路IO呢?

咱们回顾一下多线程模型 ,它最大的技术难点是acceptrecv函数都是阻塞的。只有没有新连贯上来,accept就阻塞住了,无奈解决后续的业务逻辑;没有数据过去,recv又阻塞住了 ,无奈解决新的accept申请。因而,只有可能搞定在同一个线程里同时accept和recv的问题,仿佛所有问题就迎刃而解了。

有人说,这怎么可能嘛?必定要两个线程的 。

还真有可能,而这所谓的可能 ,就是IO多路复用技术。

IO多路复用

所谓的IO多路复用,它的核心思想就是,把监听新客户端连贯的操作转包进来,让零碎内核来做这件事件。即由内核来负责监听有没有连贯建设、有没有读写申请 ,作为服务端,只须要注册相应的事件,当事件触发时,由内核告诉服务端程序去解决就行了。

这样做的益处不言而喻:只须要在一个主线程里,就能够实现所有的工作,既不会阻塞,也不会节约太多资源。

说得通俗易懂一些,就是原来须要由主线程干的活,当初都交给内核去干了。咱们不必阻塞在acceptrecv那里,而是由内核通知程序,有新客户端连贯上来了 ,或者有数据发送过去了,咱们再去调用acceptrecv就行了,其余工夫,咱们能够解决其余的业务逻辑。

那么有人问了,你不还是要调用acceptrecv吗?为什么当初就不会阻塞了呢 ?

这就要深刻说一下listenaccept的关系了。

如果服务器是海底捞火锅店的话,listen就是门口迎宾的小姐,当来了一个客人(客户端),就将其迎进店内。而accept则是店内的大堂经理 ,当没人来的时候,就始终闲在那里没事做,listen将客人 迎进来之后,accept就会调配一个服务员(fd)专门 服务于这个客人 。

所以说,只有listen失常工作,就能源源不断地将客人迎进饭店(客户端能 失常连贯上服务器),即便此时并没有accept。那么,有人必定有疑难,总不能始终 往里迎吧,酒店也是有大小的,全副挤在大堂也装不下 那么多人啊。还记得 listen函数的第二个参数backlog吗?它就示意在没有accept之前,最多能够迎多少个客人进来。

因而,对于多线程模型来说,accept作为大堂经理,在 没客人来的时候 ,就眼巴巴地盯着门口 ,啥也不干,当listen把人迎进来了,才开始干活。只能说,摸鱼,还是accpet会啊。

而IO多路复用,则相当于请了一个秘书。accept作为大堂经理,必定有很多其余事件能够忙,他就不必 始终盯着门口,当listen把人迎进来之后,秘书就会把客人(们)带到经理身边,让经理安顿服务员(fd)。

只是这个秘书是内核提供的,因而不仅收费,而且勤快。收费的劳动力 ,何乐而不为呢?

它的流程图大略是上面这样子的:

咱们通常所说的IO多路复用技术,在Linux环境下,次要有三种实现,别离为select、poll 和 epoll,以及io_uring。在darwin平台 ,则有kqueue,Windows 下则是 iocp。从性能上来说,iocp要优于epoll,与io_uring并驾齐驱。但select、poll、epoll的演变是一个继续迭代的过程,虽说从效率以及应用普及率上来说,epoll堪称经典,但并不是另外两种实现就毫无用处,也是有其存在的意义的,尤其是select。

本文不会花太多笔墨来介绍kqueue,笔者始终认为,拿MacOS作为服务器开发,要么脑子瓦特了,要么就是钱烧的。基本上除了本人写写 demo外,极少能在生产环境真正用起来。而iocp自成一派,将来有暇,将专门开拓专题细说。io_uring作为较新的内核才引入的个性,本文也不宜大肆开展。

唯有select、poll 以及epoll,久经工夫考验,已被宽泛使用于各大出名网络应用,并由此诞生出许多经典的网络模型,切实是值得好好细说。

select

原型

select函数原型:

/* According to POSIX.1-2001, POSIX.1-2008 */       #include <sys/select.h>       /* According to earlier standards */       #include <sys/time.h>       #include <sys/types.h>       #include <unistd.h>       int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数阐明:

  • nfds: 最大的文件描述符+1,代表监听这一组描述符(为什么要+1?因为除了以后最大描述符之外,还有可能有新的fd连贯上来)
  • fd_set: 是一个位图汇合, 对于同一个文件描述符,能够监听不同的事件
  • readfds:文件描述符“可读”事件
  • writefds:文件描述符“可写”事件
  • exceptfds:文件描述符“异样”事件,个别内核用的,理论编程很少应用
  • timeout:超时工夫:0是立刻返回,-1是始终阻塞,如果大于0,则达到设置值的微秒数即返回
  • 返回值: 所监听的所有监听汇合中满足条件的总数(满足条件的读、写、异样事件的总数),出错时返回-1,并设置errno。如果超时工夫触发,则返回0。

从select的函数原型可知,它次要依赖于三个bitmap的汇合,别离为可读事件汇合,可写事件汇合,以及异样事件汇合。咱们只须要将待监听的fd退出到对应的汇合中,当有对应事件触发,咱们再从汇合中将其 拿进去 进行解决就行了。

那么,怎么将文件描述符加到监听事件汇合中呢?

内核为咱们提供了四个操作宏:

void FD_CLR(int fd, fd_set *set);    //将fd从set中革除进来,位图置为0int  FD_ISSET(int fd, fd_set *set);   //判断fd是否在汇合中,返回值为1,阐明满足了条件void FD_SET(int fd, fd_set *set);    //将fd设置到set中去,位图置为1void FD_ZERO(fd_set *set);    //将set汇合清空为0值 

有了以上根底,咱们 就能大抵梳理一下select解决的流程:

  1. 创立fd_set 位图汇合(3个汇合,一个readfds,一个writefds,一个exceptfds)
  2. FD_ZERO将set清空
  3. 应用FD_SET将须要监听的fd设置对应的事件
  4. select函数注册事件,只有select函数返回了大于1的值,阐明有事件触发,这时候把set拿进去做判断
  5. FD_ISSET 判断fd到底触发了什么事件

实现

其代码 实现如下所示:

#include <stdio.h>#include <stdlib.h>#include  <unistd.h>#include <string.h>#include <arpa/inet.h>#include <ctype.h>int main(int argc, char *argv[]){    int i, n, maxi;    int nready,  client[FD_SETSIZE];    // FD_SETSIZE 为内核定义的,大小为1024, client保留曾经被监听的文件描述符,防止每次都遍历1024个fd    int maxfd, listenfd, connfd,  sockfd;    char  buf[BUFSIZ], str[INET_ADDRSTRLEN];  // INET_ADDRSTRLEN = 16    struct sockaddr_in clie_addr, serv_addr;    socklen_t clie_addr_len;    fd_set rset, allset;   //allset为所有曾经被监听的fd汇合,rset为select返回的有监听事件的fd    listenfd = socket(AF_INET, SOCK_STREAM, 0);   //创立服务端fd    bzero(&serv_addr, sizeof(serv_addr));    serv_addr.sin_family = AF_INET;    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);    serv_addr.sin_port = htons(8888);    if (bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {        perror("bind");    }    listen(listenfd, 20);    maxfd = listenfd;    maxi = -1;    for(i = 0; i < FD_SETSIZE; i++) {        client[i] = -1;    }    FD_ZERO(&allset);    FD_SET(listenfd, &allset);    //----------------------------------------------------------    //至此,初始化全副实现, 开始监听    while(1) {        rset = allset;  //allset不能被select改掉了,所以要复制一份进去放到rset        nready = select(maxfd+1, &rset, NULL, NULL,  NULL);        if (nready < 0) {            perror("select");        }        //listenfd有返回,阐明有新连贯建设了         if (FD_ISSET(listenfd, &rset)) {            clie_addr_len = sizeof(clie_addr);            connfd = accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len);            printf("received form %s at port %d\n", inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)), ntohs(clie_addr.sin_port));            //把新连贯的client fd放到client数组中            for (i = 0; i < FD_SETSIZE; i++) {                if (client[i] == -1) {                    client[i] = connfd;                    break;                }            }            //连接数超过了1024, select函数解决不了了,间接报错返回            if (i == FD_SETSIZE) {                fputs("too many clients\n", stderr);                exit(1);            }            FD_SET(connfd, &allset);    //把新的客户端fd退出到下次要监听的列表中            if (connfd > maxfd) {                maxfd = connfd;      //次要给select第一个参数用的            }            if (i > maxi) {                maxi = i;    //保障maxi存的总是client数组的最初一个下标元素            }            //如果nready = 0, 阐明新连贯都曾经解决完了,且没有已建设好的连贯触发读事件            if (--nready == 0) {                continue;            }        }        for (i = 0; i <= maxi; i++) {            if ((sockfd = client[i]) < 0) {                continue;            }            //sockfd 是存在client里的fd,该函数触发,阐明有数据过去了            if (FD_ISSET(sockfd, &rset)) {                if ((n = read(sockfd, buf, sizeof(buf))) == 0) {                    printf("socket[%d] closed\n", sockfd);                    close(sockfd);                    FD_CLR(sockfd, &allset);                    client[i] = -1;                } else if (n > 0) {                    //失常接管到了数据                    printf("accept data: %s\n", buf);                }                if (--nready == 0) {                    break;                }                    }        }    }    close(listenfd);    return 0;}

毛病

select作为IO多路复用的初始版本,只能说是能用而已,性能并不能高到哪儿去,应用的局限性也比拟大。次要体现在以下几个方面:

  • 文件描述符下限:1024,同时监听的最大文件描述符也为1024个
  • select须要遍历所有的文件描述符(1024个),所以通常须要自定义数据结构(数组),独自存文件描述符,缩小遍历
  • 监听汇合和满足条件的汇合是同一个汇合,导致判断和下次监听时须要对汇合读写,也就是说,下次监听时须要清零,那么以后的汇合后果就须要独自保留。

长处

但select也并不是一无是处,我集体是非常喜爱select这个函数的,次要得益于以下几个方面:

  • 它至多提供了单线程同时解决多个IO的一种解决方案,在一些简略的场景(比方并发小于 1024)的时候 ,还是很有用途的
  • select的实现比起poll和epoll,要简单明了许多,这也是我为什么举荐在一些简略场景优先应用select的起因
  • select是跨平台的,相比于poll和epoll是Unix独有,select显著有更加广大的施展空间
  • 利用select的跨平台个性,能够实现很多乏味的性能。比方实现一个跨平台的sleep函数。

    • 咱们晓得,Linux下的原生sleep函数是依赖于sys/time.h的,这也就意味着它无奈被Windows平台调用。
    • 因为select函数自身跨平台,而第五个参数恰好是一个超时工夫,即:咱们能够传入一个超时工夫,此时程序就会阻塞在select这里,直到超时工夫触发,这也就间接地实现了sleep性能。
    • 代码实现如下

      //传入一个微秒工夫void general_sleep(int t){    struct timeval tv;     tv.tv_usec = t % 10e6;    tv.tv_sec = t / 10e6;    select(0, NULL, NULL, NULL, &tv);}
    • select实现的sleep函数至多有两个益处:

      • 能够跨平台调用
      • 精度能够准确到微秒级,比起Linux原生的sleep函数,精度要高得多。

poll

原型

#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);struct pollfd {      int   fd;         /* file descriptor */      short events;     /* requested events */      short revents;    /* returned events */};

参数阐明:

fds: 数组的首地址

nfds: 最大监听的文件描述符个数

timeout: 超时工夫

鉴于select函数的一些 毛病和局限性,poll的实现就做了一些降级。首先,它冲破了1024文件描述符的限度,其次,它将事件封装了一下 ,形成了pollfd的构造体,并将这个 构造体中注册的事件间接与fd进行了绑定,这样 就无需每次有事件触发,就遍历所有的fd了,咱们只须要遍历这个 构造体数组中的fd即可。

那么 ,poll函数能够注册哪些事件类型呢?

POLLIN 读事件POLLPRI 触发异样条件POLLOUT 写事件POLLRDHUP  敞开连贯POLLERR  产生了谬误POLLHUP  挂断POLLNVAL 有效申请,fd未关上POLLRDNORM 等同于POLLINPOLLRDBAND 能够读取优先带数据(在 Linux 上通常不应用)。POLLWRNORM 等同于POLLOUTPOLLWRBAND 能够写入优先级数据。

事件尽管比拟多,但咱们次要关怀POLLINPOLLOUT就行了。

实现

#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <string.h>#include <netinet/in.h>#include <arpa/inet.h>#include <poll.h>#include <errno.h>#include <ctype.h>#define OPEN_MAX 1024int main(int argc, char *argv[]){    int i, maxi, listenfd, connfd,  sockfd;    int nready;  // 承受poll返回值,记录满足监听事件的fd个数    ssize_t n;      char  buf[BUFSIZ], str[INET_ADDRSTRLEN];  // INET_ADDRSTRLEN = 16    struct pollfd client[OPEN_MAX];     //用来寄存监听文件描述符和事件的汇合    struct sockaddr_in cliaddr, servaddr;    socklen_t clilen;       listenfd = socket(AF_INET, SOCK_STREAM, 0);   //创立服务端fd    int opt = 1;    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));  //设置端口复用    bzero(&servaddr, sizeof(servaddr));    servaddr.sin_family = AF_INET;    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);    servaddr.sin_port = htons(8888);    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {        perror("bind");    }    listen(listenfd, 128);    //设置第一个要监听的文件描述符,即服务端的文件描述符    client[0].fd = listenfd;    client[0].events = POLLIN;   //监听读事件    for(i = 1; i < OPEN_MAX; i++) {   //留神从1开始,因为0曾经被listenfd用了        client[i].fd = -1;    }    maxi = 0;   //因为曾经加进去一个了,所以从0开始就行    //----------------------------------------------------------    //至此,初始化全副实现, 开始监听    for(;;) {        nready = poll(client,  maxi+1, -1);   // 阻塞监听是否有客户端读事件申请        if (client[0].revents & POLLIN) {   // listenfd触发了读事件            clilen = sizeof(cliaddr);            connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen);    //承受新客户端的连贯申请            printf("recieved from %s at port %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));            for (i = 0; i < OPEN_MAX; i++) {                if (client[i].fd < 0) {                    client[i].fd = connfd;  // 将新连贯的文件描述符加到client数组中                    break;                }            }            if (i == OPEN_MAX) {                perror("too many open connections");            }            client[i].events = POLLIN;            if(i > maxi) {                maxi = i;            }            if (--nready == 0) {                continue;            }        }                //后面的if没满足,阐明有数据发送过去,开始遍历client数组        for (i = 1; i <= maxi; i++) {            if ((sockfd = client[i].fd) < 0) {                continue;            }            //读事件满足,用read去承受数据            if (client[i].revents & POLLIN) {                if ((n = read(sockfd, buf, sizeof(buf))) < 0) {                    if (errno = ECONNRESET) {   // 收到RST标记                        printf("client[%d] aborted conection\n", client[i].fd);                        close(sockfd);                        client[i].fd = -1;  //poll中不监控该文件描述符,间接置-1即可,无需像select中那样移除                    } else {                        perror("read error");                    }                } else if (n == 0) {    //客户端敞开连贯                    printf("client[%d] closed connection\n", client[i].fd);                    close(sockfd);                    client[i].fd = -1;                 } else {                    printf("recieved data: %s\n", buf);                }            }            if (--nready <= 0) {                break;            }                    }    }    close(listenfd);    return 0;}

长处

poll函数相比于select函数来说,最大的长处就是冲破了1024个文件描述符的限度,这使得百万并发变得可能。

而且不同于select,poll函数的监听和返回是离开的,因而不必在每次操作之前都独自备份一份了,简化了代码实现。因而,能够了解为select的降级增强版。

毛病

尽管poll不须要遍历所有的文件描述符了,只须要遍历退出数组中的描述符,范畴放大了很多,但毛病依然是须要遍历。假如真有百万并发的场景,当仅有两三个事件触发的时候,依然要遍历上百万个文件描述符,只为了找到那触发事件的两三个fd,这样看来 ,就有些得失相当了。而这个毛病,将在epoll中得以彻底解决。

poll作为 一个适度版本的实现 ,说实话位置有些难堪:它既不具备select函数跨平台的劣势,又不具备epoll的高性能。因而应用面以及遍及水平相对来说,反而是三者之中最差劲的一个。

若说它的惟一应用场景,大略也就是开发者既想冲破1024文件描述符的限度,又不想把代码写得像epoll那样简单了。

epoll

原型

epoll堪称是以后IO多路复用的最终状态,它是 poll的 加强版本。咱们说poll函数,尽管冲破了select函数1024文件描述符的限度,且把监听事件和返回事件离开了,然而说到底还是要遍历所有文件描述符,能力晓得到底是哪个文件描述符触发了事件,或者须要独自定义一个数组。

而epoll则能够返回一个触发了事件的所有描述符的数组汇合,在这个数组汇合里,所有的文件描述符都是须要解决的,就不须要咱们再独自定义数组了。

尽管epoll功能强大了,然而应用起来却麻烦得多。不同于select和poll应用一个函数监听即可,epoll提供了三个函数。

epoll_create

首先,须要应用epoll_create创立一个句柄:

#include <sys/epoll.h>int epoll_create(int size);

该函数返回一个文件描述符,这个文件描述符并不是 一个惯例意义 的文件描述符,而是一个均衡二叉树(精确来说是红黑树)的根节点。size则是树的大小,它代表你将监听多少个文件描述符。epoll_create将依照传入的大小,结构出一棵大小为size的红黑树。

留神:这个size只是倡议值,理论内核并不一定局限于size的大小,能够监听比size更多的文件描述符。然而因为均衡二叉树减少节点时可能须要自旋,如果size与理论监听的文件描述符差异过大,则会减少内核开销。

epoll_ctl

第二个函数是epoll_ctl, 这个函数次要用来操作epoll句柄,能够应用该函数往红黑树里减少文件描述符,批改文件描述符,和删除文件描述符。

能够看到,select和poll应用的都是bitmap位图,而epoll应用的是红黑树。

#include <sys/epoll.h>int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll_ctl有四个参数,参数1就是 epoll_create创立进去的句柄。

第二个参数op是操作标记位,有三个值,别离如下:

  • EPOLL_CTL_ADD 向树减少文件描述符
  • EPOLL_CTL_MOD 批改树中的文件描述符
  • EPOLL_CTL_DEL 删除树中的文件描述符

第三个参数就是须要操作的文件描述符,这个没啥说的。

重点看第四个参数,它是一个构造体。这个构造体原型如下:

           typedef union epoll_data {               void        *ptr;               int          fd;               uint32_t     u32;               uint64_t     u64;           } epoll_data_t;           struct epoll_event {               uint32_t     events;      /* Epoll events */               epoll_data_t data;        /* User data variable */           };

第一个元素为uint32_t类型的events,这个和poll相似,是一个bit mask,次要应用到的标记位有:

  • EPOLLIN 读事件
  • EPOLLOUT 写事件
  • EPOLLERR 异样事件

这个构造体还有第二个元素,是一个epoll_data_t类型的联合体。咱们先重点关注外面的fd,它代表一个文件描述符,初始化的时候传入须要监听的文件描述符,当监听返回时,此处会传出一个有事件产生的文件描述符,因而,无需咱们遍历失去后果了。

epoll_wait

epoll_wait才是真正的监听函数,它的原型如下:

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

第一个参数不用说了, 留神第二个参数,它尽管也是struct epoll_event *类型,然而和epoll_ctl中的含意不同,epoll_ctl代表传入进去的是一个地址,epoll_wait则代表传出的是一个数组。这个数组就是返回的所有触发了事件的文件描述符汇合。

第三个参数maxevents代表这个数组的大小。

timeout不用说了,它代表的是超时工夫。不过要留神的是,0代表立刻返回,-1代表永恒阻塞,如果大于0,则代表毫秒数(留神select的timeout是微秒)。

这个函数的返回值也是有意义的,它代表有多少个事件触发,也就能够简略了解为传出参数events的大小。

监听流程

大抵梳理一下epoll的监听流程:

  • 首先,要有一个服务端的listenfd
  • 而后,应用epoll_create创立一个句柄
  • 应用epoll_ctl将listenfd退出到树中,监听EPOLLIN事件
  • 应用epoll_wait监听
  • 如果EPOLLIN事件触发,阐明有客户端连贯上来,将新客户端退出到events中,从新监听
  • 如果再有EPOLLIN事件触发:
  • 遍历events,如果fd是listenfd,则阐明又有新客户端连贯上来,反复下面的步骤,将新客户端退出到events中
  • 如果fd不为listenfd,这阐明客户端有数据发过来,间接调用read函数读取内容即可。

触发

epoll有两种触发形式,别离为程度触发边际触发

  • 程度触发

    所谓的程度触发,就是只有仍有数据处于就绪状态,那么可读事件就会始终触发。

    举个例子,假如客户端一次性发来了4K数据 ,然而服务器recv函数定义的buffer大小仅为1024字节,那么一次必定是不能将所有数据都读取完的,这时候就会持续触发可读事件,直到所有数据都解决实现。

    epoll默认的触发形式就是程度触发。

  • 边际触发

    边际触发恰好相反,边际触发是只有数据发送过去的 时候会触发一次,即便数据没有 读取完,也不会持续触发。必须client再次调用send函数触发了可读事件,才会持续读取。

    假如客户端 一次性发来4K数据,服务器recv的buffer大小为 1024字节,那么服务器在第一次收到1024字节之后就不会持续,也不会有新的可读事件触发。只有 当客户端 再次发送数据的时候,服务器可读事件触发 ,才会持续读取第二个1024字节数据。

    留神:第二次可读事件触发时,它读取的依然是上次未读完的数据 ,而不是客户端第二次发过来的新数据。也就是说:数据没读完尽管不会持续触发EPOLLIN,但不会失落数据。

  • 触发形式的设置:

    程度触发和边际触发在内核里 应用两个bit mask辨别,别离为:

    EPOLLLT 程度 触发

    EPOLLET 边际触发

    咱们只须要在注册事件的时候将其与须要注册的工夫 做一个位或运算即可:

    ev.events = EPOLLIN;    //LTev.events = EPOLLIN | EPOLLET;   //ET

实现

#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<arpa/inet.h>#include<sys/epoll.h>#include<ctype.h>#define OPEN_MAX 1024int main(int argc, char **argv){    int i, listenfd, connfd, sockfd,epfd, res, n;    ssize_t nready = 0;    char buf[BUFSIZ] = {0};    char str[INET_ADDRSTRLEN];    socklen_t clilen;    struct sockaddr_in cliaddr, servaddr;    struct epoll_event event, events[OPEN_MAX];    //开始创立服务端套接字    listenfd = socket(AF_INET, SOCK_STREAM, 0);    int opt = 1;    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));    bzero(&servaddr, sizeof(servaddr));    servaddr.sin_family = AF_INET;    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);    servaddr.sin_port = htons(8888);    bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));    listen(listenfd, 128);    //开始初始化epoll    epfd = epoll_create(OPEN_MAX);    event.events = EPOLLIN;    event.data.fd = listenfd;    res = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &event);    if (res == -1) {        perror("server epoll_ctl error");        exit(res);    }        for(;;) {        //开始监听        nready = epoll_wait(epfd, events, OPEN_MAX, -1);        if (nready == -1) {            perror("epoll_wait error");            exit(nready);        }        for (i = 0; i < nready; i++) {            if (!(events[i].events & EPOLLIN)) {                continue;            }            if (events[i].data.fd == listenfd) {                //有新客户端连贯                clilen = sizeof(cliaddr);                connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);                printf("received from %s at port %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),                    ntohs(cliaddr.sin_port));                event.events = EPOLLIN;                event.data.fd = connfd;                if (epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &event) == -1) {                    perror("client epoll_ctl error");                    exit(-1);                }            } else {                //有数据能够读取                sockfd = events[i].data.fd;                n = read(sockfd, buf, sizeof(buf));                if (n ==0) {                    //读到0,阐明客户端敞开                    epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);                    close(sockfd);                    printf("client[%d] closed connection\n", sockfd);                } else if (n < 0){                    //出错                    epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);                    close(sockfd);                    printf("client[%d] read error\n", sockfd);                } else {                    //读到了数据                    printf("received data: %s\n", buf);                }            }        }    }    close(listenfd);    close(epfd);         return 0;}

长处

epoll的长处不言而喻,它解决了poll须要遍历所有注册的fd的 问题,只须要关怀触发了工夫的极少量fd即可,大大晋升了效率。

而更有意思 的是epoll_data_t这个联合体,它外面有四个元素:

typedef union epoll_data {      void        *ptr;      int          fd;      uint32_t     u32;      uint64_t     u64; } epoll_data_t;

简略开发时,咱们能够将fd记录在其中,然而 咱们留神到 这外面还有一个void *类型的元素,那就提供了有限可能。它能够是一个struct,也能够是一个callback,也能够是struct嵌套callback,从而实现无线的扩大可能。赫赫有名的反应堆reactor模型就是通过这种形式实现的。

在下篇专题里,笔者将带大家走进reactor模型,领略epoll的神奇魅力。

毛病

什么?epoll也有毛病?当然有,我认为epoll的最大毛病就是代码实现起来变得复杂了,写起来简单,了解起来更简单。

而且还有一个不能算毛病的毛病,对于笔者这样一个长期开发跨平台应用程序的开发者来说,epoll虽好,但无奈实现一套跨平台的接口封装,却过于鸡肋了。