就好比接收快递一样,select就是快递员上门挨家挨户遍历去问有没有快递,而epoll不同,epoll就好比在楼下放一个箱子,然后谁有快递就将快递放在楼下箱子里面,也就是那个事件触发,就在箱子里面遍历就行。
使用epoll的好处
- 没有文件描述符的限制
这个可以达到几万的大小,远远超过了select所能承载的数量 - 工作效率不会随着文件描述符的增加而下降
对于select而言,文件描述符越多,在应用层会遍历所有的文件描述符,那么工作效率就会随着文件描述符的增加而下降,而epoll不会 - Epoll经过系统优化更高效
epoll事件的触发模式
Level Trigger没有处理反复发送
第一种是水平触发,socket接收缓冲区不为空,有数据可读,读事件一直触发,socket发送缓冲区不满 可以继续写入数据 写事件一直触发。
Edge Trigger只发送一次(边缘出发)
socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件,socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件,边沿触发仅触发一次,水平触发会一直触发。
epoll 常用API
epoll_create 函数
其实就是上面说的创建的一个箱子。这个其实他就是一个链表,只要size>0就行
#include <sys/epoll.h>
int epoll_create(int size);
创建一个指示 epoll 内核事件表的文件描述符,该描述符将用作其他 epoll 系统调用的第一个参数,此处的 size 参数不起作用。
epoll_ctl 函数
这个就是操作那栋楼的住户,可以增加、删除住户。一开始就是要把服务器socket加入进去。
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
该函数用于操作内核事件表监控的文件描述符上的事件:注册、修改、删除:
epfd:为 epoll_create 的句柄;
op:表示动作,用 3 个宏来表示:
- EPOLL_CTL_ADD:注册新的 fd 到 epfd;
- EPOLL_CTL_MOD:修改已经注册的 fd 的监听事件;
- EPOLL_CTL_DEL:从 epfd 删除一个 fd;
event:告诉内核需要监听的事件。
其中,event 是 epoll_event 结构体指针类型,表示内核监听的事件,具体定义如下:
struct epoll_event {
__uint32_t events;
epoll_data_t data;
};
events 描述事件类型,其中 epoll 事件类型有以下几种:
- EPOLLIN:表示对应的文件描述符可读(包括对端SOCKET正常关闭)
- EPOLLOUT:表示对应的文件描述符可写;
- EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
- EPOLLERR:表示对应的文件描述符发生错误;
- EPOLLHUP:表示对应的文件描述符被挂断;
- EPOLLLET:将 EPOLL 设置为边缘触发(ET)模式;
- EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列中。
epoll_wait 函数
就是检查那个箱子里面发生事件的socket。然后循环也是循环epoll_wait返回的socket.
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
该函数用于等待所监控的文件描述符上有事件的产生,返回就绪的文件描述符的个数。
events:用来存储内核得到的事件的集合;
maxevents:告知内核这个 events 有多大,这个值不能大于创建 epoll_create() 时的大小;
timeout:超时时间:
-1
:阻塞;0
:立即返回,非阻塞;>0
:指定毫秒数;
返回值:成功返回有多少文件描述符就绪,时间到时返回 0,出错时返回-1。
epoll的事件
- EPOLLET
epoll默认是水平触发,让水平触发变成边缘触发,用这个事件 - EPOLLIN和EPOLLOUT
这个两个是有数据来和往外写 - EPOLLPRI
出现中断出现问题的时候 - EPOLLERR
EPOLL读写的时候出现问题了 - EPOLLHUP
挂起的时候会出现
epoll重要的结构体
typedef union epoll_data {//联合体,只能用一个
void *ptr;
int fd;
uint32_tu32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events,就是epollin,epollout,epollerror等*/
epoll_data_t data; /* User data variable ,这里也就是socket_fd*/
};
主要代码
对于select和epoll我们首先都是要先将监听的那个socket加入进去,也就是这栋楼第一个住户。首先要创建一个epoll,然后把一开始创造的那个socket加入到epoll中去。
//创建epoll
epoll_fd = epoll_create(256);
//先将侦听的socket_fd添加进去,然后再将与数据通讯的客户端的socket_fd添加进去
ev.events=EPOLLIN;//对于侦听的这个事件来说就是输入,就是in,这个一般不变成边缘触发,为了保证所有来的都能连上
ev.data.fd=socket_fd;//这个就是文件描述符socket
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,socket_fd,&ev);
然后利用epoll_wait判断发生事件的个数,也就是箱子里面发生事件的socket。
event_number = epoll_wait(epoll_fd,events,MAX_EVENTS,TIMEOUT);//发生事件的个数
然后判断是那个events[i]发生的事件
如果是那个侦听的那个一开始的socket发生了事件,那说明是来了新的socket需要接收,然后需要将这个新来的accept_fd加入到epoll中,一开始就一个监听的socket,点击连接之后新来的住户。调用的是epoll_ctl(epoll_fd,EPOLL_CTL_ADD,accept_fd,&ev);//将accept_fd添加到epoll中去
注意这里加入的是一个结构体struct epoll_event ev
。
这个结构体ev有两个数据,一个是ev.events
就是事件,比如说EPOLLIN,EPOLLOUT等。ev.data.fd
就是socket_fd。
if(events[i].data.fd==socket_fd){//如果这个是侦听的socket发生事件了,那么说明是来了新的连接
socklen_t addr_len=sizeof(struct sockaddr);
accept_fd = accept(socket_fd,
(struct sockaddr *)&remoteaddr,
&addr_len);
//设置成非阻塞
//创建了socket之后我们要设置成异步的
flags = fcntl(accept_fd,F_GETFL,0);
//然后设置成非阻塞
fcntl(accept_fd,F_SETFL,flags | O_NONBLOCK);
ev.events=EPOLLIN | EPOLLET;//|上边缘触发
ev.data.fd=accept_fd;
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,accept_fd,&ev);//将accept_fd添加到epoll中去
}
但是如果是已经连接的events[i]发生了事件,那说明是收发消息
else if(events[i].events&EPOLLIN){//这里只介绍读的
do{
memset(in_buff, 0, sizeof(in_buff));
//接收消息
ret = recv(events[i].data.fd,(void *)in_buff,MESSAGE_LEN,0);
if(ret==0){
close(events[i].data.fd);
}
if(ret==MESSAGE_LEN){//缓冲区满了
std::cout<<"maybe have data..."<<std::endl;
}
}while(ret<-1&&errno==EINTR);
if(ret<0){
switch(errno){
case EAGAIN:
break;
dafault:
break;
}
}
if(ret>0){//打印信息
std::cout<<"receive messaage:"<<in_buff<<std::endl;
//返回消息
send(events[i].data.fd,(void*)in_buff,MESSAGE_LEN,0);
}
}
}
总的代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <iostream>
#include <fcntl.h>
#include <sys/types.h>
//端口
#define PORT 8888
#define MESSAGE_LEN 1024
#define MAX_EVENTS 20
#define TIMEOUT 500
int main(int argc,char* argv[]){
int ret=-1;
int on=1;
int backlog=10;//缓冲区大小
int socket_fd,accept_fd;
struct sockaddr_in localaddr,remoteaddr;
char in_buff[MESSAGE_LEN]={0,};
int epoll_fd;
struct epoll_event ev,events[MAX_EVENTS];//epoll中event的结构体
int event_number;
int flags;
socket_fd=socket(AF_INET,SOCK_STREAM,0);
if(socket_fd==-1){
std::cout<<"Failed to create socket!"<<std::endl;
exit(-1);
}
//创建了socket之后我们要设置成异步的
flags = fcntl(socket_fd,F_GETFL,0);
//然后设置成非阻塞
fcntl(socket_fd,F_SETFL,flags | O_NONBLOCK);
ret=setsockopt(socket_fd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
if(ret==-1){
std::cout<<"Failed to set socket options!"<<std::endl;
}
localaddr.sin_family=AF_INET;//地址族
localaddr.sin_port=htons(PORT);//端口号
localaddr.sin_addr.s_addr=INADDR_ANY;//这个就是0
bzero(&(localaddr.sin_zero), 8);
ret= bind(socket_fd,(struct sockaddr *)&localaddr,sizeof(struct sockaddr));//绑定
if(ret==-1){//绑定失败
std::cout<<"Failed to bind addr!"<<std::endl;
exit(-1);
}
ret = listen(socket_fd,backlog);//第二个是缓冲区大小,因为同一时间只能处理一个,其他都放在缓冲区
if(ret==-1){
std::cout<<"failed to listen socket!"<<std::endl;
exit(-1);
}
//创建epoll
epoll_fd = epoll_create(256);
//先将侦听的socket_fd添加进去,然后再将与数据通讯的客户端的socket_fd添加进去
ev.events=EPOLLIN;//对于侦听的这个事件来说就是输入,就是in,这个一般不变成边缘触发,为了保证所有来的都能连上
ev.data.fd=socket_fd;//这个就是文件描述符socket
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,socket_fd,&ev);
while(1){//等待连接
event_number = epoll_wait(epoll_fd,events,MAX_EVENTS,TIMEOUT);//发生事件的个数
for(int i=0;i<event_number;i++){//有多少个文件描述符发生事件了
if(events[i].data.fd==socket_fd){//如果这个是侦听的socket发生事件了,那么说明是来了新的连接
socklen_t addr_len=sizeof(struct sockaddr);
accept_fd = accept(socket_fd,
(struct sockaddr *)&remoteaddr,
&addr_len);
//设置成非阻塞
//创建了socket之后我们要设置成异步的
flags = fcntl(accept_fd,F_GETFL,0);
//然后设置成非阻塞
fcntl(accept_fd,F_SETFL,flags | O_NONBLOCK);
ev.events=EPOLLIN | EPOLLET;//|上边缘触发
ev.data.fd=accept_fd;
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,accept_fd,&ev);//将accept_fd添加到epoll中去
}else if(events[i].events&EPOLLIN){//这里只介绍读的
do{
memset(in_buff, 0, sizeof(in_buff));
//接收消息
ret = recv(events[i].data.fd,(void *)in_buff,MESSAGE_LEN,0);
if(ret==0){
close(events[i].data.fd);
}
if(ret==MESSAGE_LEN){//缓冲区满了
std::cout<<"maybe have data..."<<std::endl;
}
}while(ret<-1&&errno==EINTR);
if(ret<0){
switch(errno){
case EAGAIN:
break;
dafault:
break;
}
}
if(ret>0){//打印信息
std::cout<<"receive messaage:"<<in_buff<<std::endl;
//返回消息
send(events[i].data.fd,(void*)in_buff,MESSAGE_LEN,0);
}
}
}
}
std::cout<<"quit servet...\n"<<std::endl;
close(socket_fd);
return 0;
}
epoll可以放在多线程里面,这个是要加锁的,但是不推荐,加锁是对性能的一个消耗。