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

news2024/11/23 12:50:53

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 5

int 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 5

int 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.coloradmin.cn/o/1041666.html

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

相关文章

python+vue驾校驾驶理论考试模拟系统

管理员的主要功能有&#xff1a; 1.管理员输入账户登陆后台 2.个人中心&#xff1a;管理员修改密码和账户信息 3.用户管理&#xff1a;管理员可以对用户信息进行添加&#xff0c;修改&#xff0c;删除&#xff0c;查询 4.添加选择题&#xff1a;管理员可以添加选择题目&#xf…

读高性能MySQL(第4版)笔记15_备份与恢复(下)

1. 二进制日志 1.1. 服务器的二进制日志是需要备份的最重要元素之一 1.2. 对于基于时间点的恢复是必需的&#xff0c;并且通常比数据要小&#xff0c;所以更容易被进行频繁的备份 1.3. 如果有某个时间点的数据备份和所有从那时以后的二进制日志&#xff0c;就可以重放从上次…

MySQL数据库入门到精通8--进阶篇( MySQL管理)

7. MySQL管理 7.1 系统数据库 Mysql数据库安装完成后&#xff0c;自带了一下四个数据库&#xff0c;具体作用如下&#xff1a; 7.2 常用工具 7.2.1 mysql 该mysql不是指mysql服务&#xff0c;而是指mysql的客户端工具。 语法 &#xff1a; mysql [options] [database] 选…

NumPy数值计算

1、Numpy概念 1.1Numpy是什么&#xff1f; Numpy是&#xff08;Numerical Python的缩写&#xff09;&#xff1a;一个开源的Python科学计算库使用NumPy可以方便的使数组、矩阵进行计算包含线性代数、傅里叶变换、随机数生成等大量函数 1.2为什么使用Numpy 对于同样的数值计…

Windows打开:控制面板\网络和 Internet\网络连接 显示空白怎么办?

Windows打开&#xff1a;控制面板\网络和 Internet\网络连接 显示空白怎么办&#xff1f; 最近有用户反馈遇到这个问题&#xff0c;问题产生原因&#xff1a;在卸载某个软件的时候&#xff0c;系统提示需要重新启动计算机&#xff0c;但是&#xff0c;启动之后&#xff0c;就…

前端项目练习(练习-002-NodeJS项目初始化)

首先&#xff0c;创建一个web-002项目&#xff0c;内容和web-001一样。 下一步&#xff0c;规范一下项目结构&#xff0c;将html&#xff0c;js&#xff0c;css三个文件放到 src/view目录下面&#xff1a; 由于html引入css和js时&#xff0c;使用的是相对路径&#xff0c;所以…

从聚水潭到金蝶云星空通过接口配置打通数据

从聚水潭到金蝶云星空通过接口配置打通数据 数据源平台:聚水潭 聚水潭SaaSERP于2014年4月上线&#xff0c;目前累计超过2.5万商家注册使用&#xff0c;成为淘宝应用服务市场ERP类目商家数和商家月订单增速最快的ERP。2014年及2015年“双十一”当天&#xff0c;聚水潭SaaSERP平稳…

python的讲解和总结V2.0

python的讲解和总结V2.0 一、Python的历史二、Python的特点三、Python的语法四、Python的应用领域五、Python的优缺点优点a. 简单易学&#xff1a;b. 可读性强&#xff1a;c. 库和框架丰富&#xff1a;d. 可移植性强&#xff1a;e. 开源&#xff1a; 缺点a. 运行速度较慢&#…

C 语言基础题:PTA L1-027 出租

下面是新浪微博上曾经很火的一张图&#xff1a; 一时间网上一片求救声&#xff0c;急问这个怎么破。其实这段代码很简单&#xff0c;index数组就是arr数组的下标&#xff0c;index[0]2 对应 arr[2]1&#xff0c;index[1]0 对应 arr[0]8&#xff0c;index[2]3 对应 arr[3]0&…

springboot 捕获数据库唯一索引导致的异常

在一些业务场景中,需要保证数据的唯一性,一般情况下,我们会先到数据库中去查询是否存在,再去判断是否可以插入新的数据.如果是在高并发的情况下,可能还是会出现重复的情况.这时候可能就需要用到锁.也可以在数据库中设置唯一索引. 如果使用唯一索引,在插入相同数据的情况下会抛出…

【postgresql】ERROR: column “xxxx.id“ must appear in the GROUP BY

org.postgresql.util.PSQLException: ERROR: column "xxx.id" must appear in the GROUP BY clause or be used in an aggregate function 错误&#xff1a;列“XXXX.id”必须出现在GROUP BY子句中或在聚合函数中使用 在mysql中是正常使用的&#xff0c;在postgre…

GAN笔记:利普希茨连续(Lipschitz continuity)

利普希茨连续&#xff08;Lipschitz continuity&#xff09;是一个数学概念&#xff0c;用于描述一个函数在其定义域内的变化程度。在生成对抗网络&#xff08;GAN&#xff09;中&#xff0c;利普希茨连续性对于鉴别器&#xff08;Discriminator&#xff09;的设计和训练具有重…

麻将技术从入门到高手,麻将听牌从基础到进阶

一、教程描述 本套麻将教程&#xff0c;大小8.82G&#xff0c;共有132个文件。 二、教程目录 麻将教程001-麻将的基本概念.mp4 麻将教程002-数牌的特性.mp4 麻将教程003-好坏搭判断.mp4 麻将教程004-拆搭原则.mp4 麻将教程005-听牌攻略.mp4 麻将教程006-进程判断.mp4 …

MySQL高级语句(第二部分)

MySQL高级语句(第二部分)一、视图表 create view1、视图表概述2、视图表能否修改&#xff1f;&#xff08;面试题&#xff09;3、基本语法3.1 创建3.2 查看3.3 删除 4、通过视图表求无交集值 二、case语句三、空值(null) 和 无值(’ ) 的区别四、正则表达式五、存储过程1、简介…

【LeetCode热题100】--238.除自身以外数组的乘积

238.除自身以外数组的乘积 思路&#xff1a; 利用索引左侧所有数字的乘积和右侧所有数字的乘积&#xff08;即前缀和后缀&#xff09;相乘得到答案 算法&#xff1a; 1.初始化两个空数组L和R&#xff0c;对于给定索引i&#xff0c;L[i]代表的是i左侧所有数字的乘积&#xff…

基于springboot会员制医疗预约服务管理信息系统springboot017

大家好✌&#xff01;我是CZ淡陌。一名专注以理论为基础实战为主的技术博主&#xff0c;将再这里为大家分享优质的实战项目&#xff0c;本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路…

2023最新PS(photoshop)Win+Mac免费下载安装包及教程内置AI绘画-网盘下载

2023最新PS(photoshop)WinMac免费下载安装包及教程内置AI绘画-网盘下载 2023最新PS(photoshop)免费下载安装教程来咯&#xff5e; 「PhotoShop」全套&#xff0c;winmac&#xff1a; https://pan.quark.cn/s/9d8d8ef5c400#/list/share 所有版本都有 1&#xff0c;复制链接…

高并发下双重检测锁DCL指令重排问题剖析

文章目录 一、引言1.1 双重检查锁定&#xff08;Double-Checked Locking&#xff0c;简称DCL&#xff09;定义介绍1.2 高并发环境下DCL的应用和优势 二、DCL存在的问题2.1 DCL的代码示例2.2 指令重排的定义和工作原理2.3 指令重排导致DCL失效的情况分析 三、深入分析指令重排和…

「大数据-2.1」HDFS集群启停命令

目录 一、HDFS集群一键启停脚本 1. HDFS集群的一键启动脚本 2. HDFS集群的一键关闭脚本 二、单进程启停 1. hadoop-daemon.sh脚本 2. hdfs脚本 三、总结 1. 一键启停脚本 2. 独立进程启停 一、HDFS集群一键启停脚本 Hadoop HDFS组件内置了HDFS集群的一键启停脚本。 1. HDFS集群…

JavaScript 期约 Promise 总结

同步与异步的概念 JavaScript 是一门单线程的语言&#xff0c;这意味着它在任何给定的时间只能执行一个任务。 然而&#xff0c;JavaScript 通过异步编程技术来处理并发操作&#xff0c;以避免阻塞主线程的情况。 在上图中&#xff0c;同步行为的进程 A 因为等待进程 B 执行完…