epoll 是一种在 Linux 系统上用于高效事件驱动编程的 I/O 多路复用机制。它相比于传统的 select 和 poll 函数具有更好的性能和扩展性。
epoll 的主要特点和原理:
1、事件驱动:epoll 是基于事件驱动的模型,它通过监听事件来触发相应的回调函数,而不是像传统的阻塞模型那样持续轮询。这样可以避免无效的轮询操作,提高效率。
2、高效:epoll 使用了红黑树(rbtree)和哈希表(hash table)的数据结构来存储和管理大量的文件描述符,使得在大规模连接的情况下,对文件描述符的管理和查找操作具备较高的效率。
3、边缘触发:epoll 提供了边缘触发(edge-triggered)的工作模式。在边缘触发模式下,只有当文件描述符的状态发生变化时,epoll 才会通知应用程序。这使得应用程序能够更精确地处理事件,避免了事件的丢失和重复触发。
4、扩展性:epoll 支持高并发的连接,可以同时监听大量的文件描述符,且不随文件描述符数量的增加而性能下降。它采用了事件通知的方式,只有在文件描述符发生状态变化时才会通知应用程序,避免了大量的轮询操作。
使用 epoll 的基本步骤如下:
1、创建 epoll 实例,通过调用 epoll_create 函数创建一个 epoll 对象。
2、将需要监听的文件描述符加入 epoll 实例,通过调用 epoll_ctl 函数将文件描述符添加到 epoll 中,并指定需要监听的事件类型。
epoll_ctl 是一个用于控制 epoll 实例的系统调用函数,它用于向 epoll 实例中添加、修改或删除文件描述符及其关联的事件。
3、 进入事件循环,调用 epoll_wait 函数等待事件发生。该函数会阻塞程序执行,直到有事件发生或超时。
epoll_wait 是一个用于等待事件的系统调用函数,它在 epoll 实例上进行阻塞等待,直到有事件就绪或超时。
4、 当 epoll_wait 返回时,根据返回的就绪事件进行相应的处理。可以通过遍历返回的事件列表来获取就绪的文件描述符和事件类型。
epoll 在网络编程中广泛应用,特别适用于高并发的服务器开发,能够处理大量的并发连接和高频率的 I/O 事件。它提供了高效的事件驱动模型,可以大大提升程序的性能和可扩展性。
一、epoll_create
int epoll_create(int size);
epoll_create 函数接受一个参数 size,该参数指定了 epoll 实例所能处理的最大文件描述符数目。它返回一个整数值,表示 epoll 实例的文件描述符,用于后续的 epoll 相关操作。
入参:
size 参数是一个提示值,用于告诉内核 epoll 实例需要处理的最大文件描述符数目。内核会根据此提示值进行一些优化,但实际上该值在大多数情况下并不会限制 epoll 实例所能处理的文件描述符数目。
返回值:
- 成功时返回一个非负整数,表示 epoll 实例的文件描述符。
- 如果调用失败,返回值为 -1,并设置相应的错误码,可以通过 errno 来获取具体的错误信息。
epoll_create 函数创建的 epoll 实例是默认的边缘触发模式(Edge Triggered Mode)。这意味着当文件描述符上的事件状态从未就绪变为就绪时,epoll 会返回该事件,而不是只在事件状态为就绪时返回一次。
在使用完 epoll 实例后,应当使用 close 函数显式关闭 epoll 实例的文件描述符,以释放相关资源。
二、epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
入参:
- epfd:表示 epoll 实例的文件描述符,即通过 epoll_create 创建的返回值。
- op:表示要进行的操作类型,可以是以下三种值之一:
- EPOLL_CTL_ADD:将文件描述符 fd 添加到 epoll 实例中,关联的事件由 event 参数指定。
- EPOLL_CTL_MOD:修改已添加到 epoll 实例中的文件描述符 fd 的关联事件,新的事件由 event 参数指定。
- EPOLL_CTL_DEL:从 epoll 实例中删除文件描述符 fd。
- fd:表示要添加、修改或删除的文件描述符。
- event:指向一个 struct epoll_event 结构体的指针,用于设置文件描述符的关联事件。
event 字段表示要关注的事件类型,可以是以下事件类型的组合:- EPOLLIN:表示可读事件。
- EPOLLOUT:表示可写事件。
- EPOLLRDHUP:表示对端关闭连接或关闭写端。
- EPOLLPRI:表示有紧急数据可读。
- EPOLLERR:表示发生错误。
- EPOLLHUP:表示连接关闭。
- EPOLLET:使用边缘触发模式(Edge Triggered Mode)。
- EPOLLONESHOT:在事件触发后,将文件描述符从 epoll 实例中删除,需要重新添加才能再次触发。
struct epoll_event {
__uint32_t events; // 表示要关注的事件类型
epoll_data_t data; // 用户数据,可以是文件描述符或指针
};
typedef union epoll_data {
void *ptr; // 指针类型的用户数据
int fd; // 文件描述符类型的用户数据
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
返回值:
- 成功时返回 0
- 失败时返回 -1,并设置相应的错误码,可以通过 errno 来获取具体的错误信息。
三、epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
入参:
- epfd:表示 epoll 实例的文件描述符,即通过 epoll_create 创建的返回值。
- events:指向一个 struct epoll_event 数组的指针,用于存储就绪的事件信息。
- maxevents:表示 events 数组的大小,即最多可以存储的事件数目。
- timeout:表示等待的超时时间,以毫秒为单位。可以是以下值之一:
- -1:表示永久阻塞,直到有事件就绪。
- 0:表示非阻塞,立即返回。
- 大于 0:表示超时时间,等待指定的毫秒数后返回。
返回值:
- epoll_wait 函数用于阻塞等待 epoll 实例上的事件就绪。当有事件就绪时,它将填充 events 数组,并返回就绪事件的数量。
- 如果 epoll_wait 函数返回值大于 0,则表示有就绪事件,并且可以通过遍历 events 数组来获取每个就绪事件的相关信息。
- 如果 epoll_wait 函数返回值为 0,表示超时时间到达,即没有事件就绪。
- 如果 epoll_wait 函数返回值为 -1,表示调用出错,可以通过 errno 来获取具体的错误信息。
四、代码实现
4.1、缓存大小大于发送数据的大小
#include <iostream>
//socket
#include <sys/types.h>
#include <sys/socket.h>
//close
#include <unistd.h>
//exit
#include <stdlib.h>
//perror
#include <stdio.h>
//memset
#include <string.h>
//htons
#include <arpa/inet.h>
/* According to earlier standards */
#include <sys/time.h>
//epoll
#include <sys/epoll.h>
#define PORT 8596
#define MESSAGE_SIZE 1024
#define FD_SIZE 1024
#define MAX_EVENTS 20
#define TIME_OUT 500
int main(){
int ret=-1;
int socket_fd=-1;
int accept_fd=-1;
int backlog=10;
int flags=1;
struct sockaddr_in local_addr,remote_addr;
struct epoll_event ev,events[FD_SIZE];
int epoll_fd=-1;
int event_number=0;
//create socket
socket_fd=socket(AF_INET,SOCK_STREAM,0);
if(socket_fd == -1){
perror("create socket error");
exit(1);
}
//set option of socket
ret = setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &flags, sizeof(flags));
if ( ret == -1 ){
perror("setsockopt error");
}
//set socket address
local_addr.sin_family=AF_INET;
local_addr.sin_port=htons(PORT);
local_addr.sin_addr.s_addr=INADDR_ANY;
bzero(&(local_addr.sin_zero),8);
//bind socket
ret=bind(socket_fd, (struct sockaddr *)&local_addr,sizeof(struct sockaddr_in));
if(ret == -1){
perror("bind socket error");
exit(1);
}
ret=listen(socket_fd, backlog);
if(ret ==-1){
perror("listen error");
exit(1);
}
//创建epoll
epoll_fd=epoll_create(256);
ev.data.fd=socket_fd;
ev.events=EPOLLIN;
//将socket_fd加入到epoll中
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,socket_fd,&ev);
//loop to accept client
for(;;){
event_number=epoll_wait(epoll_fd,events,MAX_EVENTS,-1);
for(int i=0;i<event_number;i++){
if(events[i].data.fd==socket_fd){
socklen_t addrlen = sizeof(remote_addr);
accept_fd=accept(socket_fd,( struct sockaddr *)&remote_addr, &addrlen);
ev.data.fd=accept_fd;
ev.events=EPOLLIN | EPOLLET;
//添加accept_fd到epoll中
if((epoll_ctl(epoll_fd,EPOLL_CTL_ADD,accept_fd,&ev))==-1){
close(accept_fd);
}
}else if(events[i].events & EPOLLIN){
//有数据可读
char in_buf[MESSAGE_SIZE];
memset(in_buf, 0, MESSAGE_SIZE);
//receive data
ret = recv( events[i].data.fd, &in_buf, MESSAGE_SIZE, 0 );
if(ret <= 0){
switch (errno){
case EAGAIN: //暂时没有数据
break;
case EINTR: //被终断
ret = recv(events[i].data.fd, &in_buf, MESSAGE_SIZE, 0);
break;
default:
printf("the client is closed, fd:%d\n", events[i].data.fd);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, &ev);
close(events[i].data.fd);
break;
}
}
printf("receive message:%s\n", in_buf);
send(events[i].data.fd, &in_buf, ret, 0);
}
}
}
printf("quit server....");
// 关闭监听 socket 和 epoll 实例
close(socket_fd);
close(epoll_fd);
return 0;
}
- 服务端
- 客户端1
- 客户端2
从代码直观就能看出epoll的代码量和逻辑实现相比select,都特别简洁。epoll 在网络编程中广泛应用,特别适用于高并发的服务器开发,能够处理大量的并发连接和高频率的 I/O 事件。它提供了高效的事件驱动模型,可以大大提升程序的性能和可扩展性。
4.2、缓存大小小于发送数据大小
4.2.1、线性触发(Level-Triggered,简称LT)
在线性触发模式下,当文件描述符上有可读、可写或异常事件发生时,epoll_wait() 函数会立即返回该事件,并且在下次调用 epoll_wait() 时仍然会返回该事件,直到应用程序处理完该事件或者文件描述符不可用(例如,读取缓冲区为空或写入缓冲区已满)为止。
#include <iostream>
//socket
#include <sys/types.h>
#include <sys/socket.h>
//close
#include <unistd.h>
//exit
#include <stdlib.h>
//perror
#include <stdio.h>
//memset
#include <string.h>
//htons
#include <arpa/inet.h>
/* According to earlier standards */
#include <sys/time.h>
//epoll
#include <sys/epoll.h>
#define PORT 8596
#define MESSAGE_SIZE 10
#define FD_SIZE 1024
#define MAX_EVENTS 20
#define TIME_OUT 500
int main(){
int ret=-1;
int socket_fd=-1;
int accept_fd=-1;
int backlog=10;
int flags=1;
struct sockaddr_in local_addr,remote_addr;
struct epoll_event ev,events[FD_SIZE];
int epoll_fd=-1;
int event_number=0;
//create socket
socket_fd=socket(AF_INET,SOCK_STREAM,0);
if(socket_fd == -1){
perror("create socket error");
exit(1);
}
//set option of socket
ret = setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &flags, sizeof(flags));
if ( ret == -1 ){
perror("setsockopt error");
}
//set socket address
local_addr.sin_family=AF_INET;
local_addr.sin_port=htons(PORT);
local_addr.sin_addr.s_addr=INADDR_ANY;
bzero(&(local_addr.sin_zero),8);
//bind socket
ret=bind(socket_fd, (struct sockaddr *)&local_addr,sizeof(struct sockaddr_in));
if(ret == -1){
perror("bind socket error");
exit(1);
}
ret=listen(socket_fd, backlog);
if(ret ==-1){
perror("listen error");
exit(1);
}
//创建epoll
epoll_fd=epoll_create(256);
ev.data.fd=socket_fd;
ev.events=EPOLLIN;
//将socket_fd加入到epoll中
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,socket_fd,&ev);
//loop to accept client
for(;;){
event_number=epoll_wait(epoll_fd,events,MAX_EVENTS,-1);
printf("epoll_wait:%d\n",event_number);
for(int i=0;i<event_number;i++){
if(events[i].data.fd==socket_fd){
socklen_t addrlen = sizeof(remote_addr);
accept_fd=accept(socket_fd,( struct sockaddr *)&remote_addr, &addrlen);
ev.data.fd=accept_fd;
ev.events=EPOLLIN;
//添加accept_fd到epoll中
if((epoll_ctl(epoll_fd,EPOLL_CTL_ADD,accept_fd,&ev))==-1){
close(accept_fd);
}
}else if(events[i].events & EPOLLIN){
//有数据可读
char in_buf[MESSAGE_SIZE];
memset(in_buf, 0, MESSAGE_SIZE);
//receive data
ret = recv( events[i].data.fd, &in_buf, MESSAGE_SIZE, 0 );
if(ret <= 0){
switch (errno){
case EAGAIN: //说明暂时已经没有数据了,要等通知
break;
case EINTR: //被终断
printf("recv EINTR... \n");
ret = recv(events[i].data.fd, &in_buf, MESSAGE_SIZE, 0);
break;
default:
printf("the client is closed, fd:%d\n", events[i].data.fd);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, &ev);
close(events[i].data.fd);
break;
}
}
printf("receive message:%s\n", in_buf);
send(events[i].data.fd, &in_buf, ret, 0);
}
}
}
printf("quit server....");
// 关闭监听 socket 和 epoll 实例
close(socket_fd);
close(epoll_fd);
return 0;
}
- MESSAGE_SIZE调整为10
- epoll默认使用线性触发(Level-Triggered,简称LT)
- 在线性触发的模式下,如果发送者发送的数据大于接收者缓存的大小,则会多次触发epoll_wait函数,多次读取数据,直至数据读完。
4.2.2、边缘触发(Edge-Triggered,简称ET)
在边缘触发模式下,epoll_wait() 函数只会在文件描述符上的状态发生变化时返回事件,例如由不可读变为可读或由不可写变为可写。如果文件描述符状态保持不变,epoll_wait() 不会返回该事件。
#include <iostream>
//socket
#include <sys/types.h>
#include <sys/socket.h>
//close
#include <unistd.h>
//exit
#include <stdlib.h>
//perror
#include <stdio.h>
//memset
#include <string.h>
//htons
#include <arpa/inet.h>
/* According to earlier standards */
#include <sys/time.h>
//epoll
#include <sys/epoll.h>
#define PORT 8596
#define MESSAGE_SIZE 10
#define FD_SIZE 1024
#define MAX_EVENTS 20
#define TIME_OUT 500
int main(){
int ret=-1;
int socket_fd=-1;
int accept_fd=-1;
int backlog=10;
int flags=1;
struct sockaddr_in local_addr,remote_addr;
struct epoll_event ev,events[FD_SIZE];
int epoll_fd=-1;
int event_number=0;
//create socket
socket_fd=socket(AF_INET,SOCK_STREAM,0);
if(socket_fd == -1){
perror("create socket error");
exit(1);
}
//set option of socket
ret = setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &flags, sizeof(flags));
if ( ret == -1 ){
perror("setsockopt error");
}
//set socket address
local_addr.sin_family=AF_INET;
local_addr.sin_port=htons(PORT);
local_addr.sin_addr.s_addr=INADDR_ANY;
bzero(&(local_addr.sin_zero),8);
//bind socket
ret=bind(socket_fd, (struct sockaddr *)&local_addr,sizeof(struct sockaddr_in));
if(ret == -1){
perror("bind socket error");
exit(1);
}
ret=listen(socket_fd, backlog);
if(ret ==-1){
perror("listen error");
exit(1);
}
//创建epoll
epoll_fd=epoll_create(256);
ev.data.fd=socket_fd;
ev.events=EPOLLIN;
//将socket_fd加入到epoll中
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,socket_fd,&ev);
//loop to accept client
for(;;){
event_number=epoll_wait(epoll_fd,events,MAX_EVENTS,-1);
printf("epoll_wait:%d\n",event_number);
for(int i=0;i<event_number;i++){
if(events[i].data.fd==socket_fd){
socklen_t addrlen = sizeof(remote_addr);
accept_fd=accept(socket_fd,( struct sockaddr *)&remote_addr, &addrlen);
ev.data.fd=accept_fd;
ev.events=EPOLLIN | EPOLLET;
//添加accept_fd到epoll中
if((epoll_ctl(epoll_fd,EPOLL_CTL_ADD,accept_fd,&ev))==-1){
close(accept_fd);
}
}else if(events[i].events & EPOLLIN){
//有数据可读
char in_buf[MESSAGE_SIZE];
memset(in_buf, 0, MESSAGE_SIZE);
//receive data
ret = recv( events[i].data.fd, &in_buf, MESSAGE_SIZE, 0 );
if(ret <= 0){
switch (errno){
case EAGAIN: //说明暂时已经没有数据了,要等通知
break;
case EINTR: //被终断
printf("recv EINTR... \n");
ret = recv(events[i].data.fd, &in_buf, MESSAGE_SIZE, 0);
break;
default:
printf("the client is closed, fd:%d\n", events[i].data.fd);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, &ev);
close(events[i].data.fd);
break;
}
}
printf("receive message:%s\n", in_buf);
send(events[i].data.fd, &in_buf, ret, 0);
}
}
}
printf("quit server....");
// 关闭监听 socket 和 epoll 实例
close(socket_fd);
close(epoll_fd);
return 0;
}
- ev.events=EPOLLIN | EPOLLET
如果想设置该文件描述符是边缘触发模式,则需要在后面增加一个EPOLLET的标识符 - 边缘触发模式下,只有文件描述符状态发生变化,才会触发epoll_wait
边缘触发(Edge-Triggered)模式相对于线性触发(Level-Triggered)模式具有以下优势:
-
精确性:边缘触发模式只在文件描述符状态发生变化时触发事件,而不是在文件描述符可读或可写时都触发事件。这意味着边缘触发模式更加精确,只有在状态变化时才会通知应用程序,可以避免一些不必要的事件通知。
-
高效性:由于边缘触发模式只在状态变化时触发事件,相比线性触发模式可以减少事件通知的次数,减少了不必要的系统调用。这在高并发、高吞吐量的系统中特别有效,能够提升系统的性能。
-
解决"惊群"效应:边缘触发模式可以有效解决"惊群"效应(Thundering Herd),即当有多个线程或进程等待同一个文件描述符上的事件时,线性触发模式下可能会导致多个线程或进程同时被唤醒处理同一个事件。而边缘触发模式只会唤醒一个线程或进程处理事件,可以避免"惊群"效应。
适用场景:
-
高性能网络服务器:对于高并发、高吞吐量的网络服务器,边缘触发模式能够更加高效地处理大量的连接和数据,并提升系统的性能。
-
实时系统:在实时系统中,边缘触发模式能够提供更精确的事件通知,确保及时处理关键任务和事件,满足实时性要求。
-
非阻塞式 I/O:当使用非阻塞式 I/O 进行事件驱动的编程时,边缘触发模式可以更好地控制事件的触发和处理,提供更高的灵活性和效率。
4.2.3、边缘触发(Edge-Triggered,简称ET)循环读数据
#include <iostream>
//socket
#include <sys/types.h>
#include <sys/socket.h>
//close
#include <unistd.h>
//exit
#include <stdlib.h>
//perror
#include <stdio.h>
//memset
#include <string.h>
//htons
#include <arpa/inet.h>
/* According to earlier standards */
#include <sys/time.h>
//epoll
#include <sys/epoll.h>
#include <fcntl.h>
#define PORT 8596
#define MESSAGE_SIZE 10
#define FD_SIZE 1024
#define MAX_EVENTS 20
#define TIME_OUT 500
void set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl");
exit(EXIT_FAILURE);
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl");
exit(EXIT_FAILURE);
}
}
int main(){
int ret=-1;
int socket_fd=-1;
int accept_fd=-1;
int backlog=10;
int flags=1;
struct sockaddr_in local_addr,remote_addr;
struct epoll_event ev,events[FD_SIZE];
int epoll_fd=-1;
int event_number=0;
//create socket
socket_fd=socket(AF_INET,SOCK_STREAM,0);
if(socket_fd == -1){
perror("create socket error");
exit(1);
}
//设置非阻塞
set_nonblocking(socket_fd);
//set option of socket
ret = setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &flags, sizeof(flags));
if ( ret == -1 ){
perror("setsockopt error");
}
//set socket address
local_addr.sin_family=AF_INET;
local_addr.sin_port=htons(PORT);
local_addr.sin_addr.s_addr=INADDR_ANY;
bzero(&(local_addr.sin_zero),8);
//bind socket
ret=bind(socket_fd, (struct sockaddr *)&local_addr,sizeof(struct sockaddr_in));
if(ret == -1){
perror("bind socket error");
exit(1);
}
ret=listen(socket_fd, backlog);
if(ret ==-1){
perror("listen error");
exit(1);
}
//创建epoll
epoll_fd=epoll_create(256);
ev.data.fd=socket_fd;
ev.events=EPOLLIN;
//将socket_fd加入到epoll中
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,socket_fd,&ev);
//loop to accept client
for(;;){
event_number=epoll_wait(epoll_fd,events,MAX_EVENTS,-1);
printf("epoll_wait:%d\n",event_number);
for(int i=0;i<event_number;i++){
int fd=events[i].data.fd;
if(fd==socket_fd){
socklen_t addrlen = sizeof(remote_addr);
accept_fd=accept(socket_fd,( struct sockaddr *)&remote_addr, &addrlen);
//设置非阻塞
set_nonblocking(accept_fd);
ev.data.fd=accept_fd;
ev.events=EPOLLIN | EPOLLET;
//添加accept_fd到epoll中
if((epoll_ctl(epoll_fd,EPOLL_CTL_ADD,accept_fd,&ev))==-1){
close(accept_fd);
}
}else if(events[i].events & EPOLLIN){
//有数据可读
char in_buf[MESSAGE_SIZE];
//receive data
while(1){
//清空缓存,避免数据脏读
memset(in_buf,0,MESSAGE_SIZE);
ret = recv( fd, &in_buf, MESSAGE_SIZE, 0 );
if(ret == 0){
printf("the client is closed, fd:%d\n", events[i].data.fd);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, &ev);
close(events[i].data.fd);
break;
}else{
if(errno == EWOULDBLOCK || errno == EAGAIN){
printf("read finished.....\n");
break;
}
}
printf("epollServer receive message:%s\n", in_buf);
}
// send(events[i].data.fd, &in_buf, ret, 0);
}
}
}
printf("quit server....");
// 关闭监听 socket 和 epoll 实例
close(socket_fd);
close(epoll_fd);
return 0;
}
- recv函数是阻塞函数,服务端一定要设置非阻塞,否则循环读以后会一直阻塞,其他客户端无法再连接服务端
- 每次读数据之前要清空缓存