概述
如果咱们要开发一个高并发的TCP程序。惯例的做法是:多过程或者多线程。即:应用其中一个线程或者过程去监听有没有客户端连贯上来,一旦有新客户端连贯,就新开一个线程,将其扔到线程(或过程)中去解决具体的读写操作等业务逻辑,主线程(过程)持续期待,监听其余的客户端。
这样操作往往存在很大的弊病。首先是浪费资源,要晓得,单个过程的最大虚拟内存是4G,单个线程的虚拟内存也有将近8M,那么,如果上万个客户端连贯上来,服务器将会承受不住。
其次是浪费时间,因为你必须始终等在accept
那个中央,非常被动。
上述的网络模型,其实说白了,就是一个线程一路IO,在单个线程里只能解决一个IO。因而,也可称之为单路IO。而一路IO,就是一个并发。有多少个并发,就必须要开启多少个线程,因而,对资源的节约是显而易见的。
那么,有没有一种形式,能够在一个线程里,解决多路IO呢?
咱们回顾一下多线程模型 ,它最大的技术难点是accept
和recv
函数都是阻塞的。只有没有新连贯上来,accept
就阻塞住了,无奈解决后续的业务逻辑;没有数据过去,recv
又阻塞住了 ,无奈解决新的accept
申请。因而,只有可能搞定在同一个线程里同时accept和recv的问题,仿佛所有问题就迎刃而解了。
有人说,这怎么可能嘛?必定要两个线程的 。
还真有可能,而这所谓的可能 ,就是IO多路复用技术。
IO多路复用
所谓的IO多路复用,它的核心思想就是,把监听新客户端连贯的操作转包进来,让零碎内核来做这件事件。即由内核来负责监听有没有连贯建设、有没有读写申请 ,作为服务端,只须要注册相应的事件,当事件触发时,由内核告诉服务端程序去解决就行了。
这样做的益处不言而喻:只须要在一个主线程里,就能够实现所有的工作,既不会阻塞,也不会节约太多资源。
说得通俗易懂一些,就是原来须要由主线程干的活,当初都交给内核去干了。咱们不必阻塞在accept
和recv
那里,而是由内核通知程序,有新客户端连贯上来了 ,或者有数据发送过去了,咱们再去调用accept
和recv
就行了,其余工夫,咱们能够解决其余的业务逻辑。
那么有人问了,你不还是要调用accept
和recv
吗?为什么当初就不会阻塞了呢 ?
这就要深刻说一下listen
和accept
的关系了。
如果服务器是海底捞火锅店的话,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解决的流程:
- 创立fd_set 位图汇合(3个汇合,一个readfds,一个writefds,一个exceptfds)
- FD_ZERO将set清空
- 应用FD_SET将须要监听的fd设置对应的事件
- select函数注册事件,只有select函数返回了大于1的值,阐明有事件触发,这时候把set拿进去做判断
- 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 能够写入优先级数据。
事件尽管比拟多,但咱们次要关怀POLLIN
和POLLOUT
就行了。
实现
#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虽好,但无奈实现一套跨平台的接口封装,却过于鸡肋了。