1.epoll API 介绍
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 */
};常见的Epoll检测事件:- EPOLLIN- EPOLLOUT- EPOLLERR// 对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);- 参数:- epfd : epoll实例对应的文件描述符- op : 要进行什么操作EPOLL_CTL_ADD: 添加EPOLL_CTL_MOD: 修改EPOLL_CTL_DEL: 删除- fd : 要检测的文件描述符- event : 检测文件描述符什么事情// 检测函数----检测epoll树中是否有就绪的文件描述符
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);- 参数:- epfd : epoll实例对应的文件描述符- events : 传出参数,保存了发送了变化的文件描述符的信息- maxevents : 第二个参数结构体数组的大小- timeout : 阻塞时间- 0 : 不阻塞- -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞- > 0 : 阻塞的时长(毫秒)- 返回值:- 成功,返回发送变化的文件描述符的个数 > 0- 失败 -1// 创建epoll实例,通过一棵红黑树管理待检测集合
int epoll_create(int size);
// epoll 的使用
// 操作步骤
// 在服务器使用 epoll 进行 IO 多路转接的操作步骤如下:1.创建监听的套接字int lfd = socket(AF_INET, SOCK_STREAM, 0);2.设置端口复用(可选)int opt = 1;setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));3.使用本地的IP与端口和监听的套接字进行绑定int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));4.给监听的套接字设置监听listen(lfd, 128);5.创建 epoll 实例int epfd = epoll_create(100);6.将用于监听的套接字添加到 epoll 实例中struct epoll_event ev;ev.events = EPOLLIN; //检测lfd读缓冲区是否有数据ev.data.fd = lfd;int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);接着创建一个数组,用于存储epoll_wait()返回的文件描述符struct epoll_event evs[1024];7.检测添加到epoll实例中的文件描述符是否已经就绪,并将这些已就绪的文件描述符进行处理int num = epoll_wait(epfd, evs, size, -1);① 如果监听的是文件描述符,和新客户端建立连接,将得到的文件描述符添加到epoll实例中int cfd = accept(curfd,NULL,NULL);ev.events = EPOLLIN;ev.data.fd = cfd;新得到的文件描述符添加到epoll模型中,下一轮循环的时候就可以被检测了epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);② 如果是通信的文件描述符,和对应的客户端通信,如果连接已断开,将该文件描述符从epoll实例中删除int len = recv(curfd,buf,sizeof(buf),0);if(len == 0) {// 将这个文件描述符从epoll实例中删除epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);close(curfd);}else if(len > 0) {send(curfd,buf,len,0);}8.重复第 7 步的操作
server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/epoll.h>int main() {// 创建socketint lfd = socket(AF_INET,SOCK_STREAM,0);struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(9999);saddr.sin_addr.s_addr = INADDR_ANY;// 绑定int ret = bind(lfd,(struct sockaddr*)&saddr,sizeof(saddr));if(ret == -1) {perror("bind");exit(-1);}// 监听ret = listen(lfd,8);if(ret == -1) {perror("listen");exit(-1);}// 用epoll_create()创建一个epoll实例int epfd = epoll_create(100);// 将监听的文件描述符相关的检测信息添加到epoll实例中struct epoll_event epev;epev.events = EPOLLIN;epev.data.fd = lfd;epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&epev);// 创建一个数组,用于存储epoll_wait()返回的文件描述符struct epoll_event epevs[1024];while (1) {ret = epoll_wait(epfd,epevs,1024,-1);if(ret == -1) {perror("epoll_wait");exit(-1);}printf("ret = %d\n",ret);for(int i = 0;i < ret;i++) {int curfd = epevs[i].data.fd;if(curfd == lfd) {// 监听的文件描述符有数据到达,有客户端连接struct sockaddr_in caddr;int len = sizeof(caddr);int cfd = accept(lfd,(struct sockaddr*)&caddr,&len);// epev.events = EPOLLIN | EPOLLOUT;epev.events = EPOLLIN;epev.data.fd = cfd;epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&epev);} else {// if(epevs[i].events & EPOLLOUT) {// continue;// }// 有数据到达,需要通信char buf[1024] = {0};int len = read(curfd,buf,sizeof(buf));if (len == -1) {perror("read");exit(-1);} else if(len == 0) {printf("client closed...\n");epoll_ctl(epfd,EPOLL_CTL_DEL,curfd,NULL);close(curfd);} else if(len > 0) {printf("recv buf = %s\n",buf);write(curfd,buf,strlen(buf) + 1);}}}}close(lfd);close(epfd);return 0;
}
client.c
#include <stdio.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>int main(int argc,char* argv[]) {int fd = socket(AF_INET,SOCK_STREAM,0);if(fd == -1) {perror("socket");return -1;}struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(9999);inet_pton(AF_INET,"127.0.0.1",&saddr.sin_addr.s_addr);// 连接服务器int ret = connect(fd,(struct sockaddr*)&saddr,sizeof(saddr));if(ret == -1) {perror("connect");return -1;}int num = 0;while (1) {char sendBuf[1024] = {0};sprintf(sendBuf,"send data %d",num++);write(fd,sendBuf,strlen(sendBuf) + 1);// 接收int len = read(fd,sendBuf,sizeof(sendBuf));if(len == -1) {perror("read");return -1;}else if(len > 0) {printf("read buf = %s\n",sendBuf);}else{printf("服务器已经断开连接...\n");break;}// sleep(1);usleep(1000);}close(fd);return 0;
}
2.epoll 的两种工作模式
Epoll 的工作模式:LT 模式 (水平触发)假设委托内核检测读事件 -> 检测fd的读缓冲区读缓冲区有数据 - > epoll检测到了会给用户通知a.用户不读数据,数据一直在缓冲区,epoll 会一直通知b.用户只读了一部分数据,epoll会通知c.缓冲区的数据读完了,不通知LT(level - triggered)是缺省的工作方式,并且同时支持 block 和 no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,内核还是会继续通知你的。ET 模式(边沿触发)假设委托内核检测读事件 -> 检测fd的读缓冲区读缓冲区有数据 - > epoll检测到了会给用户通知a.用户不读数据,数据一直在缓冲区中,epoll下次检测的时候就不通知了b.用户只读了一部分数据,epoll不通知c.缓冲区的数据读完了,不通知ET(edge - triggered)是高速工作方式,只支持 no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个 fd 作 IO 操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
【注意】 ET模式需要配合循环+非阻塞
(1)LT 模式
epoll_lt.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/epoll.h>int main() {// 创建socketint lfd = socket(AF_INET,SOCK_STREAM,0);struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(9999);saddr.sin_addr.s_addr = INADDR_ANY;// 绑定int ret = bind(lfd,(struct sockaddr*)&saddr,sizeof(saddr));if(ret == -1) {perror("bind");exit(-1);}// 监听ret = listen(lfd,8);if(ret == -1) {perror("listen");exit(-1);}// 用epoll_create()创建一个epoll实例int epfd = epoll_create(100);// 将监听的文件描述符相关的检测信息添加到epoll实例中struct epoll_event epev;epev.events = EPOLLIN;epev.data.fd = lfd;epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&epev);// 创建一个数组,用于存储epoll_wait()返回的文件描述符struct epoll_event epevs[1024];while (1) {ret = epoll_wait(epfd,epevs,1024,-1);if(ret == -1) {perror("epoll_wait");exit(-1);}printf("ret = %d\n",ret);for(int i = 0;i < ret;i++) {int curfd = epevs[i].data.fd;if(curfd == lfd) {// 监听的文件描述符有数据到达,有客户端连接struct sockaddr_in caddr;int len = sizeof(caddr);int cfd = accept(lfd,(struct sockaddr*)&caddr,&len);// epev.events = EPOLLIN | EPOLLOUT;epev.events = EPOLLIN;epev.data.fd = cfd;epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&epev);} else {// if(epevs[i].events & EPOLLOUT) {// continue;// }// 有数据到达,需要通信char buf[5] = {0};int len = read(curfd,buf,sizeof(buf));if (len == -1) {perror("read");exit(-1);} else if(len == 0) {printf("client closed...\n");epoll_ctl(epfd,EPOLL_CTL_DEL,curfd,NULL);close(curfd);} else if(len > 0) {printf("recv buf = %s\n",buf);write(curfd,buf,strlen(buf) + 1);}}}}close(lfd);close(epfd);return 0;
}
(2)ET 模式
struct epoll_event {uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */
};常见的Epoll检测事件:- EPOLLIN- EPOLLOUT- EPOLLERR- EPOLLET
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>int main() {// 创建socketint lfd = socket(AF_INET,SOCK_STREAM,0);struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(9999);saddr.sin_addr.s_addr = INADDR_ANY;// 绑定int ret = bind(lfd,(struct sockaddr*)&saddr,sizeof(saddr));if(ret == -1) {perror("bind");exit(-1);}// 监听ret = listen(lfd,8);if(ret == -1) {perror("listen");exit(-1);}// 用epoll_create()创建一个epoll实例int epfd = epoll_create(100);// 将监听的文件描述符相关的检测信息添加到epoll实例中struct epoll_event epev;epev.events = EPOLLIN;epev.data.fd = lfd;epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&epev);// 创建一个数组,用于存储epoll_wait()返回的文件描述符struct epoll_event epevs[1024];while (1) {ret = epoll_wait(epfd,epevs,1024,-1);if(ret == -1) {perror("epoll_wait");exit(-1);}printf("ret = %d\n",ret);for(int i = 0;i < ret;i++) {int curfd = epevs[i].data.fd;if(curfd == lfd) {// 监听的文件描述符有数据到达,有客户端连接struct sockaddr_in caddr;int len = sizeof(caddr);int cfd = accept(lfd,(struct sockaddr*)&caddr,&len);// 设置cfd属性非阻塞int flag = fcntl(cfd,F_GETFL);flag |= O_NONBLOCK; fcntl(cfd,F_SETFL,flag);epev.events = EPOLLIN | EPOLLET;// 设置边沿触发epev.data.fd = cfd;epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&epev);} else {if(epevs[i].events & EPOLLOUT) {continue;}// 循环读取出所有的数据char buf[5];int len = 0;while ((len = read(curfd,buf,sizeof(buf))) > 0) {// 打印数据// printf("recv data : %s\n",buf);write(STDOUT_FILENO,buf,len);write(curfd,buf,len);}if(len == 0) {printf("client closed...\n");}else if(len == -1) {if(errno == EAGAIN) {printf("data over......\n");} else {perror("read");exit(-1);}}}}}close(lfd);close(epfd);return 0;
}
client.c
#include <stdio.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>int main(int argc,char* argv[]) {int fd = socket(AF_INET,SOCK_STREAM,0);if(fd == -1) {perror("socket");return -1;}struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(9999);inet_pton(AF_INET,"127.0.0.1",&saddr.sin_addr.s_addr);// 连接服务器int ret = connect(fd,(struct sockaddr*)&saddr,sizeof(saddr));if(ret == -1) {perror("connect");return -1;}int num = 0;while (1) {char sendBuf[1024] = {0};// sprintf(sendBuf,"send data %d",num++);fgets(sendBuf,sizeof(sendBuf),stdin);write(fd,sendBuf,strlen(sendBuf) + 1);// 接收int len = read(fd,sendBuf,sizeof(sendBuf));if(len == -1) {perror("read");return -1;}else if(len > 0) {printf("read buf = %s\n",sendBuf);}else{printf("服务器已经断开连接...\n");break;}// sleep(1);// usleep(1000);}close(fd);return 0;
}