当前位置: 首页 > news >正文

多路转接与Reactor

目录

五种IO模型

阻塞与非阻塞

select

poll

epoll

Reactor


五种IO模型

IO = 等 + 拷贝数据

比如传输层的接收缓冲区没有数据,那上层只能等它有数据了才能拷贝数据!

高效的IO本质:减少单位时间内:"等"的比重!!!

一个江边,有5个人,张三,一直盯着自己的鱼漂,也不干别的事,当动了就起杆;李四,则在等

的过程,一会儿玩手机,一会儿看会儿书,当鱼漂动了就起杆;王五,则是通过一个铃铛与自己的

杆连着,当有鱼上钩时,铃铛就会响;赵六,则是有很多鱼竿,哪个鱼漂动了,就去起哪个杆;田

七是老板,让自己的秘书替自己钓鱼,然后自己就离开了,当田七给的桶装满了鱼的时候,秘书就

给田七打电话,让他来拿鱼和接他!

张三——阻塞等待

李四——非阻塞等待

王五——信号驱动

赵六——多路转接

田七——异步IO

钓鱼 = 等+ 钓,前面几个都是同步IO!鱼竿 <-> fd

几乎所有的IO函数,核心工作其实就2类,1.等;2.拷贝

阻塞IO: 在内核将数据准备好之前,系统调用会一直等待,所有的套接字,默认都是阻塞方式

非阻塞IO: 如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错

误码

信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作

多路转接:最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态

异步IO: 由内核在数据拷贝完成时, 通知应用程序

在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间, 让IO更高效,最核心

的办法是让等待的时间尽量少

注意:这里的同步与进程/线程的同步没有任何关系!!!

阻塞与非阻塞

阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程

什么叫做等事件就绪

IO事件就绪分为读事件就绪和写事件就绪,只有当缓冲区的数据达到一定量的时候,才会被应用层

拷贝数据!

非阻塞IO

获得/设置文件状态标记(cmd=F_GETFL或F_SETFL)

如下图,将文件描述符设置为非阻塞

如下图,从键盘读取数据,读取成功就打印读取到的数据和errno

在非阻塞情况下,我们读取数据,如果数据没有就绪,系统是以出错的形式返回的(不是错误),而

没有就绪和真正的错误,使用的是同样的方式标识,那就需要用errno来进一步区分!errno为11

时,表示没有就绪,EAGAIN == 11,EAGAIN是一个宏!

运行结果

多路转接

select:只负责一件事情,等的过程!

select定位:只负责等待,得到fd就绪,通知上层进行读取或者写入

注意:select没有所谓的读取和写入数据的功能!!!

read、write、recv、send本身也有等待的功能,但是只能传入1个fd,而select能够同时等待多

个fd!!!

我们想要的等待结果:读就绪,写就绪,异常就绪

函数原型如下图

nfds = maxfd + 1

fd_set是一个位图结构:比特位的"位置"代表哪一个sock,如下图

三个readfds,writefds,exceptfds参数分别表示:只关心读、写、异常,且是输入输出型参数!

以读为例,select的核心功能有两点

1.用户告知内核,你要帮我关心哪些fd上的读事件就绪

2.内核告知用户,你所关心的哪些fd上的读事件已经就绪

比特位的内容

输入时:用户告诉内核,你要帮我关心的fd集合,将0-5的bit位都置为1,表示0-5都要关心!

输出时:内核告诉用户,你关心的哪些fd上面的事件已经就绪,3号bit位为1,表示3号fd已经就绪

写和异常也类似于读!

参数timeout是等待方式,也是一个输入输出型参数,假设设置5秒,3秒就绪,那返回的就是剩余

时间2秒!有三种等待方式

1.只要不就绪,我就不返回,阻塞等待,timeout为NULL

2.只要不就绪,立马返回,非阻塞等待,timeout为{0,0}

3.设置好deadline,deadline之内,按1进行,deadline之外,按2进行,假设deadline为5秒,

timeout为{5,0},5秒超时!

上述三种方式,都是只要就绪,立马返回!!!

位图在设置的时候,我们不能自己去进行位操作,而要调用下面的接口!!!

用来清除描述词组set中相关fd的bit位

用来测试描述词组set中相关fd的bit位是否为真

用来设置描述词组set中相关fd的bit位

用来清除描述词组set的全部bit位

返回值,大于0表示等待的fd的数量,等于0表示超时,小于0表示出现错误!

理解select执行过程

例子如下:

(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。

(2)若fd=5,执行FD_SET(fd,&set);后set变为0010,0000(第5位置为1)

(3)若再加入fd=2,fd=1,则set变为0010,0011,用户告诉内核

(4)执行select(6,&set,0,0,0)阻塞等待

(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011,内核告诉用户

注意:没有事件发生的fd=5被清空

如上,只是模拟了一次,select因为使用输入输出型参数表示不同的含义,意味着后面每一次,都需要对fd_set进行重新设置!

如何得知之前都有哪些fd?

用户必须定义数组或者其它容器结构,来把历史fd全部保存起来!!!

代码实现

前期准备

创建一个第三方数组,用来保存历史fd

因为我们要采用多路转接的方式,即使用select,所以监听之后不能直接accept,accept是阻塞

式等待,因为它不知道有listen_sock上有新链接,对于所有的服务器,最开始的时候,只有

listen_sock!!!

初始化数组,同时定义一个读位图变量,并将监听套接字设为0号fd!

然后是事件死循环,因为我们不知道有哪些关系读事件的fd要检测,所以只能遍历所有的fd,将关

心读事件的fd添加到rfds中,以及找到最大fd!这里采用阻塞式等待!

然后根据返回值来执行对应的操作!

当等待成功时,即有事件就绪,需要循环遍历所有的fd,来找到哪些fd就绪了!如果为-1则表示该

位置的fd未被使用!如果FD_ISSET为真,表示该fd合法

合法的fd不一定是就绪的fd,所以需要判断是否是listen_sock

如果是listen_sock,就accpt,链接成功,如果有服务器有剩余的fd,因为无法确定该链接是否有

数据到来,而判断是否有数据只有select知道,所以只能将新的链接的sock保存在数组中,待下一

轮循环时去读取数据,没有则关闭新的链接!

如果是普通的sock,即读事件就绪!就可以读取数据,也分为三种情况,如果读取成功(s>0),就

打印读取到的信息,如果对端断开了链接(s == 0),就关闭相应的fd,同时将数组中相应位置的值

为-1,表示该位置的fd已经关了!如果小于0,表示读取错误,和对端断开链接处理一致!

运行结果

总结

优点

可以一次等待多个fd,在一定程度上,提高IO的效率

缺点

每次都要重新设置,每次完成之后,需要遍历检测

fd_set,它能够让select同时检测的fd是有上限的!

select底层需要轮询式的检测那些fd上的那些事件就绪了!

select可能会较为高频率的进行用户到内核,内核到用户的频繁拷贝问题!

通过缺点的第三点,我们也可以得出因为os也要轮询式的检测,而os又喜欢前闭后开的区间,所

以就有了nfds = max_fd + 1!

poll

函数原型

poll的第一个参数是一个结构体指针,是一个结构体数组的首地址!该结构体内部一个有三个变

量,一个是要关心的fd,另外两个则是事件!第二个参数nfds则和select的一样,第三个参数

timeout和select的也很类似,虽然类型不同,但意思都差不多,0表示非阻塞,-1表示阻塞,大于

0,表示在这个时间内阻塞,超过这个时间非阻塞!

事件是short类型,那就有16个bit位,就可用这些bit位,来表示那些事件,比如读事件,写事件,

异常事件,如下图,每一个事件都是一个宏,都只占用一个bit位!

添加读事件和写事件,events | = POLLIN,events |= POLLOUT

检测读事件就绪,if(events & POLLIN) 

 代码实现

如下图,用poll来等待0号描述符!

运行结果

poll的优点

不同与select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现

1.包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式,接口使用比select

方便!

2.poll并没有最大数量限制 (但是数量过大后性能也是会下降,因为os采用的是轮询遍历的方式)

poll的缺点

poll中监听的文件描述符数目增多时

1.和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符

2.每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中

3.同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增

长, 其效率也会线性下降

epoll

epoll_create: 创建一个epoll的句柄,自从linux2.6.8之后,size参数是被忽略的,所以可以随便

填,只要大于0即可!

注意:用完之后, 必须调用close()关闭

epoll_ctl:用户告诉内核,只要调用一次,内核就永远记住了!

第一个参数epfd是epoll_create的返回值

第二个参数是操作,可填的内容如下所示

EPOLL_CTL_ADD:注册新的fd到epfd中

EPOLL_CTL_MOD:修改已经注册的fd的监听事件

EPOLL_CTL_DEL:从epfd中删除一个fd

第三个参数就是要操作的fd

第四个参数是用户告诉内核,要关心的事件!结构体第一个变量与poll的event一样,第二个结构

体变量中的fd是用户要内核关心的fd!

epoll_wait:内核告诉用户

events和epoll_ctl中的events一样,只不过其中的内容是内核告诉用户的!

maxevents告诉内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的

size!

参数timeout与poll的timeout一样!

epoll原理

通知就绪时,CPU会通过中断的方式,从内核上下文切换到中断向量表,将外设上的数据拷贝到内

存中!

比如select、poll,内核会通过轮询的方式检测就绪的fd,但epoll1没有这么做,而是在硬件与操

系统之间建立了一个回调机制,可以将就绪的fd尾插到就绪队列中,就不用再去遍历了!

凡是在红黑树中的节点,对应的fd和事件,就是OS系统需要关心的,红黑树就相当于select中的

第三方数组!!!

epoll_create的作用就是创建epoll模型

epoll_ctl的作用:1.插入节点到红黑树;2.建立该fd对应的回调策略

epoll_wait的作用:以O(1)的时间复杂度,检测是否有事件就绪,即判断就绪队列中是否有节点

代码实现

建立tcp,监听socket

创建epoll模型,获得epfd

将listen_sock和它所关心的事件,添加到内核

事件循环

有事件就绪时,循环遍历,用if语句条件为按位与的方式,是什么事件的fd就绪!以读事件为例!

然后还要判断是普通sock,还是listen_sock,普通sock就可以直接读取数据,listen_sock就直

Accept,连接到的sock,还不能读取数据,因为无法判断其是否有数据到来,只有epoll才知道

是否有数据,所以只能托管给epoll!

读取数据,成功就直接打印,s == 0时表示对端链接关闭,所以sock也要关闭,同时告诉内核不要

再关心该sock了,即调用epoll_ctl,删除红黑树上的此节点,读取失败也做一样的处理!

运行结果

上面读取数据的处理,是有问题的,因为你无法保证读取到的是客户端给你发的一个完整的报文!

同时,如果你还有数据没有读完,还在buffer中,下一轮循环时,之前buffer所占用的空间早就被

释放了,数据也就没有了!

epoll的工作方式

LT:水平触发

ET:边缘触发

下面以拿快递的例子来解释这两种工作方式

假设你在淘宝上买了5件东西,它是陆陆续续到达的,开始到达了2个,张三就给你打电话,要你来

拿快递,而你此时正在打游戏,就说马上来拿,却依旧在打游戏,张三此时就去给别人打电话去

了,过了不久,又给你打电话,叫你去拿快递,你说知道了!打了几个电话后,你嫌烦,就去拿快

递去了,同时另外3个快递也来了,但是买的东西比较大或者重,你拿了2个之后,觉得累,就又坐

着打游戏去了,张三发现你又不来拿了,就又给你打电话!

下次你又买了5件东西,只是打电话的人换成了李四,李四就不是惯你毛病的人,开始来了3个,李

四给你打电话,告诉你有3个快递到了,要你来拿,然后就挂断电话了,就不再继续打电话了,下

次又来了2个快递,又给你打电话,告诉你有2个快递,要你来拿!

水平触发就如同张三的工作方式,边缘触发就如同李四的工作方式!!!

对于张三:底层只要有你的包裹,就会一直通知你

对于李四:只有当快递点中有你的快递到来时,从无到有,从有到多的时候会给你打一个电话,除

此之外,不会给你多打一个电话!通过它的这种通知策略,倒逼程序员一旦开始读取数据,就要一

直读完!

两种不同的通知方式,从通话效率上来说,李四的效率更高,因为一个人一天能打的电话量是有限

的,而李四不会打多余的电话,张三就可能给一个人打很多个重复的电话

select、poll、epoll默认为水平触发

如下图,在事件中添加EPOLLET,就成为了边缘触发的工作方式

ET模式下的fd,必须是非阻塞的原因

ET -> 通知一次 -> recv/accept/read -> 准备读取 -> 我怎么保证我将本次全部读取完呢?-> 循环

读取 -> 可能会在读取的最后一次我们会卡住(阻塞住了) -> 单进程 -> 如何解决?->ET模式下的所

有的fd,必须将该fd设置为非阻塞!

例如:你妈把每个月该给你的零花钱给了你爸,你每天找你爸要100,当第四天时,你爸告诉你,

他只有30块钱了,直接给你了,你第五天就不会找你爸要钱了,因为你知道已经要完了这个月的零

花钱了!这种情况还算好的!

假设你爸总共只有400元,你前四天每天都要到了100元,第五天你依旧会去要,因为你无法判定

是否还有零花钱,这种情况就很糟糕了!此时,本来没有数据了,你还要去读,那就会卡住,即阻

塞住了,客户端因为没有收到响应,以为你没有读完,下次也不会给你发送数据了,所以须将ET

模式下所有的fd,必须被设置为非阻塞!

下面对于epoll基于ET模式进行服务器设计,写一个计算器!同时要针对上面写的epoll代码的缺

点,要做到以下3点

1.我们需要给每一个fd,都要有自己专属的输入输出缓冲区!

2.虽然已经对等和拷贝在接口层面已经进行了分离,但是在代码逻辑上,依旧是耦合在一起的

3.epoll最大的优势在于 就绪事件通知机制!

代码实现

对于Reactor.hpp文件

首先定义一个事件节点

每个sock都有自己的输入缓冲区和输出缓冲区

一般处理IO的时候,我们只有三种接口需要处理,即读取、写入和异常,所有先定义了一个函数指

针,然后在节点内添加了三个函数指针类型的成员

针对上面所说的第二点与第三点,通过回调的方式,来进行解耦!

设置Event回指Reactor的指针

构造函数

对于Reactor类

首先是定义一个epfd成员,其次是定义一个unordere_map类型的成员,来让sock与其读写事件

形成映射关系

然后是构造函数

初始化Reactor,即创建epoll模型

插入事件,即让内核关心sock,同时将映射关系,插入到unordered_map中

删除事件,即让内核不再关心sock,同时将映射关系,从unordered_map中移除

针对上面所述的第三点,写一个就绪事件的派发逻辑

首先定义一个结构体数组,来保存内核告诉用户已经就绪的事件,对于n个就绪事件,循环处

代表差错处理,将所有的错误问题全部转化成为让IO函数去解决

对于读事件就绪,直接调用回调方法,执行对应的读取,同时要保证这个sock还在events内

判断sock是否还在events内

对于写事件就绪,直接调用回调方法,执行对应的写入,同时要保证这个sock还在events内

对于epoll_server.hpp文件

如下图的代码和之前的代码类似,所有不多赘述,只不过这里要多调用一个设置非阻塞接口!

创建一个Reactor对象

Reactor(反应堆模式):通过多路转接方案,被动的采用事件派发的方式,去反向的调用对应的回

调函数

向Reactor反应堆中加柴火

首先得有柴火,Accepter:链接管理器,如下图

将准备好的柴火放入反应堆Reactor中,同时设置成ET模式

开始进行事件派发!

对于Util.hpp文件

将sock设置为非阻塞

对于Accepter.hpp文件

写一个Accepter接口,循环接收到来的sock,将要关心的事件告诉给内核!Recver、Sender、

Errorer则负责真正的读取!

对于Server.hpp文件

Recver函数

1.真正的读取

把接收到的数据全部都写入到该sock自己的输入缓冲区里面,当s < 0时,就发生了读写错误,可

能是IO被信号打断了,也有可能是没有数据读了,还有可能是真的出错了!

2.分包,对于一个或者多个报文,解决粘包问题,如1+2X3+4X5+6X,要分解成1+2,3+4,5+6

SplitSegment函数放在Util.hpp文件中,将分成的多个报文都装入到vector中,不足一个报文就不

要去处理,放在inbuffer中即可!

3.反序列化,即针对一个报文,提取有效参与计算或者存储的信息

4.业务逻辑 -- 得到结果

 

5.构建响应,同时,发送数据,即将数据写入到输出缓冲区内

 

6.尝试直接或间接进行发送。写事件一般基本上都是就绪的,但是用户不一定是就绪的!所以对于

写事件,我们通常都是按需设置,而不能在InsertEvent时,同时设置写事件和读事件!

在Reactor.hpp文件中,写一个EnableRW的接口,设置读事件和写事件!

Sender函数

total是本轮累计发送的数据量,当total的大小为输出缓冲区的数据量时,就表示发送完毕!

发送过程中可能会被信号中断,也可能数据没有发完,但是不能再发了,或者是发送失败!

根据不同的情况,即SenderCore函数的返回值,做不同的处理!发送完毕,就直接关闭写事件,没

有发完,就让它继续发,发送失败,就调用异常函数!

Errorer函数

发生异常,直接删除该sock,即不再让内核关心了!

运行结果

相关文章:

  • Python实现九九乘法表的几种方式,入门必备案例~超级简单~
  • windows常用命令大全
  • Open3D(C++)欧式聚类分割
  • 用python实现提高自己博客访问量
  • 第十三届蓝桥杯C++B组省赛 I 题——李白打酒加强版 (AC)
  • 常见的probe set和gallery set究竟是什么
  • 【微机接口】中断系统:中断的应用
  • Spring MVC入口Servlet原理简介说明(HttpServletBean,FrameworkServlet,DispatcherServlet)
  • 【附源码】Python计算机毕业设计社区卫生预约挂号系统
  • 【C++】顺序表,链表,栈的练习(千万要会做)每日小细节007
  • k8s编程operator——client-go基础部分
  • MySQL纯代码复习
  • 零基础入门学习Web开发:HTML篇(一)
  • 【云原生】docker 搭建ElasticSearch7
  • ubuntu安装openresty
  • AngularJS指令开发(1)——参数详解
  • fetch 从初识到应用
  • flutter的key在widget list的作用以及必要性
  • Javascript Math对象和Date对象常用方法详解
  • JavaScript/HTML5图表开发工具JavaScript Charts v3.19.6发布【附下载】
  • Javascript编码规范
  • Meteor的表单提交:Form
  • PHP 7 修改了什么呢 -- 2
  • Swoft 源码剖析 - 代码自动更新机制
  • Vue2.x学习三:事件处理生命周期钩子
  • 开放才能进步!Angular和Wijmo一起走过的日子
  • 看域名解析域名安全对SEO的影响
  • 前言-如何学习区块链
  • 数据仓库的几种建模方法
  • 数据结构java版之冒泡排序及优化
  • 腾讯大梁:DevOps最后一棒,有效构建海量运营的持续反馈能力
  • 一些关于Rust在2019年的思考
  • 由插件封装引出的一丢丢思考
  • 转载:[译] 内容加速黑科技趣谈
  • 看到一个关于网页设计的文章分享过来!大家看看!
  • 《TCP IP 详解卷1:协议》阅读笔记 - 第六章
  • # 手柄编程_北通阿修罗3动手评:一款兼具功能、操控性的电竞手柄
  • #ifdef 的技巧用法
  • #stm32驱动外设模块总结w5500模块
  • $(function(){})与(function($){....})(jQuery)的区别
  • (+3)1.3敏捷宣言与敏捷过程的特点
  • (1)Nginx简介和安装教程
  • (1)虚拟机的安装与使用,linux系统安装
  • (17)Hive ——MR任务的map与reduce个数由什么决定?
  • (2/2) 为了理解 UWP 的启动流程,我从零开始创建了一个 UWP 程序
  • (NO.00004)iOS实现打砖块游戏(十二):伸缩自如,我是如意金箍棒(上)!
  • (PWM呼吸灯)合泰开发板HT66F2390-----点灯大师
  • (Python第六天)文件处理
  • (Repost) Getting Genode with TrustZone on the i.MX
  • (zt)最盛行的警世狂言(爆笑)
  • (附源码)ssm失物招领系统 毕业设计 182317
  • (紀錄)[ASP.NET MVC][jQuery]-2 純手工打造屬於自己的 jQuery GridView (含完整程式碼下載)...
  • (十八)用JAVA编写MP3解码器——迷你播放器
  • (十一)JAVA springboot ssm b2b2c多用户商城系统源码:服务网关Zuul高级篇
  • (转)AS3正则:元子符,元序列,标志,数量表达符