特点
- 对于待检测集合select和poll是基于线性方式处理的,epoll是基于红黑树来管理待检测集合的。
- select和poll每次都会线性扫描整个待检测集合,集合越大速度越慢,epoll使用的是回调机制,效率高,处理效率也不会随着检测集合的变大而下降
- select和poll工作过程中存在内核/用户空间数据的频繁拷贝问题,在epoll中内核和用户区使用的是共享内存(基于mmap内存映射区实现),省去了不必要的内存拷贝。
- 程序猿需要对select和poll返回的集合进行判断才能知道哪些文件描述符是就绪的,通过epoll可以直接得到已就绪的文件描述符集合,无需再次检测
- 使用epoll没有最大文件描述符的限制,仅受系统中进程能打开的最大文件数目限制
epoll操作函数
在epoll中一共提供是三个API函数,分别处理不同的操作,函数原型如下:
#include <sys/epoll.h>// 创建epoll实例,通过一棵红黑树管理待检测集合
int epoll_create(int size);
// 管理红黑树上的文件描述符(添加、修改、删除)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 检测epoll树中是否有就绪的文件描述符
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
使用示例
#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>// server
int main(int argc, const char* argv[])
{// 创建监听的套接字int lfd = socket(AF_INET, SOCK_STREAM, 0);if(lfd == -1){perror("socket error");exit(1);}// 绑定struct sockaddr_in serv_addr;memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(9999);serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 本地多有的IP// 设置端口复用int opt = 1;setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));// 绑定端口int ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));if(ret == -1){perror("bind error");exit(1);}// 监听ret = listen(lfd, 64);if(ret == -1){perror("listen error");exit(1);}// 现在只有监听的文件描述符// 所有的文件描述符对应读写缓冲区状态都是委托内核进行检测的epoll// 创建一个epoll模型int epfd = epoll_create(100);if(epfd == -1){perror("epoll_create");exit(0);}// 往epoll实例中添加需要检测的节点, 现在只有监听的文件描述符struct epoll_event ev;ev.events = EPOLLIN; // 检测lfd读读缓冲区是否有数据ev.data.fd = lfd;ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);if(ret == -1){perror("epoll_ctl");exit(0);}struct epoll_event evs[1024];int size = sizeof(evs) / sizeof(struct epoll_event);// 持续检测while(1){// 调用一次, 检测一次int num = epoll_wait(epfd, evs, size, -1);for(int i=0; i<num; ++i){// 取出当前的文件描述符int curfd = evs[i].data.fd;// 判断这个文件描述符是不是用于监听的if(curfd == lfd){// 建立新的连接int cfd = accept(curfd, NULL, NULL);// 新得到的文件描述符添加到epoll模型中, 下一轮循环的时候就可以被检测了ev.events = EPOLLIN; // 读缓冲区是否有数据ev.data.fd = cfd;ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);if(ret == -1){perror("epoll_ctl-accept");exit(0);}}else{// 处理通信的文件描述符// 接收数据char buf[1024];memset(buf, 0, sizeof(buf));int len = recv(curfd, buf, sizeof(buf), 0);if(len == 0){printf("客户端已经断开了连接\n");// 将这个文件描述符从epoll模型中删除epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);close(curfd);}else if(len > 0){printf("客户端say: %s\n", buf);send(curfd, buf, len, 0);}else{perror("recv");exit(0);} }}}return 0;
}
解决边沿模式接收全部数据问题
// 持续检测while(1){// 调用一次, 检测一次int num = epoll_wait(epfd, evs, size, -1);printf("==== num: %d\n", num);for(int i=0; i<num; ++i){// 取出当前的文件描述符int curfd = evs[i].data.fd;// 判断这个文件描述符是不是用于监听的if(curfd == lfd){// 建立新的连接int cfd = accept(curfd, NULL, NULL);// 将文件描述符设置为非阻塞// 得到文件描述符的属性int flag = fcntl(cfd, F_GETFL);flag |= O_NONBLOCK;fcntl(cfd, F_SETFL, flag);// 新得到的文件描述符添加到epoll模型中, 下一轮循环的时候就可以被检测了// 通信的文件描述符检测读缓冲区数据的时候设置为边沿模式ev.events = EPOLLIN | EPOLLET; // 读缓冲区是否有数据ev.data.fd = cfd;ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);if(ret == -1){perror("epoll_ctl-accept");exit(0);}}else{// 处理通信的文件描述符// 接收数据char buf[5];memset(buf, 0, sizeof(buf));// 循环读数据while(1){int len = recv(curfd, buf, sizeof(buf), 0);if(len == 0){// 非阻塞模式下和阻塞模式是一样的 => 判断对方是否断开连接printf("客户端断开了连接...\n");// 将这个文件描述符从epoll模型中删除epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);close(curfd);break;}else if(len > 0){// 通信// 接收的数据打印到终端write(STDOUT_FILENO, buf, len);// 发送数据send(curfd, buf, len, 0);}else{// len == -1if(errno == EAGAIN){printf("数据读完了...\n");break;}else{perror("recv");exit(0);}}}}}}