[C++网络协议] 优于select的epoll

1.epoll函数为什么优于select函数

select函数的缺点:

  1. 调用select函数后,要针对所有文件描述符进行循环处理。
  2. 每次调用select函数,都需要向该函数传递监视对象信息。

对于缺点2,是提高性能的最大障碍。因为,套接字是操作系统来管理的,所以每次调用select函数,都会将要监视的对象信息传递给操作系统,这会对程序造成很大的负担。而且无法通过代码来解决,所以,缺点2是提高性能的最大障碍。

所以,有没有这么一种函数,仅向操作系统传递一次监控对象,当监视范围或内容发生变化时,只通知发生变化的事项呢?

答:epoll函数就具有问题里所说的功能。

适合select函数的使用情形:

  1. 系统需要具有兼容性。epoll函数是基于Linux系统的,而select函数几乎所有系统都有。
  2. 服务器端接入者少。

综上,epoll函数的优点:

  1. 无需编写以监视状态变化为目的的针对所有文件描述符的循环语句
  2. 调用对应于select函数的epoll_wait函数时,无需每次都传递监视对象信息,造成性能负担。

2.epoll函数

2.1 epoll_create函数

作用:创建保存epoll文件描述符的空间

#include <sys/epoll.h>int epoll_create(int size);    //size:epoll实例的大小
成功返回epoll文件描述符
失败返回-1

调用epoll_create函数时创建的文件描述符保存空间称为“epoll例程”。

size参数的传递,只是向操作系统提供建议,实际上操作系统会根据情况调整epoll例程的大小。更实际上的是,Linux2.6.8版本后的内核将完全忽略size参数。

注意:epoll_create函数创建的资源与套接字相同,都由操作系统来管理。所以返回的epoll文件描述符主要用于区分epoll例程的,需要终止时,也要和其他文件描述符相同,要调用close函数。

2.2 epoll_ctl函数

作用:向空间注册或注销文件描述符

#include<sys/epoll.h>
int epoll_ctl(
int epfd,                    //用于注册监视对象的epoll例程的文件描述符
int op,                      //用于指定监视对象的添加、删除、更改操作
int fd,                      //需要监视的文件描述符
struct epoll_event* event    //监视对象的事件类型
);
成功返回0,失败返回-1

参数epfd:指定epoll例程空间

参数op:

含义
EPOLL_CTL_ADD将文件描述符注册到epoll例程
EPOLL_CTL_DEL将文件描述符从epoll例程中删除,第四个参数填NULL
EPOLL_CTL_MOD更改注册的文件描述符的关注事件发生情况

参数fd:需要监视的文件描述符

参数event:

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
events常量(可以通过位或“|”运算符传递多个参数含义
EPOLLIN需要读取数据的情况
EPOLLOUT输出缓冲为空,可以立即发送数据的情况
EPOLLPRI收到OOB数据的情况
EPOLLRDHUP断开连接或半关闭的情况,边缘触发下很有用
EPOLLERR发生错误的情况
EPOLLET以边缘触发的方式得到事件通知
EPOLLONESHOT

发生一次事件后,相应文件描述符不再收到事件通知。

因此,需要调用epoll_ctl函数,运用第二个参数的EPOLL_CTL_MOD来再次设置事件

一般,epoll_event结构体里,只需要填写,events常量,和data联合体里的fd(要监视的文件描述符)即可。例如:

struct epoll_event event;
event.events=EPOLLIN;
event.data.fd=sockfd;
...
epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&event);

2.3 epoll_wait函数

作用:与select函数类似,等待文件描述符发生变化

#include<sys/epoll.h>int epoll_wait(
int epfd,                    //epoll例程指向的文件描述符
struct epoll_event* events,  //保存发生事件的文件描述符集合的结构体地址
int maxevents,               //第二个参数中可以保存的最大事件数
int timeout                  //以1/1000秒为单位的等待时间,传递-1,则会阻塞直到发生事件
);
成功返回发生事件的文件描述符数量
失败返回-1

参数epfd: 指定的epoll例程空间

参数events:

需要分配动态空间(malloc),一般来说分配动态空间时epoll_event结构体的最大可存放数量,就是参数maxevents的值。

参数max_events:events指向的空间里,最大可保存的epoll_event结构体的数量

参数timeout:超时时间。

例如:

#define EPOLL_SIZE 50
struct epoll_event* ep_events;
ep_events=(struct epoll_event*)malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
...
int event_cnt=epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);

3.条件触发和边缘触发

条件触发和边缘触发发生在epoll_wait函数时。

3.1 条件触发

含义:当输入缓冲中存有数据时,就会一直触发该事件。例如:当客户端传来20个字节的数据到服务器端,假设服务器端每次只读取4个字节,那么服务器端在客户端中20个字节的数据没有读取完之前,会一直触发EPOLLIN事件,即epoll_wait函数会一直将此客户端的文件描述符给填入到epoll_event结构体里的fd参数里。epoll函数和select函数默认是条件触发。

其实现思路和select函数是差不多的。

条件触发服务器端:

...//头文件
#define EPOLL_SIZE 50
#define READ_SIZE 5int main()
{......//这里是正常的socket、bind、listen函数int epollfd=epoll_create(EPOLL_SIZE);epoll_event serverevent;serverevent.events=EPOLLIN;serverevent.data.fd=serverfd;if(-1==epoll_ctl(epollfd,EPOLL_CTL_ADD,serverfd,&serverevent)){std::cout<<"server epoll_ctl fail!"<<std::endl;return 0;}int count;epoll_event* occurevent;occurevent=(epoll_event*)malloc(sizeof(epoll_event)*EPOLL_SIZE);while(1){count=epoll_wait(epollfd,occurevent,EPOLL_SIZE,-1);std::cout<<"触发EPOLLIN事件!"<<std::endl;for(int i=0;i<count;++i){if(occurevent[i].data.fd==serverfd){sockaddr_in clientAddr;memset(&clientAddr,0,sizeof(clientAddr));socklen_t clientAddrLen=sizeof(clientAddr);int clientfd=accept(serverfd,(sockaddr*)&clientAddr,&clientAddrLen);if(clientfd==-1){std::cout<<"accept fail!"<<std::endl;continue;}epoll_event tempevent;tempevent.events=EPOLLIN;tempevent.data.fd=clientfd;if(-1==epoll_ctl(epollfd,EPOLL_CTL_ADD,clientfd,&tempevent)){std::cout<<"epoll_ctl fail!"<<std::endl;continue;}}else{int clientfd=occurevent[i].data.fd;char buff[1024];int readLen=read(clientfd,buff,READ_SIZE);if(readLen>0){std::cout<<"客户端发来的消息:"<<buff<<std::endl;write(clientfd,buff,readLen);}else if(readLen==0){epoll_ctl(epollfd,EPOLL_CTL_DEL,clientfd,NULL);close(clientfd);}}}}close(serverfd);close(epollfd);return 0;
}

执行结果:

客户端:

服务器端:

可以看出服务器端一共触发了5次,第5次应该读的是‘/0’字符。

3.2 边缘触发

含义:当输入缓冲中接收到数据时,有且仅会触发一次事件。即使缓冲中的数据没有读取完,也不会再触发了,只有当缓冲中数据读取完全,下一次有数据传输到缓冲中时,才会再次触发epoll函数要设置成条件触发需要在调用epoll_wait函数时传入EPOLLET参数。

边缘触发服务器端需要注意以下两点:

     1.通过errno变量验证错误原因(因为边缘触发的特性,所以每次触发事件,都需要将缓冲中的数据给读完。)

        当缓冲中的数据读完时,read函数会返回-1,表示产生了一个错误,这时仅凭这些内容无法得到产生错误的原因,所以,Linux提供了一个全局变量:

#include<error.h>
int errno;

这个变量存储者错误的常量代码,即当缓冲中的数据读完时,read函数会返回-1,同时,errno变量会被赋值为EAGAIN常量。所以此时你可以用这个常量判断缓冲中的数据是否读完。

int res=read(...);
if(res>0)
{...
}
else if(res==-1&&errno==EAGAIN)    //表明缓冲中数据已经读完了
{...
}
else
{...
}

     2.要更改套接字特性,完成非阻塞I/O。

        在边缘触发方式下,以阻塞方式工作的read&write函数有可能会引起服务器端长时间的停顿。所以要完成非阻塞I/O。

那么怎么完成非阻塞I/O?

答:使用fcntl函数。

此函数在 [C++ 网络协议] 多种I/O函数里有说过,当时是修改recv函数的第四个参数,发送MSG_OOB带外数据时,作为接收方为了避免当有多个进程时不能判断是哪个进程要执行信号处理函数的问题,而将当前套接字的处理进程设置为主进程,代码如:

fcntl(clientfd,F_SETOWN,getpid());
#include<fcntl.h>
int fcntl(
int filedes,    //属性更改目标的文件描述符
int cmd,        //函数调用目的
...
);
成功返回cmd参数相关值
失败返回-1
cmd参数含义
F_GETFL获取filedes参数所指的文件描述符属性(int型,代表其属性)
F_SETFL设置其文件描述符属性

非阻塞I/O的属性是O_NONBLOCK。

使用示例:

int flag=fcntl(fd,F_GETFL,0);        //先获取当前文件描述符的属性
fcntl(fd,F_SETFL,flag|O_NONBLOCK);   //将文件描述符的属性和非阻塞IO属性位或

边缘触发服务器端:

......
#define EPOLL_SIZE 50
#define READ_SIZE 5int main()
{......int epollfd=epoll_create(EPOLL_SIZE);epoll_event serverevent;serverevent.events=EPOLLIN;serverevent.data.fd=serverfd;if(-1==epoll_ctl(epollfd,EPOLL_CTL_ADD,serverfd,&serverevent)){std::cout<<"server epoll_ctl fail!"<<std::endl;return 0;}int count;epoll_event* occurevent;occurevent=(epoll_event*)malloc(sizeof(epoll_event)*EPOLL_SIZE);while(1){count=epoll_wait(epollfd,occurevent,EPOLL_SIZE,-1);std::cout<<"触发事件!"<<std::endl;for(int i=0;i<count;++i){if(occurevent[i].data.fd==serverfd){sockaddr_in clientAddr;memset(&clientAddr,0,sizeof(clientAddr));socklen_t clientAddrLen=sizeof(clientAddr);int clientfd=accept(serverfd,(sockaddr*)&clientAddr,&clientAddrLen);if(clientfd==-1){std::cout<<"accept fail!"<<std::endl;continue;}//与条件触发不同之处(1)******************************************************int flag=fcntl(clientfd,F_GETFL,0);fcntl(clientfd,F_SETFL,flag|O_NONBLOCK);//与条件触发不同之处(1)******************************************************epoll_event tempevent;//与条件触发不同之处(2)******************************************************tempevent.events=EPOLLIN|EPOLLET;//与条件触发不同之处(2)******************************************************tempevent.data.fd=clientfd;if(-1==epoll_ctl(epollfd,EPOLL_CTL_ADD,clientfd,&tempevent)){std::cout<<"epoll_ctl fail!"<<std::endl;continue;}}else{int clientfd=occurevent[i].data.fd;//与条件触发不同之处(3)******************************************************while(1){char buff[1024];int readLen=read(clientfd,buff,READ_SIZE);if(readLen==-1 && errno==EAGAIN)    //说明已经读完了数据{std::cout<<"客户端发送的消息已读完!"<<std::endl;break;}else if(readLen>0){std::cout<<"客户端发来的消息:"<<buff<<std::endl;write(clientfd,buff,readLen);}else if(readLen==0){epoll_ctl(epollfd,EPOLL_CTL_DEL,clientfd,NULL);close(clientfd);break;}}//与条件触发不同之处(3)******************************************************}}}close(serverfd);close(epollfd);return 0;
}

执行结果:

客户端:

服务器端:

可以看到边缘触发与条件触发执行后的区别。

3.3 条件触发和边缘触发的比较

从服务器端实现模型的角度来分析:边缘触发可以分离接收数据和处理数据的时间点

例如:

此服务器运行流程如下:

  1. 服务器端分别从A,B,C接收数据
  2. 服务器端按照A,B,C的顺序重新组合收到的数据
  3. 组合的数据将发送给任意主机

要完成该过程,要按如下流程运行程序:

  1. 客户端按照A,B,C的顺序连接服务器端,并依序向服务器端发送数据
  2. 需要接收数据的客户端应在客户端A,B,C之前连接到服务器端并等候

但实际上,会出现不同的状况,如:

  1. 客户端C,B正向服务器端发送数据,但A还未连接到服务器端
  2. 客户端A,B,C乱序发送数据
  3. 服务器端已收到数据,但要接收数据的目标客户端还未连接到服务端

所以,这时如果使用边缘触发,那么就可以让服务器来决定读取和处理的时间点,而如果是条件触发,那么如果服务器打算延时处理,那么服务器就会不停的收到事件触发,导致服务器端不堪承受。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/142351.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

python+requests+pytest+allure自动化框架

1.核心库 requests request请求 openpyxl excel文件操作 loggin 日志 smtplib 发送邮件 configparser unittest.mock mock服务 2.目录结构 base utils testDatas conf testCases testReport logs 其他 2.1base base_path.py 存放绝对路径,dos命令或Jenkins执行…

C++,异常、转换函数、智能指针

目录 一、异常 1 C 异常机制&#xff1a; 2 使用try catch进行异常处理. 3、c 已经内置标准异常类&#xff0c;专业用于抛出的语法中 4 自定义异常&#xff1a; 5 函数只抛出&#xff0c;不处理。让上层函数处理&#xff0c;并且上层函数还可以不处理&#xff0c;让上上层…

Spring 学习(六)代理模式

10. 代理模式 案例 10.1 静态代理 角色分析 抽象角色&#xff1a;一般使用接口或者抽象类实现。真实角色&#xff1a;被代理的角色。代理角色&#xff1a;代理真实角色&#xff0c;含附属操作。客户&#xff1a;访问代理对象的角色。 租房案例 定义租赁接口 /*** TODO* 租房*…

MySQL 基础

本系列文章为【狂神说 Java 】视频的课堂笔记&#xff0c;若有需要可配套视频学习。 1. 简介 数据库&#xff08;DB&#xff0c;Database&#xff09;是安装在操作系统上的存储数据的软件。 关系型数据库&#xff08;RDB&#xff09;以行列形式存储数据。 非关系型数据库&am…

竞赛选题 基于视觉的身份证识别系统

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 基于机器视觉的身份证识别系统 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff01; &#x1f9ff; 更多资料, 项目分享&#xff1a; https://gitee.com/dancheng-sen…

第二届全国高校计算机技能竞赛——Java赛道

第二届全国高校计算机技能竞赛——Java赛道 小赛跳高 签到题 import java.util.*; public class Main{public static void main(String []args) {Scanner sc new Scanner(System.in);double n sc.nextDouble();for(int i 0; i < 4; i) {n n * 0.9;}System.out.printf(&…

JavaScript系列从入门到精通系列第四篇:JavaScript基本语法(二)

文章目录 前言 一&#xff1a;Number类型 1&#xff1a;字符串与Number类型 2&#xff1a;检查数据类型 3&#xff1a;Number最大值 4&#xff1a;Number四则运算精确性 二&#xff1a;布尔值 1&#xff1a;布尔值数量 2&#xff1a;布尔值类型查看 三&#xff1a;N…

基于微信小程序的电影院订票系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言运行环境说明用户微信小程序端的主要功能有&#xff1a;管理员的主要功能有&#xff1a;具体实现截图详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考论文参考源码获取 前言 &#x1f497;博主介绍&…

python -文件相关操作

文章目录 前言python -文件相关操作1. 读取文件1.1. 读取整个文件内容1.2. 读取文件的一行内容1.3. 将文件的内容按行存储到一个列表中 2. 写入文件3. 删除文件4. 追加文件5. 遍历文件5.1. 使用 os 模块 遍历文件5.2. # 使用 glob 模块 遍历文件5.3. 使用os.listdir() 函数遍历…

LeetCode 接雨水 双指针

原题链接&#xff1a; 力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 题面&#xff1a; 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图&#xff0c;计算按此排列的柱子&#xff0c;下雨之后能接多少雨水。 示例 1&#xff1a; 输入&#xff1a…

TS编译选项——不允许使用隐式any类型、不明确类型的this、严格检查空值、编译后文件自动设置严格模式

一、不允许使用隐式any类型 在tsconfig.js文件中配置noImplicitAny属性 {"compilerOptions": {// 不允许使用隐式any类型"noImplicitAny": true} } 开启后即可禁止使用隐式的any类型 注意&#xff1a;显式的any类型并不会被禁止 二、不允许使用不明确类…

uniapp——实现base64格式二维码图片生成+保存二维码图片——基础积累

最近在做二维码推广功能&#xff0c;自从2020年下半年到今天&#xff0c;大概有三年没有用过uniapp了&#xff0c;而且我之前用uniapp开发的程序还比较少&#xff0c;因此很多功能都浪费了很多时间去查资料&#xff0c;现在把功能记录一下。 这里写目录标题 效果图1.base64生成…

算法基础之归并排序

一、归并排序的形象理解 原题链接 示例代码 void merge_sort(int q[], int l, int r) {if (l > r) return;int mid l r >> 1;merge_sort(q, l, mid), merge_sort(q, mid 1, r);int k 0, i l, j mid 1;while (i < mid && j < r) //第一处if (q[i]…

通过410s读取电表数据并接入物联网平台

通过410s读取电表数据并接入物联网平台 设备接线准备设备调试代码实现Modbus TCP Client 读取电表数据读取寄存器数据转成32bit Float格式然后使用modbusTCP Client 读取数据 使用mqtt协议接入物联网平台最终代码实现 设备接线准备 设备调试 代码实现 Modbus TCP Client 读取…

LeetCode刷题

一 螺旋矩阵 题目链接&#xff1a;59. 螺旋矩阵 II - 力扣&#xff08;LeetCode&#xff09; 题目描述&#xff1a; 给你一个正整数 n &#xff0c;生成一个包含 1 到 n2 所有元素&#xff0c;且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。 示例 1&#xff1a;…

【论文阅读 08】Adaptive Anomaly Detection within Near-regular Milling Textures

2013年&#xff0c;太老了&#xff0c;先不看 比较老的一篇论文&#xff0c;近规则铣削纹理中的自适应异常检测 1 Abstract 在钢质量控制中的应用&#xff0c;我们提出了图像处理算法&#xff0c;用于无监督地检测隐藏在全局铣削模式内的异常。因此&#xff0c;我们考虑了基于…

如何正确使用MySQL的索引呢?

前言: 📕作者简介:热爱编程的小七,致力于C、Java、Python等多编程语言,热爱编程和长板的运动少年! 📘相关专栏Java基础语法,JavaEE初阶,数据库,数据结构和算法系列等,大家有兴趣的可以看一看。 😇😇😇有兴趣的话关注博主一起学习,一起进步吧! 一、索引使用…

探索创意的新辅助,AI与作家的完美合作

在现代社会&#xff0c;文学创作一直是人类精神活动中的重要一环。从古典文学到现代小说&#xff0c;从诗歌到戏剧&#xff0c;作家们以他们的独特视角和文学天赋为我们展示了丰富多彩的人生世界。而近年来&#xff0c;人工智能技术的快速发展已经渗透到各行各业&#xff0c;文…

【数据结构】二叉树的销毁 二叉树系列所有源代码(终章)

目录 一&#xff0c;二叉树的销毁 二&#xff0c;二叉树系列所有源代码 BTee.h BTee.c Queue.h Queue.c 一&#xff0c;二叉树的销毁 二叉树建好了&#xff0c;利用完了&#xff0c;也该把申请的动态内存空间给释放了&#xff0c;那要如何释放呢&#xff1f; 我们还是以…

LeetCode力扣020:有效的括号

有效的括号 实现思路 设立判定条件遍历的范围 代码实现 class Solution(object):def isValid(self, s):""":type s: str:rtype: bool"""nlen(s)for i in range(0,n-1):if s[i]( and s[i1]!):return Falseif s[i][ and s[i1]!]:return Falseif s…