今天我们来了解一下关于epoll服务器的知识。
1、什么是epoll?
epoll是linux内核为处理大批量句柄而作了改进的poll,它是Linux下多路复用IO接口select/poll的增强版本,
epoll在底层实现了自己的高速缓存区,并且建立了一个红黑树用于存放socket,另外维护了一个就绪队列用来存放准备就绪的事件。
2、epoll的实现原理
首先我们来看3个函数
(1)epoll_create()
函数原型:
int epoll_create(int size);
此函数调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。
(2)epoll_ctl()
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
通过调用此函数向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。
(3)epoll_wait()
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
通过调用此函数收集在epoll监控中已经发生的事件。
当系统执行epoll_ create时,就创建了红黑树和就绪队列,执行epoll_ ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后调用回调函数,用于当中断事件来临时向就绪队列中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。
3、实现epoll服务器的具体代码
#include<stdio.h>
#include<sys/epoll.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>
#define SIZE 64
static void usage(const char*proc)
{
printf("Usage:%s [local_ip] [local_port]\n",proc);
}
int startup(const char *ip,int port)//创建套接字
{
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("socket");
exit(3);
}
int opt=1;//如果客户端在,服务器主动断开,服务器进入time_out状态导致链接断开,用setsockopt函数
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(port);
local.sin_addr.s_addr=inet_addr(ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
{
perror("bind");
exit(4);
}
if(listen(sock,10)<0)
{
perror("listen");
exit(5);
}
return sock;
}
int main(int argc,char *argv[])
{
if(argc!=3)
{
usage(argv[0]);
return 1;
}
int listen_sock=startup(argv[1],atoi(argv[2]));//监听套接字
int epfd=epoll_create(256);//创建epoll模型
if(epfd<0)
{
perror("epoll_create");
return 2;
}
printf("sock: %d ,epfd: %d\n",listen_sock,epfd);
//关心listen_socket
struct epoll_event ev;
ev.events=EPOLLIN;
ev.data.fd=listen_sock;
epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);//让红黑树有一个节点
int num =0;
int timeout =-1;
struct epoll_event revs[SIZE];
while(1)
{
switch(num=epoll_wait(epfd,revs,SIZE,timeout))//查找红黑树是否有就绪事件
{
case 0:
printf("timeout...\n");
break;
case -1:
perror("epoll_wait");
break;
default:
{ //至少有一个事件就绪
int i=0;
for(;i<num;i++)
{
int fd=revs[i].data.fd;
if(fd==listen_sock&& (revs[i].events&EPOLLIN))
//listen_sock ready!
{
struct sockaddr_in client;
socklen_t len =sizeof(client);
int new_sock=accept(listen_sock,(struct sockaddr*)&client,&len);
if(new_sock<0)
{
perror("accept");
continue;
}
printf("get a new client [%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
//添加文件描述符到epoll模型中
ev.events=EPOLLIN;
ev.data.fd=new_sock;
epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&ev);
}
else if(fd!=listen_sock)
//其他文件描述符
{
if(revs[i].events&EPOLLIN)//normal sock read ev ready!
{
char buf[1024];
ssize_t s=read(fd,buf,sizeof(buf)-1);
if(s>0)
{
buf[s]=0;
printf("%s",buf);
//read done!start write
ev.events=EPOLLOUT;
ev.data.fd=fd;
epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev);
}
else if(s==0)
{
close(fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
printf("client quit!\n");
}
else
{
close(fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
perror("read");
}
}
else if(revs[i].events&EPOLLOUT)//normal sock write ev ready!
{
const char *echo="HTTP/1.0 200 OK\r\n\r\n<html><hl>hello epoll!!</htl></html><br/>";
write(fd,echo,strlen(echo));
close(fd);//写完关闭链接
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
}
else//other ev
{
}
}
else
{
}
}
}
break;
}
}
close(listen_sock);
close(epfd);
return 0;
}
当代码运行成功时结果如下:
然后我们在自己的浏览器上输入自己的ip地址:端口号就可以看到如下结果:
4、epoll模型的优点
(1)epoll维护的描述符数目无上限,性能不会随着描述符数目的增加而下降
(2)epoll先通过epoll_ctl注册一个描述符到内核中,并一直维护着而不像poll每次操作都将所有要监控的描述符传递给内核
(3)在描述符读写就绪时,通过回掉函数将自己加入就绪队列中,之后epoll_wait返回该就绪队列,所以用户不需要遍历整个文件描述符判断哪些事件就绪。
(4)支持ET高效模式。
总结:
select,poll实现需要自己不断轮询所有事件集合直到设备就绪,select和poll要遍历整个fd集合,而epoll只要判断一下就绪队列是否为空就行了,这节省了大量的CPU时间,这就是回调机制带来的性能提升。