一、EPOLL的函数接口
1、监听树节点
#include <sys/epoll.h>
/* 监听树节点 */
struct epoll_event
{
events = //设置监听事件 EPOLLIN |EPOLLOUT
struct data
{
int sockfd //监听的sockfd
}
}
2、监听集合
/*创建epoll监听集合[红黑树] , 成功返回树文件描述符*/
int epfd = epoll_create(int max); //epoll监听数量
3、监听树的增删改查
epoll_ctl(int epfd , int cmd , int sockfd , struct epoll_event * env);
参数介绍:
epfd = 监听树的文件描述符
cmd = EPOLL_CTL_ADD [监听树节点添加] EPOLL_CTL_DEL [监听树节点删除] EPOLL_CTL_MOD [监听树节点修改]
sockfd = 与监听树中某个监听节点对应
env = 监听树节点,用于设置监听
4、Epoll模型的监听函数
int readycode = epoll_wait(int epfd , struct epoll_event * readyarr, int maxevents , int timeout);
参数介绍:
epfd = 设置监听树
readyarray = 就绪队列,有事件就绪,内核传出就绪的节点到此队列中(遍历处理即可) , 就绪队列大小一般与最大监听数一致
maxevents = 最大就绪数 ,一般与最大监听数一致
timeout = 等待超时, 可以根据需求改变epoll工作模式
timeout = -1 阻塞等待监听
timeout = 0 非阻塞监听
timeout > 0 定时阻塞
二、EPOLL模型服务端代码
#include<SOCKET_API.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8000
#define BUFSIZE 1500
#define TIMEOUT 1
#define BACKLOG 128
#define SETSIZE 1024
#define EPOLLSIZE 200000
int main()
{
//网络初始化
struct sockaddr_in serveraddr,clientaddr;
bzero(&serveraddr,sizeof(serveraddr));
serveraddr.sin_family=AF_INET;
serveraddr.sin_port=htons(SERVER_PORT);
inet_pton(AF_INET,SERVER_IP,&serveraddr.sin_addr.s_addr);
int serverfd=SOCKET(AF_INET,SOCK_STREAM,0);
BIND(serverfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr));
LISTEN(serverfd,BACKLOG);
//业务处理数据
int recvsize;
char buffer[BUFSIZE];
bzero(buffer,sizeof(buffer));
int flags;
int clientfd;
socklen_t addrlen;
//epoll初始化
int readycode;
int epfd;
struct epoll_event listen_set[EPOLLSIZE]; //epoll就绪队列
epfd = epoll_create(EPOLLSIZE); //创建监听树
struct epoll_event lnode; //监听节点
lnode.events = EPOLLIN;
lnode.data.fd = serverfd;//设置监听节点
//添加监听节点
epoll_ctl(epfd,EPOLL_CTL_ADD,serverfd,&lnode);
printf("epoll server Waiting..\n");
while(TIMEOUT)
{
readycode = epoll_wait(epfd,listen_set,EPOLLSIZE,-1);
while(readycode)//循环处理就绪
{
if(listen_set[readycode-1].data.fd == serverfd)
{
//serverfd就绪?建立连接
addrlen = sizeof(clientaddr);
clientfd = ACCEPT(serverfd,(struct sockaddr*)&clientaddr,&addrlen);
//对clientfd设置监听
lnode.data.fd = clientfd;
lnode.events = EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&lnode);
}
else
{
//clientfd就绪?处理业务
if((recvsize=RECV(listen_set[readycode-1].data.fd,buffer,sizeof(buffer),0))>0)
{
printf("epoll server recv request size = %d\n",recvsize);
flags = 0;
while(flags < recvsize)
{
buffer[flags] = toupper(buffer[flags]);
flags++;
}
int sendsize = SEND(listen_set[readycode-1].data.fd,buffer,recvsize,0);
printf("epoll server send response size = %d\n",sendsize);
bzero(buffer,sizeof(buffer));
}
else if(recvsize == 0)//客户端退出
{
close(listen_set[readycode-1].data.fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,listen_set[readycode].data.fd,NULL);//delete
}
}
readycode--;
}
}
return 0;
}
三、EPOLL的工作流程
当我们有了监听树以后,监听树上有监听节点,每一个监听节点除了有指定的sockfd和设置的监听事件以外,还有一个ep_callback()回调函数,(select poll epoll)监听网络IO的,网络IO到底是谁来监听,别人发数据,谁知道网络事件就绪,网络设备,网卡或者是驱动层接口,epoll摒弃了传统的IO设备等待队列,直接用监听节点与网络设备绑定 ,每当向监听树上添加一个节点,这个节点的ep_callback()就会被注册到网络设备上的驱动层,sockfd与网络设备绑定 ,将节点中的回调函数注册到网络设备中。假设现在有某个节点的ep_callback()已经注册到网络设备上了,某一时刻节点产生就绪事件,网络设备监听到就会调用这个节点对应的回调函数,将就绪事件传出到就绪链表(双向链表)中(内核层的就绪链表,而非用户层的就绪队列)。就绪事件epitem类型,是一个结构体类型,我们可以简单理解为由监听节点和其他部分组成,某个节点就绪就通过回调函数将这个节点传出到就绪链表中。epoll模型不需要轮询,只需要检测就绪链表是否为空即可,为空就睡眠等待,不为空就返回就绪。就绪链表与就绪队列也有一层关系,需要将就绪链表中的内容拷贝一份到就绪队列中,拷贝过程中会有开销,为了使开销更小,效率更高,利用MMAP将内核层的就绪事件传到用户层,内存共享映射,而不是传统的copy拷贝。
四、EOLL模型的优缺点
1、优点
1)采用的是网络设备绑定+回调机制监听反馈事件,不需要轮询监听,效率高开销小,没有监听数量限制 ,可以实现百万级监听(监听能力出众)。
2)当epoll模型监听到sockfd事件就绪,可以直接传出就绪的sockfd到就绪队列,用户只需要遍历就绪队列依次处理就绪即可(使用方便)。
3)相比SELECT与POLL,EPOLL业务处理更及时,其一不需要轮询监听(延迟) ,其二不需要查找就绪(延迟) ,大大提高业务处理的实时性。
4)EPOLL的监听树是在内核空间创建的, 用户使用时只需要将准备的节点,拷贝挂载的内核监听树中即可, 而且监听树内部有去重机制, 最大限度的保证每个节点只拷贝一次并且只挂载一次(减少不必要的系统开销)。
5)EPOLL兼容性比较好, LINUX与UNIX平台兼容出色
2、弊端
EPOLL监听核心是红黑树(近似平衡二叉树),如果对监听集合访问较频繁,会导致系统开销增大 (维护监听树的成本较高)。
五、单进程IO复用模型
1)单进程模型中, 客户端的请求业务不能过于复杂, 如果过于复杂导致服务端进程处理时间过长(服务端其他业务停摆)。
2)如果业务处理简单(ms,us内可以处理完毕) 秒内也可以处理大量客户端业务。
3)单进程模型需要对业务复杂度进行限制,还要对业务量进行限制。(不能很好提供服务)