epoll 全称 eventpoll,是 linux 内核实现IO多路转接/复用(IO multiplexing)的一个实现。
epoll是select和poll的升级版,相较于这两个前辈,epoll改进了工作方式,因此它更加高效。
当多路复用的文件数量庞大、IO流量频繁的时候,推荐使用epoll()。
在epoll中一共提供是三个API函数,分别处理不同的操作
#include
// 创建epoll实例,通过一棵红黑树管理待检测集合
int epoll_create(int size);
// 管理红黑树上的文件描述符(添加、修改、删除)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 检测epoll树中是否有就绪的文件描述符
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
select/poll低效的原因之一是将“添加/维护待检测任务”和“阻塞进程/线程”两个步骤合二为一。每次调用select都需要这两步操作,然而大多数应用场景中,需要监视的socket个数相对固定,并不需要每次都修改。epoll将这两个操作分开,先用epoll_ctl()维护等待队列,再调用epoll_wait()阻塞进程(解耦)。通过下图的对比显而易见,epoll的效率得到了提升。
epoll_create()函数的作用是创建一个红黑树模型的实例,用于管理待检测的文件描述符的集合。
int epoll_create(int size);
函数参数 size:指定一个大于0的数值就可以
函数返回值:
epoll_ctl()函数的作用是管理红黑树实例上的节点,可以进行添加、删除、修改操作。
// 联合体, 多个变量共用同一块内存
typedef union epoll_data {
void *ptr;
int fd; // 通常情况下使用这个成员, 和epoll_ctl的第三个参数相同即可
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数参数:
函数返回值:
epoll_wait()函数的作用是检测创建的epoll实例中有没有就绪的文件描述符。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
函数参数:
函数返回值:
服务器端
服务器:
//
// Created by 47468 on 2024/1/26.
//
#include "arpa/inet.h"
#include "unistd.h"
#include
#include "cstdlib"
#include "iostream"
#include
#include
using namespace std;
int main(){
// 1.创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(0);
}
// 2. 绑定 ip, port
struct sockaddr_in saddr{};
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
int res = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
if(res == -1){
perror("bind");
exit(0);
}
// 3. 监听
res = listen(lfd, 128);
if(res == -1){
perror("listen");
exit(0);
}
// 4. 创建epoll实例对象
int epfd = epoll_create(1);
// 5. 将用于监听的套接字添加到epoll实例中
epoll_event ev{};
ev.events = EPOLLIN;
ev.data.fd = lfd;
res = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
if(res == -1){
perror("epoll_ctl");
exit(0);
}
// 6. 检测添加到epoll实例中的文件描述符是否已就绪
// 并将这些已就绪的文件描述符进行处理
epoll_event evs[1024];
int size = sizeof(evs) / sizeof(evs[0]);
while (true){
int num = epoll_wait(epfd, evs, size, -1);
for (int i = 0; i < num; ++i){
// 取出当前的文件描述符
int fd = evs[i].data.fd;
if(fd == lfd){
// 有新客户端建立连接
sockaddr_in saddr{};
int len = sizeof(saddr);
int cfd = accept(lfd, (sockaddr *) &saddr, (socklen_t *) &len);
// 打印客户端信息
char ip[32];
cout << "有客户端建立连接, ip: "
<< inet_ntop(AF_INET, &saddr.sin_addr.s_addr, ip, sizeof(ip))
<< ", port: "
<< ntohs(saddr.sin_port)
<< endl;
// 把用于通信的套接字放到epoll实例中去
ev.data.fd = cfd;
ev.events = EPOLLIN;
res = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
if(res == -1){
perror("epoll_ctl-accept");
exit(0);
}
}
else{
// 是通信的文件描述符就绪
// 通信
char buf[1024];
memset(buf, 0, sizeof(buf));
ssize_t len = read(fd, buf, sizeof(buf));
if(len == 0){
cout << "客户端断开了连接" << endl;
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);
close(fd);
}
else if(len > 0){
cout << "client say: " << buf << endl;
for(int i = 0; i < len; ++i){
buf[i] = toupper(buf[i]);
}
write(fd, buf, len);
}
else{
perror("recv");
exit(0);
}
}
}
}
close(lfd);
return 0;
}
客户端代码不变
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket。
特点:
也就是说只要都缓冲区里面有数据, 即使没处理, 他会一直通知
写事件也是一样, 只要写缓冲区可写, 就会一直触发
ET(edge-triggered)是高速工作方式,只支持no-block socket
当文件描述符从未就绪变为就绪时,内核会通过epoll通知使用者。然后它会假设使用者知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知(only once)
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。
特点:
读事件:当读缓冲区有新的数据进入,读事件被触发一次,没有新数据不会触发该事件
写事件:当写缓冲区状态可写,写事件只会触发一次
综上所述:epoll的边沿模式下 epoll_wait()检测到文件描述符有新事件才会通知,如果不是新的事件就不通知,通知的次数比水平模式少,效率比水平模式要高。
epoll管理的红黑树示例中每个节点都是struct epoll_event类型,只需要将EPOLLET添加到结构体的events成员中即可:
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 设置边沿模式
在服务器端的代码改动:
// 把用于通信的套接字放到epoll实例中去
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET;
res = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
if(res == -1){
perror("epoll_ctl-accept");
exit(0);
}
这样的话, 把服务器端的buf该校一下, 每次只接受5个字节, 这样服务器就无法一次把数据全部接收完
客户端:
服务器:
也就是说每次有新的数据发送的时候, 服务器才能把原来缓冲区的数据读出来
第一种方式是把read函数放到while循环中一直读取, 只有有数据就读取
int len = 0;
while((len = recv(curfd, buf, sizeof(buf), 0)) > 0)
{
// 数据处理...
}
但这样的话有一个问题就是数据读取完了之后, 线程就阻塞在read函数中了, 无法继续向下运行, 所以我们需要把cfd这个文件描述符的状态设置为非阻塞
需要使用fcntl()
函数进行处理:
// 设置完成之后, 读写都变成了非阻塞模式
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
就是这样:
// 有新客户端建立连接
sockaddr_in saddr{};
int len = sizeof(saddr);
int cfd = accept(lfd, (sockaddr *) &saddr, (socklen_t *) &len);
// 把cfd设置为非阻塞模式
auto flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
// 打印客户端信息
char ip[32];
也就是说把解说数据的代码部分都放到一个while循环中
这是还存在一个问题就是, 当循环读取完客户端发来的数据后, 没数据的话read也不会阻塞, 而是直接返回-1, 这样的话会直接打印错误信息recv, 并退出程序, 这并不是我们想要的, 我们想在接受完一部分数据后, 跳出while循环, 并继续走上一层for循环, 检测有没有新的文件描述符就绪
我们先来运行程序看是什么错误信息, 根据错误信息进行判断什么时候break
我们可以看出来, 客户端之发送了一个dsa, 服务器接收到之后, 报错并直接退出
我们查一下这个错误信息, 看一下read函数的error
能看出是这个原因导致的, 所以我们在len==-1里面判断一下错误号即可:
这样就ok了
运行:
最后的服务器端的代码:
//
// Created by 47468 on 2024/1/26.
//
#include "arpa/inet.h"
#include "unistd.h"
#include
#include "cstdlib"
#include "iostream"
#include
#include
#include
#include
using namespace std;
int main(){
// 1.创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(0);
}
// 2. 绑定 ip, port
struct sockaddr_in saddr{};
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
int res = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
if(res == -1){
perror("bind");
exit(0);
}
// 3. 监听
res = listen(lfd, 128);
if(res == -1){
perror("listen");
exit(0);
}
// 4. 创建epoll实例对象
int epfd = epoll_create(1);
// 5. 将用于监听的套接字添加到epoll实例中
epoll_event ev{};
ev.events = EPOLLIN;
ev.data.fd = lfd;
res = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
if(res == -1){
perror("epoll_ctl");
exit(0);
}
// 6. 检测添加到epoll实例中的文件描述符是否已就绪
// 并将这些已就绪的文件描述符进行处理
epoll_event evs[1024];
int size = sizeof(evs) / sizeof(evs[0]);
while (true){
int num = epoll_wait(epfd, evs, size, -1);
for (int i = 0; i < num; ++i){
// 取出当前的文件描述符
int fd = evs[i].data.fd;
if(fd == lfd){
// 有新客户端建立连接
sockaddr_in saddr{};
int len = sizeof(saddr);
int cfd = accept(lfd, (sockaddr *) &saddr, (socklen_t *) &len);
// 把cfd设置为非阻塞模式
auto flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
// 打印客户端信息
char ip[32];
cout << "有客户端建立连接, ip: "
<< inet_ntop(AF_INET, &saddr.sin_addr.s_addr, ip, sizeof(ip))
<< ", port: "
<< ntohs(saddr.sin_port)
<< endl;
// 把用于通信的套接字放到epoll实例中去
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET;
res = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
if(res == -1){
perror("epoll_ctl-accept");
exit(0);
}
}
else{
// 是通信的文件描述符就绪
// 通信
char buf[5];
memset(buf, 0, sizeof(buf));
while (true) {
ssize_t len = read(fd, buf, sizeof(buf));
if (len == 0) {
cout << "客户端断开了连接" << endl;
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);
close(fd);
break;
} else if (len > 0) {
buf[len] = '\0';
cout << "client say: " << buf << endl;
for (int i = 0; i < len; ++i) {
buf[i] = toupper(buf[i]);
}
write(fd, buf, len);
} else {
// len == -1
if(errno == EAGAIN){
cout << "数据接收完毕..." << endl;
break;
}
perror("recv");
exit(0);
}
}
}
}
}
close(lfd);
return 0;
}
大致思想跟select多线程通信其实是一样的
直接上代码
服务器端:
//
// Created by 47468 on 2024/1/26.
//
#include "arpa/inet.h"
#include "unistd.h"
#include
#include "cstdlib"
#include "iostream"
#include
#include
#include
#include
#include
using namespace std;
struct socketInfo{
int fd;
int epfd;
};
void* acceptConn(void* arg){
// 打印一下线程id
cout << "acceptConn id: " << pthread_self() << endl;
auto* info = (socketInfo*)arg;
int lfd = info->fd;
int epfd = info->epfd;
// 有新客户端建立连接
sockaddr_in saddr{};
int len = sizeof(saddr);
int cfd = accept(lfd, (sockaddr *) &saddr, (socklen_t *) &len);
// 把cfd设置为非阻塞模式
auto flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
// 打印客户端信息
char ip[32];
cout << "有客户端建立连接, ip: "
<< inet_ntop(AF_INET, &saddr.sin_addr.s_addr, ip, sizeof(ip))
<< ", port: "
<< ntohs(saddr.sin_port)
<< endl;
// 把用于通信的套接字放到epoll实例中去
epoll_event ev{};
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET;
int res = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
if(res == -1){
perror("epoll_ctl-accept");
exit(0);
}
delete info;
return nullptr;
}
void* conmmunication(void* arg){
// 打印一下线程id
cout << "conmmunication id: " << pthread_self() << endl;
auto* info = (socketInfo*)arg;
int fd = info->fd;
int epfd = info->epfd;
// 是通信的文件描述符就绪
// 通信
char buf[1024];
memset(buf, 0, sizeof(buf));
while (true) {
ssize_t len = read(fd, buf, sizeof(buf));
if (len == 0) {
cout << "客户端断开了连接" << endl;
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);
close(fd);
break;
} else if (len > 0) {
buf[len] = '\0';
cout << "client say: " << buf << endl;
for (int i = 0; i < len; ++i) {
buf[i] = toupper(buf[i]);
}
write(fd, buf, sizeof(buf));
} else {
// len == -1
if(errno == EAGAIN){
cout << "数据接收完毕..." << endl;
break;
}
perror("recv");
break;
}
}
delete info;
return nullptr;
}
int main(){
// 1.创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(0);
}
// 2. 绑定 ip, port
struct sockaddr_in saddr{};
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
int res = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
if(res == -1){
perror("bind");
exit(0);
}
// 3. 监听
res = listen(lfd, 128);
if(res == -1){
perror("listen");
exit(0);
}
// 4. 创建epoll实例对象
int epfd = epoll_create(1);
// 5. 将用于监听的套接字添加到epoll实例中
epoll_event ev{};
ev.events = EPOLLIN;
ev.data.fd = lfd;
res = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
if(res == -1){
perror("epoll_ctl");
exit(0);
}
// 6. 检测添加到epoll实例中的文件描述符是否已就绪
// 并将这些已就绪的文件描述符进行处理
epoll_event evs[1024];
int size = sizeof(evs) / sizeof(evs[0]);
while (true){
int num = epoll_wait(epfd, evs, size, -1);
for (int i = 0; i < num; ++i){
auto* info = new socketInfo;
info->epfd = epfd;
info->fd = evs[i].data.fd;
pthread_t tid;
// 取出当前的文件描述符
int fd = evs[i].data.fd;
if(fd == lfd){
pthread_create(&tid, nullptr, acceptConn, info);
pthread_detach(tid);
}
else{
pthread_create(&tid, nullptr, conmmunication, info);
pthread_detach(tid);
}
}
}
close(lfd);
return 0;
}
客户端
//
// Created by 47468 on 2024/1/26.
//
#include "arpa/inet.h"
#include "unistd.h"
#include
#include
#include "cstdlib"
#include "iostream"
using namespace std;
int main(){
// 1. 创建用于通信的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1){
perror("socket");
exit(0);
}
// 2. 连接服务器
sockaddr_in saddr{};
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
inet_pton(AF_INET, "192.168.110.129", &saddr.sin_addr.s_addr);
int res = connect(fd, (sockaddr *) &saddr, sizeof(saddr));
if(res == -1){
perror("connet");
exit(0);
}
// 通信
while(true){
// 读数据
char readBuf[1024];
// 写数据
cout << "请输入要发送的字符串: " << endl;
cin.getline(readBuf, sizeof(readBuf));
// 发送数据到客户端
write(fd, readBuf, strlen(readBuf));
// 接收服务器发送的数据
ssize_t len = read(fd, readBuf, sizeof(readBuf));
// readBuf[len] = '\0';
cout << readBuf << endl;
}
close(fd);
return 0;
}