php网站开发 实战教程,网站建设技术人员要会什么,互联网站安全管理服务平台,seo在网站建设中的作用IO多路复用-epoll
1. 概述
epoll 全称 eventpoll#xff0c;是 linux 内核实现IO多路转接/复用#xff08;IO multiplexing#xff09;的一个实现。
epoll是select和poll的升级版#xff0c;相较于这两个前辈#xff0c;epoll改进了工作方式#xff0c;因此它更加高效…IO多路复用-epoll
1. 概述
epoll 全称 eventpoll是 linux 内核实现IO多路转接/复用IO multiplexing的一个实现。
epoll是select和poll的升级版相较于这两个前辈epoll改进了工作方式因此它更加高效。
对于待检测集合select和poll是基于线性方式处理的epoll是基于红黑树来管理待检测集合的。select和poll每次都会线性扫描整个待检测集合集合越大速度越慢epoll使用的是回调机制效率高处理效率也不会随着检测集合的变大而下降程序猿需要对select和poll返回的集合进行判断才能知道哪些文件描述符是就绪的通过epoll可以直接得到已就绪的文件描述符集合无需再次检测使用epoll没有最大文件描述符的限制仅受系统中进程能打开的最大文件数目限制
当多路复用的文件数量庞大、IO流量频繁的时候推荐使用epoll()。
2. 函数说明
在epoll中一共提供是三个API函数分别处理不同的操作
#include sys/epoll.h
// 创建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的数值就可以
函数返回值
失败返回-1成功返回一个有效的文件描述符通过这个文件描述符就可以访问创建的epoll实例了
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);函数参数
epfdepoll_create() 函数的返回值通过这个参数找到epoll实例op这是一个枚举值控制通过该函数执行什么操作 EPOLL_CTL_ADD往epoll模型中添加新的节点EPOLL_CTL_MOD修改epoll模型中已经存在的节点EPOLL_CTL_DEL删除epoll模型中的指定的节点 fd文件描述符即要添加/修改/删除的文件描述符eventepoll事件用来修饰第三个参数对应的文件描述符的指定检测这个文件描述符的什么事件 events委托epoll检测的事件 EPOLLIN读事件, 接收数据, 检测读缓冲区如果有数据该文件描述符就绪EPOLLOUT写事件, 发送数据, 检测写缓冲区如果可写该文件描述符就绪EPOLLERR异常事件 data用户数据变量这是一个联合体类型通常情况下使用里边的fd成员用于存储待检测的文件描述符的值在调用epoll_wait()函数的时候这个值会被传出。
函数返回值
失败返回-1成功返回0
epoll_wait()函数的作用是检测创建的epoll实例中有没有就绪的文件描述符。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);函数参数
epfdepoll_create() 函数的返回值, 通过这个参数找到epoll实例events传出参数, 这是一个结构体数组的地址, 里边存储了已就绪的文件描述符的信息maxevents修饰第二个参数, 结构体数组的容量元素个数timeout如果检测的epoll实例中没有已就绪的文件描述符该函数阻塞的时长, 单位ms 毫秒 0函数不阻塞不管epoll实例中有没有就绪的文件描述符函数被调用后都直接返回大于0如果epoll实例中没有已就绪的文件描述符函数阻塞对应的毫秒数再返回-1函数一直阻塞直到epoll实例中有已就绪的文件描述符之后才解除阻塞
函数返回值
成功 等于0函数是阻塞被强制解除了, 没有检测到满足条件的文件描述符大于0检测到的已就绪的文件描述符的总个数 失败返回-1
3. epoll的使用
3.1 操作步骤
服务器端
创建监听的套接字使用本地的IP与端口和监听的套接字进行绑定给监听的套接字设置监听创建epoll实例对象将用于监听的套接字添加到epoll实例中检测添加到epoll实例中的文件描述符是否已就绪并将这些已就绪的文件描述符进行处理
如果是监听的文件描述符和新客户端建立连接将得到的文件描述符添加到epoll实例中如果是通信的文件描述符和对应的客户端通信如果连接已断开将该文件描述符从epoll实例中删除
重复第6步的操作
3.2 示例代码
服务器:
//
// Created by 47468 on 2024/1/26.
//
#include arpa/inet.h
#include unistd.h
#include cstdio
#include cstdlib
#include iostream
#include sys/epoll.h
#include cstringusing namespace std;int main(){// 1.创建套接字int lfd socket(AF_INET, SOCK_STREAM, 0);if(lfd -1){perror(socket);exit(0);}// 2. 绑定 ip, portstruct 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;
}客户端代码不变
4. epoll的工作模式
4.1 水平模式
LTlevel triggered是缺省的工作方式并且同时支持block和no-block socket。
特点:
读事件如果文件描述符对应的读缓冲区还有数据读事件就会被触发epoll_wait()解除阻塞
也就是说只要都缓冲区里面有数据, 即使没处理, 他会一直通知
写事件也是一样, 只要写缓冲区可写, 就会一直触发
4.2 边沿模式
ETedge-triggered是高速工作方式只支持no-block socket
当文件描述符从未就绪变为就绪时内核会通过epoll通知使用者。然后它会假设使用者知道文件描述符已经就绪并且不会再为那个文件描述符发送更多的就绪通知only once
ET模式在很大程度上减少了epoll事件被重复触发的次数因此效率要比LT模式高。
特点:
读事件当读缓冲区有新的数据进入读事件被触发一次没有新数据不会触发该事件
读事件被触发可以通过调用read()/recv()函数将缓冲区数据读出如果数据没有被全部读走并且没有新数据进入读事件不会再次触发只通知一次如果数据被全部读走或者只读走一部分此时有新数据进入读事件被触发并且只通知一次
写事件当写缓冲区状态可写写事件只会触发一次
写缓冲区从不满到被写满期间写事件只会被触发一次写缓冲区从满到不满状态变为可写写事件只会被触发一次
综上所述epoll的边沿模式下 epoll_wait()检测到文件描述符有新事件才会通知如果不是新的事件就不通知通知的次数比水平模式少效率比水平模式要高。
4.3 边沿模式的设置
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个字节, 这样服务器就无法一次把数据全部接收完
客户端: 服务器: 也就是说每次有新的数据发送的时候, 服务器才能把原来缓冲区的数据读出来
4.4 边沿模式非阻塞设置
第一种方式是把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 cstdio
#include cstdlib
#include iostream
#include sys/epoll.h
#include cstring
#include fcntl.h
#include cerrnousing namespace std;int main(){// 1.创建套接字int lfd socket(AF_INET, SOCK_STREAM, 0);if(lfd -1){perror(socket);exit(0);}// 2. 绑定 ip, portstruct 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 -1if(errno EAGAIN){cout 数据接收完毕... endl;break;}perror(recv);exit(0);}}}}}close(lfd);return 0;}5. 多线程epoll边沿模式
大致思想跟select多线程通信其实是一样的
直接上代码
服务器端:
//
// Created by 47468 on 2024/1/26.
//
#include arpa/inet.h
#include unistd.h
#include cstdio
#include cstdlib
#include iostream
#include sys/epoll.h
#include cstring
#include fcntl.h
#include cerrno
#include pthread.h
using namespace std;struct socketInfo{int fd;int epfd;
};void* acceptConn(void* arg){// 打印一下线程idcout 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){// 打印一下线程idcout 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 -1if(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, portstruct 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 cstdio
#include cstring
#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;
}