我们聊了聊select和poll知道:
为了针对许多大量连接,高并发的的场景下大量的资源消耗,效率低的问题,这一节就浅浅来聊一下epoll,epoll是之前的select和poll的增强版本,是linux操作系统独有的I/O复用技术。
对于epoll来说他更灵活,解决了select和poll的弊端,使用起来也更加方便顺手,他不像select和poll那样只提供了一个方法,epoll提供了一组方法。
本节呢就是聊聊epoll的使用和一些优点,对于epoll的两种触犯机制ET和LT的探讨会放在下一节去聊聊,注意select和poll只是LT。
好了~言归正传
目录
用大白话去理解就是,比如你是老师,你给同学们布置了作业,你在班里要检查作业的时候 不是你一个个去问,“啊,你写完了没有啊?”,而是你坐在讲台上,谁写完了,谁把作业拿上来给你,比方说有三个人写完了,这三个人就把作业交给你,你就会知道“哦,有三个人写完了”,至于剩下没交给你的那就是没写完的,你也不需要去问~
大概就是这样了,关键就是不用你去一个个问,而是谁完了,谁通知你

前面说了epoll提供了一组api,方便我们去使用,下来我们看看~

epoll的核心是3个API,核心数据结构是:1个红黑树和1个链表组成。
【1. 创建内核事件表:】
函数原型为:
- #include
- int epoll_create(int size)
- //成功返回内核事件表的标识符,失败返回-1
功能:该函数 创建内核事件表用于存放描述符和关注的事件。调用这个函数的时候,在内核cache里建立了红黑树struct rb_root用于存储以后epoll_ctl传来的socket,也就是内核事件表;还建立了一个双向链表struct list_headrdllidt用于存储就绪事件。 当epoll_wait调用时,仅仅观察这个双向链表里有没有数据即可,有数据表示有就绪事件,直接返回。
size参数:表示创建的事件表需要多大,记住最后要close关闭,如果不关闭,那么就会导致fd被耗尽
注意:
size参数告诉内核这个epoll对象会处理的事件⼤致数量,⽽不是能够处理的事件的最⼤数(同时,size不要传0,会报invalid argument错误)。
在现在linux版本中,这个size参数已经没有意义了;
返回: epoll对象句柄;之后针对该epoll的操作需要通过该句柄来标识该epoll对象;
【2. 管理内核事件表:】
可以对要监听的事件进行操作:注册,删除,修改;
返回: 0表示成功, -1表示错误,根据errno错误码判断错误类型。
原型如下:
- #include
- int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
- //成功返回0,失败返回-1.
参数:
- EPOLL_CTL_ADD //注册新的文件描述符到内核事件表epfd中
- EPOLL_CTL_DEL //从内核事件表中删除文件描述符
- EPOLL_CTL_MOD //修改文件描述符
- struct epoll_event
- {
- short events;//事件类型:在每一个poll的事件类型标识前加个‘E’
- Union epoll_data_t data(联合体,用到其中的fd成员即文件描述符,其他的不用)
- }
- typedef union epoll_data {
- void *ptr; /* 指向用户自定义数据 */
- int fd; /* 注册的文件描述符 */
- uint32_t u32; /* 32-bit integer */
- uint64_t u64; /* 64-bit integer */
- } epoll_data_t;
event.events 取值:
- EPOLLIN 表示该连接上有数据可读(tcp连接远端主动关闭连接,也是可读事件,因为需要处理发送来的FIN包; FIN包就是read 返回 0)
- EPOLLOUT 表示该连接上可写发送(主动向上游服务器发起⾮阻塞tcp连接,连接建⽴成功事件相当于可写事件)
- EPOLLRDHUP 表示tcp连接的远端关闭或半关闭连接
- EPOLLPRI 表示连接上有紧急数据需要读
- EPOLLERR 表示连接发⽣错误
- EPOLLHUP 表示连接被挂起
- EPOLLET 将触发⽅式设置为边缘触发,系统默认为⽔平触发
- EPOLLONESHOT 表示该事件只处理⼀次,下次需要处理时需重新加⼊epoll
假如现在我们将监听文件描述符添加到内核事件表中:
- struct epoll_event event;
- event.events=EPOLLIN;//监听读事件
- event.data.fd=listenfd;//被监听的文件描述符
- int res=epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&event);//将event结构体中存储的事件类型添加到内核事件表中。
【3. 开始监听内核事件表的事件】
- int epoll_wait(int epfd,struct epoll_event events[],int maxevents,int timeout)
- // 成功返回就绪个数,失败-1,超时0
参数:
收集 epoll 监控的事件中已经发⽣的事件,如果 epoll 中没有任何⼀个事件发⽣,则最多等待timeout 毫秒后返回。
返回:表示当前发⽣的事件个数
返回0表示本次没有事件发⽣;
返回-1表示出现错误,需要检查errno错误码判断错误类型。
【1. 速度快:】
【 2. 查找时间复杂度低:】
【3. 采取回调函数方式处理就绪事件】
因为epoll封装了很多函数,所以操作起来比起selet和poll代码简洁了很多
服务器代码如下:
- #include
- #include
- #include
- #include
- #include
- //网络头文件
- #include
- #include
- #include
- #include
- #include
- #define MAX 10//定义最大连接数为10个
-
- int InitSocket()
- {
- int sockfd = socket(AF_INET,SOCK_STREAM,0);
- if(sockfd == -1) return -1;
- struct sockaddr_in ser;//指明地址信息,是一种通用的套接字地址
- memset(&ser,0,sizeof(ser));
- ser.sin_family = AF_INET;//设置地址家族
- ser.sin_port = htons(6000);//设置端口
- ser.sin_addr.s_addr = inet_addr("127.0.0.1");//设置地址
- int res = bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));//绑定端口号和地址
- if(res == -1) return -1;
- res = listen(sockfd,5);
- if(res == -1) return -1;
- return sockfd;
- }
- void epoll_add(int epfd,int fd)
- {
- struct epoll_event ev;
- ev.events=EPOLLIN;//读
- ev.data.fd=fd;
- if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)==-1)
- {
- printf("epoll add failed\n");
- }
- }
- void epoll_del(int epfd,int fd)
- {
- if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)==-1)
- {
- printf("epoll del failed\n");
- }
-
- }
- void accept_client(int epfd,int sockfd)
- {
- struct sockaddr_in caddr;
- int len=sizeof(caddr);
- int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
- if(c<0)
- {
- return ;
- }
- printf("accpet c=%d ip=%s\n",c,inet_ntoa(caddr.sin_addr));
- epoll_add(epfd,c);
- }
- void recv_data(int epfd,int c)
- {
- char buff[128]={0};
- int num=recv(c,buff,127,0);
- if(num<=0)//如果num==0说明客户端结束了描述符号
- {
- epoll_del(epfd,c);//移除改客户端对应的描述符
- close(c);
- printf("client close\n");
-
- return ;
- }
- printf("buff (%d)=%s\n",c,buff);
- send(c,"ok",2,0);
-
- }
-
- int main()
- {
- int sockfd = InitSocket();//监听套接字,有客户端链接时就会触发读事件。
- assert(sockfd != -1);
- //创建内核事件表
- int epfd=epoll_create(MAX);//底层,红黑树
- if(epfd==-1)
- {
- exit(1);
- }
- epoll_add(epfd,sockfd);//将监听套接子添加到内核事件表
- struct epoll_event evs[MAX];//用来接收就绪的文件描述符
- while(1)
- {
- int n=epoll_wait(epfd,evs,MAX,5000);
- if(n==-1)
- {
- printf("err\n");
- }
- else if(n==0)
- {
- printf("time out\n");
- }
- else
- {//前n个元素是数据就绪的
- for(int i=0;i
- {
- int fd=evs[i].data.fd;
- if(evs[i].events&EPOLLIN)//看读事件是不是就绪
- {
- if(fd==sockfd)
- {
- accept_client(epfd,sockfd);
- }
- else
- {
- recv_data(epfd,fd);
-
- }
-
- }
- //if(evs[i].events&EPOLLOUT)
- }
-
- }
- }
- close(sockfd);
- }
客户端的代码都是一样的,这里我就不粘了

ヾ(◍°∇°◍)ノ゙