[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.rhkb.cn/news/144567.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python爬虫实战案例——第六例

文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff01;严禁将文中内容用于任何商业与非法用途&#xff0c;由此产生的一切后果与作者无关。若有侵权&#xff0c;请联系删除。 目标&#xff1a;去哪儿网指定城市人气值最高的15个景点评论数据采集 地址&a…

微信小程序开发基础(二)基本组件

本帖开始介绍小程序中的一些基本组件~ 微信小程序是一种轻量、快速、跨平台的应用程序&#xff0c;是微信公众号的重要组成部分。随着微信小程序的普及&#xff0c;越来越多的开发者和企业开始使用微信小程序来搭建自己的应用&#xff0c;但是对于初次接触微信小程序的开发者…

排序:败者树和置换选择排序(解决外部排序中的优化问题)

1.算法目的&#xff08;败者树&#xff09; 解决多路平衡归并带来的问题。 在外部排序中&#xff0c;使用k路平衡归并策略, 选出一个最小元素需要对比关键字(k-1)次&#xff0c; 导致内部归并所需时间增加。&#xff08;可用“败者树”进行优化&#xff09; 2.败者树的定义 …

【C++11】多线程

多线程创建线程thread提供的成员函数获取线程id的方式线程函数参数的问题线程join场景和detach 互斥量库&#xff08;mutex&#xff09;mutexrecursive_mutexlock_guard 和 unique_lock 原子性操作库&#xff08;atomic&#xff09;条件变量库&#xff08;condition_varuable&a…

一文读懂集合竞价,建议收藏,读完少交学费

集合竞价每个时间段交易规则及作用都不一样&#xff0c;了解集合竞价的规则&#xff0c;有利于琢磨主力的意图。 大部分同学都不是很关心集合竞价&#xff0c;也不知道如何利用集合竞价买卖股票。如上图所示&#xff0c;有同学在9点15看着股票涨停&#xff0c;立马冲进去&…

【切片】基础不扎实引发的问题

本次文章主要是来聊聊关于切片传值需要注意的问题&#xff0c;如果不小心&#xff0c;则很容易引发线上问题&#xff0c;如果不够理解&#xff0c;可能会出现奇奇怪怪的现象 问题情况&#xff1a; 小 A 负责一个模块功能的实现&#xff0c;在调试代码的时候可能不仔细&#x…

UE蓝图学习(从Unity3D而来)

一、UE组件对比Unity3D&#xff0c;从Unity3D过渡来学的角度出发&#xff0c;可以理解为在 空物体下放置子物体。UE没有U3D那种可以将组件挂在自身空物体上面。 二、UE 蓝图的情境提示&#xff0c;必须先找到相应的类型&#xff0c;对象对象、事件事件&#xff0c;才能找到相应…

云原生Kubernetes:Pod控制器

目录 一、理论 1.Pod控制器 2.Deployment 控制器 3.SatefulSet 控制器 4.DaemonSet 控制器 5.Job 控制器 6.CronJob 控制器 二、实验 1.Deployment 控制器 2.SatefulSet 控制器 3.DaemonSet 控制器 4.Job 控制器 5.CronJob 控制器 三、问题 1. showmount -e 报错…

QT窗口的设置、按钮的创建和对象树的概念

目录 设置窗口属性 按钮的创建 对象树 对象树的概念 构建和析构的顺序问题 设置窗口属性 在Qt官方手册中查找QWidget相关信息 或者在QT软件帮助一栏直接搜索QWidget 即可找到一些要寻找的设置属性的函数 将代码写在构造函数中 widget.cpp #include "widget.h"…

1200*A. Flipping Game(前缀和)

解析&#xff1a; 100数据量&#xff0c;两层遍历每个区间&#xff0c;然后前缀和计算1的个数&#xff0c;维护最大值即可。 #include<bits/stdc.h> using namespace std; #define int long long const int N110; int n,a[N],res,sum[N]; signed main(){scanf("%ll…

保姆级 -- Zookeeper超详解

1. Zookeeper 是什么(了解) Zookeeper 是一个 分布式协调服务 的开源框架, 主要用来解决分布式集群中应用系统的一致性问题, 例如怎样避免同时操作同一数据造成脏读的问题. ZooKeeper 本质上是 一个分布式的小文件存储系统 . 提供基于类似于文件系统的目录树方式的数据存储, …

常见的7种分布式事务解决方案(2pc,3pc,Tcc,Seta、本地事务....)

一 分布式事务 1.1 分布式事务 在分布式系统中一次操作需要由多个服务协同完成&#xff0c;这种由不同的服务之间通过网络协同完成的事务称为分布式事务。 1.首先满足事务特性&#xff1a;ACID 2.而在分布式环境下&#xff0c;会涉及到多个数据库 总结&#xff1a;分布式事务…

C运算符和控制语句

几乎每一个程序都需要进行运算&#xff0c;对数据进行加工处理&#xff0c;否则程序就没有意义了。要进行运算&#xff0c;就需规定可以使用的运算符。 C语言的运算符范围很宽&#xff0c;把除了控制语句和输人输出以外的几乎所有的基本操作都作为运算符处理。 运算符分类1 除…

Apache Flume

Flume 1.9.0 Developer Guide【Flume 1.9.0开发人员指南】 Introduction【介绍】 摘自&#xff1a;Flume 1.9.0 Developer Guide — Apache Flume Overview【概述】 Apache Flume is a distributed, reliable, and available system for efficiently collecting, aggregati…

Docker 安装Redis(集群)

3主3从redis集群配置 1、新建6个docker容器 redis 实例 docker run -d --name redis-node-1 --net host --privilegedtrue -v /data/redis/share/redis-node-1:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6381 docker run -d --name redis-node-2 --ne…

C理解(二):指针,数组,字符串,函数

本文主要探讨指针&#xff0c;数组&#xff0c;字符串&#xff0c;函数 指针 int *p; 未绑定:*表示p为指针变量,占4字节 int a 1;p &a; 绑定:p与a地址绑定即p中存放a的地址 *p *p 1; 解引用:p间接访问a的存储空间…

九、Delay函数

1、两个延时函数 vTaskDelay&#xff1a;至少等待指定个数的Tick Interrupt才能变为就绪态。vTaskDelayUntil&#xff1a;等待到指定的绝对时刻&#xff0c;才能变为就绪态。 2、函数原型 /* xTicksToDelay: 等待多少个Tick */ void vTaskDelay( const TickType_t xTicksToD…

Android Studio 的android.jar文件在哪儿

一般在&#xff1a;C:\Users\admin\AppData\Local\Android\Sdk\platforms\android-33下&#xff08;不一定是33&#xff0c;这个得看你Android Studio->app->builde.gradle的targetSdk是多少&#xff09; 怎么找&#xff1a; 1.打开Android Studio 粘贴地址后&#xff0…

13.(开发工具篇github)如何在GitHub上上传本地项目

一:创建GitHub账户并安装Git 二:创建一个新的仓库(repository) 三、拉取代码 git clone https://github.com/ainier-max/myboot.git git clone git@github.com:ainier-max/myboot.git四、拷贝代码到拉取后的工程 五、上传代码 (1)添加所有文件到暂存

Ci2451-2.4g无线MCU收发芯片

Ci2451 是一款集成无线收发器和8位RISC(精简指令集)MCU的SOC芯片。 无线MCU解决方案,集成丰富的MCU资源、更小尺寸,来满足设计中的各种内存、功率、尺寸要求,充分缩短2.4GHz无线产品设计周期并优化产品成本。 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff…