相信大家都知道,poll和select可以同时进行多个文件描述符的监听与处理,那么为什么还要采用epoll方式呢?
一:为什么使用epoll
随着客户端的增加,这两个函数需要将用户空间的数据拷贝到内核空间,在内核中遍历轮询,这就相当耗时,容易出现瓶颈,而epoll函数就不需要用户态与内核态的切换,并且没有采用轮询方式,但如果文件描述符数目众多,但就绪数目少时,epoll函数就显示出了其独特优势,它是在每个文件描述上注册一个回调函数,当这个文件描述符上发生了我们所关心的事件时,就将其添加到就绪队列中进行处理,非轮询方式。
二:epoll系统调用
1. 三个函数
int epoll_create(int size): 创建一个内核事件表,底层为红黑树
int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event):将我们关心的文件描述符添加到内核事件表中,op可取添加,删除以及修改操作
int epoll_wait(int epfd,struct epoll_event *event,int maxevents,int timeout):将我们关心的文件描述符上已经准备就绪的文件描述符返回给我们,返回就绪的文件描述符个数,该函数如果检测到事件,就将所有就绪事件从内核事件表中复制到他的第二个参数指向的数组中,这个数组只用于输出epoll_wait检测到的就绪事件,而不像select与poll的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件。
2. 在关闭连接前,需要先将文件描述符从内核事件表中删除,顺序不能颠倒,一旦颠倒,已经关闭的文件描述符要从内核事件中删除,就会造成Bad file descriptor错误
3. ET与LT模式
ET模式为高效模式,它不同于LT模式的地方就在于,在epoll_wait检测到数据就绪并将此事件通知给应用程序之后,应用程序必须立即处理该事件,因为后续该函数调用便不再向应用程序通知这一件事,所以ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,效率要高一些。
LT模式为默认工作模式,当epoll_wait检测到数据就绪并将此事件通知给应用程序之后,应用程序不必立即处理该事件,因为后续该函数调用还会一直向应用程序通知这一件事,直到该事件被处理。
所以当客户端输入一个字符串,而服务器端每次只接收一个字符时,在默认得LT模式下,便是输出字符串长度加1次之后才会进行下一步,而在ET模式下,可以只接收一个字符,然后便不再提醒,直接进行下一步,可能在下一步之后才会再输出第一次的剩余数据。但ET模式易造成数据丢失,所以我们可以在服务器端循环以一个字符接收并打印的过程,但当所有数据接收完成之后,recv函数便会阻塞,从而导致无法进行下一步,所以再循环同时,还应设置使recv为非阻塞模式。
4. 使用epoll系统调用的服务器端代码
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include<sys/select.h>
#include<sys/time.h>
#include<sys/types.h>
#include<sys/epoll.h>
#define MAX 30
int create_socket();
void epoll_add(int epfd,int fd);//add a events to inside events table
void epoll_del(int epfd,int fd);//delete a sockfd from inside events table
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)
{
perror("epoll_add error\n");
}
}
void epoll_del(int epfd,int fd)
{
if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)==-1)
{
perror("epoll_del error\n");
}
}
int main()
{
int sockfd=create_socket();
assert(sockfd!=-1);
int epfd=epoll_create(MAX);
assert(epfd!=-1);
struct epoll_event events[MAX];
epoll_add(epfd,sockfd);
while(1)
{
int n=epoll_wait(epfd,events,MAX,5000);
if(n==-1)
{
perror("epoll_wait error\n");
continue;
}
else if(n==0)
{
printf("time out\n");
continue;
}
else
{
int i=0;
for(;i<n;++i)
{
if(events[i].events & EPOLLIN)
{
if(events[i].data.fd==sockfd)
{
struct sockaddr_in caddr;
int len=sizeof(caddr);
int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
if(c<0)
{
continue;
}
printf("accept ::c=%d\n",c);
epoll_add(epfd,c);
}
else
{
char buff[128]={0};
if(recv(events[i].data.fd,buff,127,0)<=0)//why we can not read many times?because once no data,the recv will block,can not over
{
printf("one client over\n");
//close(events[i].data.fd);
epoll_del(epfd,events[i].data.fd);
close(events[i].data.fd);
continue;
}
printf("read(%d)=%s\n",events[i].data.fd,buff);
send(events[i].data.fd,"ok",2,0);
}
}
}
}
}
}
int create_socket()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
assert(sockfd!=-1);
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family=AF_INET;
saddr.sin_port=htons(6000);
saddr.sin_addr.s_addr=inet_addr("192.168.31.44");
int res = bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));
assert(res!=-1);
listen(sockfd,5);
return sockfd;
}