高级I O

五种IO模型

理论认识

以前说文件的读取写入,这就是IO
IO在冯诺依曼体系中就是输入输出
今天网络也属于IO,本质就是把数据从计算机导到网卡。

系统谈IO效果不明显,在网络这里非常明显。

我们以读写为例
先读为例,今天我调用read,tcp套接字,可是底层接受缓冲区里没数据,默认read就阻塞式的等待。
今天我想写入,但是发送缓冲区已经被打满了,上层无法把数据拷贝到下层,write也就必须阻塞式的等。

IO: Input && Output
基本事实
1、应用层进行读写时,本质是把数据从用户层写给OS — 应用层读写函数本质就是拷贝函数
2、IO的过程时 = 应用层大部分时间一直在等待的操作 + 只有当有数据了才进行拷贝
所以IO本质 = 等 + 拷贝

所以read 或者 write 是由 等 和 拷贝两部分构成的

那他IO过程中等什么呢?
对读来讲,它在等OS内缓冲区有没有数据
对写来讲,他在等发送缓冲区有没有空间

真正IO当中,要进行拷贝,必须先判断条件成立
条件在今天往后要讲的话语体系里叫做读写事件
也就是说你今天想进行读,前提条件是你的读事件得就绪,什么叫读事件就绪呢,说白了就是底层缓冲区有足够数据让你读。
今天要发数据,本质上是发送缓冲区要有足够多的空间,如果没有足够空间你也就没办法把数据从应用层拷贝到内核层。
所以这叫做写事件就绪

结论
IO = 等 + 拷贝
等的过程中,等什么呢?等读写事件就绪

所以IO部分想办法让服务器支持高并发。
网络通信本质就是IO,例如网络版本计算器
,但最终本质就是网络服务器都是进行读取数据

所以什么叫做高效的IO呢?
单位时间内能拷贝更多的数据,这个IO效率就越高
反面就是
单位时间内,IO过程中,等的比重越小,IO效率越高!
因为IO = 等 + 拷贝

在得出一个结论
几乎所有的提高IO效率的策略,本质就是==单位时间内让等的比较越小!==那你的IO效率就越高了。

举个例子,网络服务器为什么要写多线程服务器?
因为多线程可以同时等待多个fd,所有 的IO等待不是串行的而是并行的,等的比重减小了。
在这里插入图片描述

五种IO模型

五个故事 以钓鱼为例
钓鱼俩阶段 : 等 + 钓
1、张三:钓鱼界新手,不灵活,凳子一把坐在河边就下钩了,从此之后张三就死盯着鱼漂,一直盯着鱼漂其他什么事也不做,只要鱼咬钩了,此时张三钓上来一条。
张三进行钓鱼过程中,一直盯着鱼漂,本质是他在等待,谁来了都打扰不了他也叫不醒他,直到底层有鱼咬钩,他才返回,我们把这种钓鱼方式叫做阻塞式钓鱼
目前为止,我们见到的大部分IO,文件接口,fread,fwrite,read,write都是阻塞式的。

2、李四:钓鱼界两三年钓龄,和张三是朋友,坐到张三旁边,就问张三:张三今天鱼情如何啊?钓多少条鱼啊?张三理都不理李四,头也不抬,李四就安心自己钓鱼了。
李四也是把鱼钩一扔,但李四闲不住,他拿着鱼竿每隔一分钟看一下鱼漂,发现没有鱼咬钩,李四就把视线转移到手机上,或者那本书看看,过了一会又去看看鱼漂有没有咬钩,发现鱼漂果然动了,李四也把鱼钓了上来。
这个过程中,李四检测有没有钓到鱼时,先判断底层有没有鱼就绪,鱼漂没动就认为鱼没到,那么李四就去做其他事情,他并不会因为鱼漂上没鱼就卡在那里,所以李四每次查鱼漂本质是检测有没有鱼,但检测条件不满足他直接返回了,继续看手机,如果检测条件就绪了他才钓鱼。
李四这种钓鱼的方式称为非阻塞式IO

其中不管是张三还是李四,他们两个拿的鱼竿,我们把他相当于网络通信中的文件描述符。
一个鱼竿就是一个文件描述符。

而李四钓鱼过程中不断重复的去非阻塞轮询自己的鱼漂有没有鱼啊,没有就返回。
所以李四这种全称策略叫做非阻塞轮询

3、王五:钓鱼界五年钓龄
王五觉得张三李四的钓鱼方式有点lOW,张三一个一动不动,李四一个一直在动。
王五也坐在了河边,坐下来就开始挂自己的鱼竿和鱼钩,当他准备把鱼钩扔到河里的时候,在鱼竿顶部上放了铃铛,再把钩扔进河里了。
王五把鱼竿插在岸边,他自己不管了,开始躺旁边玩手机,他不关注鱼竿了,后续突然铃铛响了,王五头也不抬直接把鱼竿拎起来钓了一条鱼。
以前张三李四是自己主动检测鱼漂有没有鱼上钩,而王五本质是鱼你不要让我查你了,而是当你就绪时你来通知我。
所以王五这种钓鱼方式我们称为信号驱动式IO
当地层数据就绪了,你来通知我。

4、赵六:方圆五公里的首富,喜欢钓鱼但又特别想赢。
他过来的时候开着卡车过来的,拉了一卡车鱼竿,依次把所有鱼竿都设置好扔到水里面。
从此往后我们看到这样一种阵势,张三李四王五在旁边钓鱼
另一边岸边插满了鱼竿,赵六就在岸边来回左右左右的轮询检测哪一个鱼竿有鱼咬钩了。
所以赵六一个人就获取了那么多鱼竿,以遍历的方式周期性的去检测所有的鱼竿上是否有鱼咬钩。
这种钓鱼方式我们叫做多路复用也叫做多路转接

我想问同学们,前四个人中谁的钓鱼效率是最高的?
我是以一个人为单位。
他钓鱼效率最高为什么啊?
那就是赵六了,因为他鱼竿多啊,为什么鱼竿多效率就高呢?

假设今天张三李四王五赵六这四个人,只有赵六100个鱼竿,其他都是一个鱼竿,不考虑鱼情,站在鱼的角度,当你在河里看到脑门上挂满了鱼钩,你咬哪一个啊?
假设鱼咬钩的概率是千分之一,咬张三李四王五的概率都是一样的,都是千分之一。
但是咬赵六的鱼钩的概率相当于1/10

多个鱼竿可以让每一个等待的过程时间上是并行的,重叠的,所以赵六的效率最高的。

5、田七:方圆500公里内的首富,他到没有像张三李四王五赵六是职业钓鱼佬,田七就是喜欢钓鱼,但是他是董事长太忙了,他跟着司机小王开着车到了河边,看到四个人非常奇怪,一个一动不动,一个一直在动,一个左右在动,一个躺着不动。
反正他们钓鱼的姿势形态各异,勾起了田七的钓鱼欲望,当田七准备钓鱼时,公司来个电话要开会,田七一听行吧,其实我不想钓鱼,我单纯想吃鱼,就跟小王说,把小王叫过来了,我车后备箱有桶,电话,钓鱼装备,你帮我去钓,我 去开会,等你把桶装满了你给我打电话,到时我来接你。
从此往后田七就走了,小王就去钓了,小王怎么钓不重要,这过程中田七没有参与钓鱼,也没等也没钓,真正钓鱼的是小王。
所以田七不是严格意义的钓鱼者,他应该叫钓鱼行为的发起者,田七要的是数据不管鱼怎么来的,我要的是鱼
站在田七的角度,把他的钓鱼的方式称为异步IO
为什么叫异步呢?
因为小王钓鱼的时候,田七正在开会
一般小王通常指的就是OS

也就是田七可以给OS一段缓冲区(桶),通知方式(电话),当我们把IO请求交给OS,由OS自动监测,有数据把数据放到用户层缓冲区里,然后并且通知我说鱼钓完了,数据拷贝完了,田七在应用层直接用数据就行,田七并不参与 具体IO的过程,这种情况叫异步IO

我们把前4种IO称作同步IO

在这里插入图片描述

问题
阻塞式IO VS 非阻塞式IO
所以阻塞式IO和非阻塞式IO有什么区别呢?
IO = 等 + 拷贝
阻塞式IO和非阻塞式IO在效率方面其实没有任何区别,在IO效率方面没有任何区别。

非阻塞IO有人说他效率高,不是说他IO效率高,是说他非阻塞轮询时可以做其他事情,为什么?因为IO = 等 + 拷贝
阻塞IO和非阻塞IO 在进行数据等这件事情上,区别就是等的方式不同!

第二个问题
同步IO VS 异步IO
你会发现,前4中钓鱼方式,他们在IO的时候有没有等呢?
王五没有等,王五信号驱动,王五要是没有等那他可以回家啊,为什么还在岸边啊。
你这四个人不管怎么等,当鱼咬钩了每个人要不要参与钓鱼的过程,就问你王五钓了没,这四个人钓了没,他们有鱼咬钩都钓鱼了。
所以同步IO本质是当前特定进程有没有参与IO的过程
参与下个定义,要么你参与的等,要么你参与了拷贝,或者俩都参与,只要你参与了IO,那么你就是同步IO。

王五信号驱动好像没有等,其实也算一种等,要不然他早就回家了。一旦有鱼咬钩王五自己要把鱼钓上来。他只要参与IO就是同步IO

异步IO 本质是不参与IO,只是发起IO,最后拿结果就行

这里所谓的同步IO,以前讲多线程时还谈过一个概念,线程同步,这个同步IO和线程同步概念一样吗?
答:老婆和老婆饼的关系,毫无关系。
同步IO指的是IO层面上的概念
线程同步指的是两个线程如何进行谁先谁后,我等还是你等,我运行还是你运行的问题。

最后上面讲的五个人,我们称为在IO场景中的五种IO模型

未来不管是读文件也罢,读网络也罢,不管怎么读,脱不开这五种模型,什么叫模型呢?
模型就是规律,你再怎么进行对文件描述符做处理,怎么读,一定你的读写方式归属于其中某一类。

最后这么多类中哪一种IO方式是效率最高的呢?

有人说田七,田七你再怎么能,你只交给小王一套装备,田七这种异步方式写出来的逻辑很混乱,所以很多技术取代这种异步IO了,比如什么协程

所以这么多IO中我们最值得学习的,也是效率最高的,也是最值得我们了解的叫做 多路复用

还有一个我们也要了解一下 非阻塞IO

在这里插入图片描述


非阻塞IO

快速讲一个系统调用,如何将一个文件描述符通用性的把它设置为非阻塞。

快速过一下课件上的IO模型

多路转接

多路转接和前面的函数最大的不同是,因为IO分两步,可上面的接口,一个函数承担了两种任务,等 和 数据拷贝
所以多路转接的方案在系统层面上编写了新的系统调用select,select他只负责等,事件就绪了他会通知上层,然后上层再来读,这样recvfrom ,read, wrtie各种IO接口直接调接口读数据就可以了,你做你擅长的事情,我做我擅长的事情。
在这里插入图片描述

所以select和poll epoll 最大的特点就是它一次会等待多个文件描述符。

非阻塞IO

在这里插入图片描述
在这里插入图片描述
之前提供的recv系统调用可以设置标记位设置非阻塞,但是这样弄不方便,我们要一种更通用的做法。

因为所有网络通信,包括文件,都是读写文件描述符,所以文件描述符本质就是数组下标,每一个数字下标里指向内核里的文件对象,对象里是有文件的标志位。

所以我们可以用 fcntl 设置文件描述符的属性,其实就是设置文件在底层struct file中的flag标志位
告诉内核这个指定的文件描述符我要以非阻塞的方式来操作

在这里插入图片描述
cmd
我们用的就是获取/设置文件状态标记F_GETFL F_SETFL
FL = flag
在这里插入图片描述
所以今天想单独设计一个接口,只要你把fd给我,我就把你的fd设置为非阻塞,fd只要是非阻塞了,未来你调用read, recv,write , send,都可以以非阻塞方式IO。

在这里插入图片描述

返回值获得了指定文件描述符的标志位f1
之后继续调用fcntl 来设置标志位添加O_NONBLOCK非阻塞

代码证明

我们写一个正常的阻塞式IO接口read,从
键盘流读
在这里插入图片描述

读取时read返回值为0,表示在网络中对端把链接关了,在文件中读到0证明读到文件结尾,在Linux中不想读了我们可以ctrl+D,结束读取他就读到0了

read 返回 小于0 表示read出错了。

在这里插入图片描述
进程就阻塞在这里了,在等待我们进行输入了,当前进程就开始钓鱼了,他一定在read这里阻塞了,read是个系统调用所以他进入OS内部想读,OS说你读什么读呢,你键盘根本没数据把你挂起吧,当前OS就把进程PCB搬到设备的等待队列中。

后来标准输入数据就会把输入消息回显出来,此时数据就绪执行后续逻辑。

这个就是最典型的阻塞式等待,今天我在等的时候不想让他阻塞式等待,我想把文件描述符,大家都一样大家都是文件,所以我只要把一个设置成非阻塞,其他我都能设置成非阻塞。

所以怎么把标准输入0设置成非阻塞呢?
设计SetNonBlock函数
获取struct file 对象里的文件标志位,设置非阻塞标志位。

fl就是老的标志位,是位图,我们要把他谁为非阻塞,那就fl | O_NONBLOCK
在这里插入图片描述
以前open打开文件时,也可以设置flag
也有O_NONBLOCK

在这里插入图片描述

打开文件让这个文件未来的工作方式以非阻塞方式工作。

所以文件完全可以open打开的时候设置非阻塞,因为设置文件非阻塞方式非常多,我们设置函数来非阻塞是最通用的。

在正式通信之前把0设置为非阻塞
在这里插入图片描述
非阻塞成功之后sleep一下,一会肯定会刷屏,先看结果,这次非阻塞读,程序启动我就不键盘输入,read的时候0号fd上没有数据,读条件不就绪,因为已经设置为非阻塞,所以read会立即返回。
在这里插入图片描述
此时读了一次直接read error了,那就n < 0,输出n 看看 是几呢?
在这里插入图片描述

今天读取给我直接出错了,只不过是底层没有数据,没有数据和出错是两码事,所以你不是有错误码吗,我更想看看错误码是几,错误码描述是啥?
在这里插入图片描述

在这里插入图片描述

我们从来没打过字所以肯定返回值肯定不能大于0,等于0表示文件结尾好像也不合适。
所以如果底层条件没就绪好像直接出错了。

在这里插入图片描述

错误码11,临时资源不可用。

你不是说非阻塞轮询吗怎么没轮询啊,主要是出错了我直接让他break了
在这里插入图片描述
把break删掉
在这里插入图片描述
刷屏
在这里插入图片描述

每一次轮询检测的时候 ,都告诉我资源不就绪。
所以刚刚五种IO模型中的李四就开始工作了,不断检测底层有没有数据,可是输入一些内容回车,此时数据其实能被上层读走的。

在这里插入图片描述
只不过采用的是非阻塞轮询的方式,大部分查的时候都没有数据,因为输入的太慢了。

结论

1、将FD设置为非阻塞,如果底层fd数据没有就绪,recv/ read/ write / send,返回值会以 出错的形式返回。
但是为什么以出错形式返回呢?
在这里插入图片描述
因为他没办法
返回值 > 0 就是 读到数据了
< 0 就是关闭了
只能是出错返回。
2、当他以出错形式返回时,所以真实出错不一定是真的出错了,他要分为两种情况。
a.真的出错了
b. 底层没有就绪

3、我怎么区分是真的出错,还是底层没有就绪呢?
通过errno错误码去区分!!!
错误码如果是11,就表示当前根本没出错,只不过是底层数据没就绪

如果出错了,错误码等于11,直接写11有点太扎眼,实际上不叫做11,应该叫EWOULDBLOCK 就是宏EAGAIN的值就是11 意思就是try again
在这里插入图片描述
所以EWOULDBLOCK 情况就是数据没就绪,再试试吧。
或者非阻塞期间做其他事情do_other_thing()。
在这里插入图片描述
如果错误码不是11,则是真正意义的出错了。
在这里插入图片描述
在这里插入图片描述

I/O多路转接之select

select 叫做多路转接的一种,只负责进行等待,一次等待多个文件描述符然后事件就绪了再通知上层。

在这里插入图片描述

select是干什么的呢?

io = 等 + 拷贝

select在Linux中 只负责IO当中的等,一次可以等待多个fd

select就相当于赵六,一个人抱着很多鱼竿

以前的recv,send ,read,write所有函数参数只有一个fd,也就意味着你只能抱着一个鱼竿去等了
而select可以等待多个fd.

select只负责等,接口中看不出来啥,从代码中体现
你说select可以等待多个fd,从哪看出来?
在这里插入图片描述
在这里插入图片描述

第一个参数将来select等待多个文件描述符中文件描述符值最大的+1 ,比如有5个fd,则第一个参数就是6。

返回值有三种情况
在这里插入图片描述

返回值 > 0 : 有n个fd就绪了 读写异常全部就绪的fd的个数
返回值 == 0: 超时,没有错误,但是也没有fd就绪
返回值 < 0 : 等待出错了,比如要等的文件描述符12345,2号早就被关掉了你还传给他,那就出错了。

timeval参数 是一个结构体
在这里插入图片描述
time_t 就是无符号64位数据,代表时间戳,单位:S
microseconds 代表微秒

你可以设置结构体{5,0}代表5S的意思
这个参数表示我们给select设置等待方式。
select一次等待多个fd,最后select等这么多fd,他自己也有自己的等待策略。
比如如果给strcut timeval timeout = {5,0};
代表select每隔5S,timeout一次。
我select 5S以内阻塞式等待,在5S期间没有任何一个fd就绪了,我们的select直接就会返回一次,再重新进入再设置5S重复刚才的动作,如果等待5S期间有fd就绪了我就会立即返回。
也就是select不是死等,一旦把它设置好了他可以按照特定的规律来定期性的醒来。
如果把timeout设为0,也就是select等的时候不要时间间隔,这个就是立马返回,非阻塞的一种。
相当于今天要等10个fd,你检测下来,select下去看了一下没有一个就绪,此时select立马返回。
当然也可以把这个参数设为null,阻塞等待。
我一次等待了10个fd,我等了100S,100S以内没有任何一个fd就绪,我的select就会一直卡在这里,直到有一个fd是就绪的。

如果设置了时间,这个参数是输入输出型参数。
比如每隔5S timeout一次,可是刚过去2S你就有fd就绪了,此时timeout参数输入时是5S,输出时它就会变成3S,因为2S过去了。
他表示的是超时时间还有3S。

在这里插入图片描述
下面到了最重要的地方了。

你从头到尾说select一次可以等待多个fd,你怎么让他一次等待多个fd啊。
所以看到他的参数有三个fd_set

fd_set 是内核提供的一种数据类型,它是位图
在这里插入图片描述

而其中我们一般有很多文件描述符12345,有的fd我只想关系能不能从这个fd直接读数据,有的fd是要关心它上面的写的,有的fd是关心异常。

一般对一个fd,我们想进行IO,我们就要等+拷贝,而我们要进行拷贝就必须判断当前底层有数据,也就是拷贝条件是成立的。

比如我现在去读,那底层必须得有数据了,我现在去写,底层必须得有空间啊。
这叫做读写事件要就绪。

而我们一般在正常情况下,我们要关心的fd上面的事件:

  1. 要么是特定fd上有读事件
  2. fd,有写事件
  3. fd,有异常

所以如果一个文件描述符读事件就绪,这句话翻译过来就是
可以通过这个fd直接读数据,我不会卡你的。
写事件就绪,代表可以直接向fd里去写,write也不会阻塞的。
异常就不谈了。

所以对于任何一个fd,如果只准他关心一种事件,现在认为就只有三种。
如果指向关心特定fd上如果读事件就绪了,我们就让select来通知我,此时就应该把fd设置进readfds集合里,如果你想关心写事件就绪,你就把fd设置进writefds里。
异常就不说了。
如果我想既关心读又关心写,我可以把fd
设置到readfds 和 writefds
如果我想先关心读,在关心写,我可以先把fd添加到readfds ,把数据读完了我再把对应的fd添加到writefds里

为了方便表述,我们介绍具体参数时,只拿一个来接受,一个通了剩下的两个就一通百通。
我刚说的大概率你不怎么明白,不要着急,下面来谈具体的。

说半天就想说select为什么会有三个fd_set参数
第二个告诉我fd_set是一个位图,那他为什么是一个位图呢?
主要fd值是01234567连续的数字
所以用它来表示位图是什么意思呢?

以readfds为例
在这里插入图片描述

目前得到的信息是
1、fd_set叫做读文件描述符集
2、文件描述符是连续数字0123
3、fd_set是个集合
这几个有什么关系呢?

这个集合既是输入型参数也是输出型参数,也就是除了第一个参数,select后面所有参数都是输入输出型参数。

那么当他在输入时,也就是当我们调用select的时候把位图文件描述符添加到集合里。
输入时表示的是用户告诉内核,OS啊我给你的多个或者一个fd,你要帮我关心fd上面的读事件哦,如果读事件就绪了,你要告诉我!

当我们在输出时:或者select返回时
代表内核告诉用户,用户你让我关心的多个fd中,有哪些已经就绪了,用户你赶紧读取把!

fd_set是一种位图,为了表述方便
这个位图就当成8位的位图就可以了,
今天一次想向select里添加4个fd,比如0,1,2,3
我想告诉OS你要帮我关心0,1,2,3这四个fd上面的读事件有没有就绪
所以当我们传入的时候,我们只需要把位图由全0,改成 0000 1111
比特位最低位代表的就是文件描述符。
输入时比特位的位置,从右向左数第几个,表示文件描述符编号,比特位的内容,是0还是1,表示的是否需要内核关心。

所以当我们输入的时候,是用户想告诉内核我给你一个或多个fd,你要帮我关心上面的读事件哦。
所以输入时需要传一张位图,比特位的位置表示fd编号。输入时比特位的内容为0,为1,OS要依次帮我关心比特位为1的读事件哦
在这里插入图片描述
也就是输入的时候需要给select传递一张fd表,以位图的形式呈现的。
后来OS在底层通过select帮我去关心用户设置的0,1,2,3四个fd,四个上面的读事件。
后来发现2号fd可以读了,所以OS返回的时候OS说2号fd就绪了,所以他把没有就绪的全部清零,把2号置1。
在这里插入图片描述

返回时也是一张位图,把你的位图直接修改了,给你保留一个2代表用户你自己看2号fd就绪了,哪个就绪就把哪个置1。

select成功返回时,内核告诉用户,你让我关心的这么多fd哪些已经就绪了。

返回的这张位图,比特位的位置还是代表文件描述符的编号,返回时比特位的内容是0 or 1表示哪些用户关心的fd,上面的读事件已经就绪了!

2号fd读事件已经就绪了,所以上层你就赶紧从2号已经就绪的fd赶紧读数据吧,我保证你这次读不会被阻塞了。

我们在OS中 ,select中,fd_set 是一个位图,这个位图本质是用来进行让用户和内核双方传递fd是否就绪的信息的!

所以毫无疑问,fd_set本质是个位图,传入时要设置好位图,返回还要检测位图,而且他就是输入输出型参数
所以注定了我们使用select的时候,一定会有大量的位图操作!

检测特定的某一个位图是否为真为假,把位图清零,置1。
OS不让用户直接对fd_set进行与或非操作。

所以OS提供一批位图操作的接口

在这里插入图片描述
FD_CLR 在一个集合中 把fd去掉
比如fd = 3 , 就把第三个比特位改为0
FD_ISSET 判断fd是否在集合中
FD_SET 把fd添加到集合里
FD_ZERO 将整个fd集清零

所以select最核心的就是这个fd_set

所以写writefds,异常exceptfds同理

readfds 表示一个fd添加到这个集合中表示只关心读,如果同时把fd也添加到writefds表示既关心读又关心写。
如果先添加到readfds 再添加到writefds 表示先关心读再关心写。

所以最终select本身并不负责真的读还是写,他只用来在底层检测底层读或者写入条件是否满足,满足就修改对应的文件描述符位图就行。

如上是关于select函数介绍。


select是最恶心的,因为他早嘛

直接写代码,逐步验证上面的所有内容

在这里插入图片描述
创建套接字初始化服务器,就不说了
接下来的问题是我们应该accept还是要干其他事情呢?
接下来for循环中要不要accept呢?原因呢?
能不能直接accept呢?
毫无疑问,不能直接accept,为什么不能?
accept 的本质就是在检测listensock上面有没有连接事件,什么叫连接事件,意思就是说底层有三次握手,对方给我发了SYN,我给对方SYN+ACK,最后对方给我ACK,我们accept的本质就是在检测并获取listensock上面的事件,你如果自己去accept了你阻塞进去了,说好的一次要等待多个文件描述符呢,那怎么用select呢。

说了所有的IO文件描述符事件就绪,IO分两步等和拷贝,accept大部分时间都在等,等一次accept只能等一个fd为什么不交给select呢。
所以服务器for循环\里不能accept而是要调用select

第二个问题
我们检测并获取listensock上面的事件是什么事件呢?一般叫做新链接到来,那新链接到来是什么个事件呢?
新链接到来本质是OS底层把三次握手完成,把新的连接投递到了当前服务器的全连接队列里,然后你在通过accept把链接拿上来。
新链接到来对于select来讲相当于读事件就绪
所以此时千万不能直接accept,我们应该select。

在这里插入图片描述
问题又来了,很尴尬。
那今天select 他要让我一次等多个fd,现在服务器刚开始我只有一个fd,我怎么等多个啊?
随着你连接的到来,你的新链接不是有不断的fd不断在增多吗?
所以服务器刚启动只有一个fd(listensock),正常啊,刚启动谁连你,你启动之后过一段时间才有客户端连,所以fd会有增多的过程。

所以第一件事情其他fd先不加,只加一个listensock
第一个参数fdmax+1,我现在只有一个fd,没有值最大,我就硬写listensock+1

第二个今天只关心listen套接字上面的读事件,你要把他添加到集合里。

你要有一个读文件描述符集fd_set rfds
今天要让select关心读文件描述符集
这个位图刚在栈上定义可能有些乱码,所以
要先清空FD_ZERO 把指定集合先清空。

输入时用户告诉内核,我给你多个fd帮我关心。
今天让select关心写事件,把制定的listen套接字填加到集合中FD_SET
在这里插入图片描述

这里又有一个问题,此时我们这样添加有没有把listen套接字设置进内核中呢?
我们并没有把监听套接字文件描述符设置进内核里。
rfds是栈上的类型,还是在用户空间,你把rfds传递给select后才设置进了内核。
在这里插入图片描述
写时间 和 异常 不关心 nullptr。

等的时候还要有timeval,需要头文件sys/time.h才有struct tiemval 类型
在这里插入图片描述
定义一个struct tiemval timeout = {5,0}
也就是每隔5S timeout 一次

在这里插入图片描述
至此相当于把select调起来了。

接下来对返回值做判断

这里直接switch
如果是0代表没有事件就绪,而且时间超时了。
我就想看看超时timeout中的数字变成啥了。
在这里插入图片描述
如果返回值是-1,就出错了。

缺省default 表示有事件就绪了

下面先验证你这说的输入输出型参数

把timeout放到循环体外面,为了演示效果
在这里插入图片描述
服务器启动刚开始5S啥也没有在这里插入图片描述
5S左右 timeout了, timeout时间怎么是0.0了
为什么呢?
时间timeout是输入输出形参数,5S超时之后,没有任何时间就绪,此时时间变为0 了,下一次select的时候timeout的值已经被上一次的超时修改了,所以你的超时时间就变成了0。
也就相当于没有就绪立马返回了,非阻塞轮询。

所以为了让他每隔5S进行timeout一次,所以你要超时时间重复设置,放进for循环里。
在这里插入图片描述
在这里插入图片描述
当然如果有事件就绪的话,select会直接返回
这里是0,但是每次有重新设置了timeout,所以select阻塞几秒超时一次。

结论
timeout 是输入输出,可能要进行周期的重复设置
这句话不仅仅对他有效,未来还要考虑事件

如果timeout设置为0
相当于每次select底层检测一下,没有就绪的话立马返回了,这就是传说中的非阻塞轮询。
虽然select是多路转接但他也可以以非阻塞轮询的方式检测所有文件描述符。

这种方案不会被我们采纳的,这种方式太消耗CPU资源了。

如果timeout直接设为nullptr,代表的是select会一直阻塞,直到有事件就绪(连接事件到来)。

设置timeout相当于给select等的时候各种策略

下一步呢
你不是已经把监听套接字fd设置进去了,如果有listen事件就绪了呢?

所以default中
在这里插入图片描述
这个新链接是谁后面说。

在这里插入图片描述

一旦建立了新链接,select果然告诉我们现在有了新链接,有就有呗你这个select疯狂打印有链接来了。

因为链接来了,底层有链接了,但是我们并没有把链接取走

所以select的特点是:
如果事件就绪,上层不处理,select会一直通知你!
你说我忙着呢不想处理,不行你必须立即处理。

所以现在获得新链接 走到后面就HandlerEvent()
一旦有事件就绪了,我们就要处理对应的事件了,那所有已经就绪的事件在哪里呢?
就在rfds里
在这里插入图片描述
如果未来还有写 和 异常,三个参数全部传给HandlerEvent
我们在函数里就分析fd哪些就绪了。

如果select告诉你就绪了,接下来的一次读取,我们读取fd的时候,不会被阻塞。
因为底层数据已经就绪了,你不需要等了,你只需要读。

目前这个代码根本跑不了,后续要处理listen,慢慢完善结构。

select返回时fd集合变为哪些fd已经就绪了,比特位是1
直接判断,FD_ISSET(fd,rfds)
如果条件成立,说明连接事件就绪了,那就获取新链接呗。
在这里插入图片描述

获取accept的时候会不会阻塞在这里?
不会,因为上层select告诉我事件已经就绪了。
accept今天不需要等,直接获取连接

在这里插入图片描述
只要把链接获取上来了,它就会给你提示一次。

问题
拿到新链接,接下来这种fd是真正IO通信的,所以可不可以直接read?
可以直接读取数据吗?
在这里插入图片描述

答案是不可以
我们以前可以,是用的多进程,多线程,线程池,你把fd托管给执行流的,所以他阻塞一下不影响
但目前为止我们的代码是单进程,没创建多线程,所以你敢直接读吗,我和你把链接建立上,我就立马给你发消息吗,如果不给你发,你如果直接读,当前进程直接阻塞了,你的程序在HandlerEvent就不会返回了。
此时也就不会下一次select。

你不是说select一次可以等待多个fd吗
可你给select只喂了一个listensock,有没有更多的套接字要被select管理呢?

显然随着listen套接字被设置,我们从listen套接字获取到的文件描述符就应该变得越来越多,这些listen套接字到底IO事件有没有就绪,我们完完全全要交给select处理。

所以你今天返回的新链接的sock文件描述符
要做的并不是直接read,而是想办法将sock设置进select里。

所以我们该怎么做呢?
因为只有交给select之后,select的读文件描述符集的fd会变得越来越多
select一个系统调用就能同时等待多个文件描述符。
这就是当年的赵六,抱着多个鱼竿。

fd_set 是个位图,并且是个具体类型,具体类型一定有自己的大小,只要有实际的大小,fd_set一定有它位图当中比特位的个数,一旦有比特位的个数是确定的话,也就意味着select等待多个fd一定是有上限的。

那他一次可以向select添加多少个fd呢?

在这里插入图片描述
我们就算算有多少bit吧
1024个 比特位。
位图的比特位的个数最多是1024个
也就是最多监视1024个fd

今天HandlerEvent里sock和Start函数中的select是分布在两个函数里,我们怎么把sock传给select呢?

所以使用select时,我们想进行获取文件描述符和常规的select之间,我们要进行fd的传递的话呢,以及方便后续使用select,我们需要写这样的代码时,往往需要在select服务器里设置辅助数组来进行文件描述符值之间,在不同函数之间互相传递。
这个是select最大特点之一

这个数组 fd_array[ 1024 ]
也就是fd_num_max = 1024
在这里插入图片描述
我们在服务器对象构造函数的时候把整个数组都设置为defaultFd = -1
在这里插入图片描述
接着呢,刚开始的时候只有一个listen套接字,rfds是输入输出型参数,所以当年输入的时候可能是1111 1111 可是返回的时候只有一个fd就绪了 0000 0001,甚至超时了没有fd就绪就被清零了
所以rfds 这个值 输入输出用的是同一个参数,所以返回时这个rfds就被重新覆盖了,那么位图中要关心的值就没有了,可是如果你还要再关心呢,此时就要求我们在select返回处理完之后,回到for循环最开始每一次都要把rfds要关心但没就绪的fd重新设置。

select 第一个参数的最大fd值+1
那么当前来看,对应的listensock+1不是未来服务器正常运行时的最大文件描述符。
我们不断获取新链接,对应的fd值会越来越大,你固定写listensock+1是不行的。

你所有的fd不就保存在辅助数组中吗,所以最大fd应该是动态计算的。
在这里插入图片描述

我们可以固定一下,因为listensock 是一直不变的。
fd_array[ 0 ] = listensock;
服务器启动之后呢,0下标就设置好了
接着要做两件事情
1、未来所有的fd包括listen套接字都在辅助数组里
2、我们未来也关心写事件的话,你可以再定一个关于写的辅助数组
在这里插入图片描述
3、我们还要得到最大fd值+1

让maxfd默认等于listen套接字
在这里插入图片描述
将来一个fd放在辅助数组里,其中可能12345连续的放,可能最后有3,7,8这样的fd就关掉了,所以你刚开始放的时候连续的放,最后我们关闭的时候每个文件描述符生命特征都不一样,所以他可能零零星星的就直接关了,同学们可能需要对fd array,比如你把所有fd都放在整个数组的最左侧,把-1放在最右侧。
我们今天简单粗暴一点。
循环遍历辅助数组,判断辅助数组里的值如果等于defaultfd 说明当前没人用它,就直接下一个continue,直到fd_array[ i ] 是被设置过的。
走到continue之后说明当前fd_array[ i ] 是一个合法的文件描述符,把这个合法的文件描述符添加到读文件描述符集里。

listen套接字最开始被设置过,所以肯定不等于defaultfd = -1 所以在遍历辅助数组时直接被添加到读文件描述符集里。
随着循环的进行他会把所有合法的文件描述符全部都添加到对应的rfds里。
同时在进行统计的时候,还需要更新maxfd给select 第一个参数传参。

在这里插入图片描述
我们经过这样的循环,就能在调用select 之前够把所有合法的fd重新在rfds重新添加,并且更新最大fd

这是第一次循环

在这里插入图片描述
输出日志,看到最大fd是谁
在这里插入图片描述
这个重新设置rfds的动作(第一次循环)是在服务器的for循环内部的。
也就是select调用之前每一次都要对它重新设定

有了辅助数组呢,后来HandlerEvent 函数里FD_ISSET判断listen套接字是否在集合rfds,如果在集合里说明时间就绪了,就获取连接。
你刚说了,连接获取上来了,你不能直接read,你直接read就会阻塞住,因为你并不清楚这个套接字当前上面的读事件是否就绪,你不清楚谁清楚,select最清楚,可我怎么把我accept上来的sock fd添加到select里呢?

你只需要将新获取的连接sock 添加到辅助数组里,select把消息处理完毕之后,下次循环时会重新进行把fd添加到rfds,紧接着交给select由它来监听了。

所以在HandlerEvent里,一旦有链接就绪了,用for循环辅助数组来找到没有被使用过的位置,也就是找到数组中是-1的,把新链接的fd放进去
在这里插入图片描述

这里出来循环时,第一种情况
如果等于整个数组大小全遍历完了,所有数组fd都是被占用的,我就没能力处理这个链接了,因为select参数不允许最大比特位就是1024,服务器满载。
直接关闭这个获取上来的连接
第二种就是循环中提前break了,就是在辅助数组里找到了一个没有被使用的位置
我们把新获取的连接往辅助数组可以使用的位置一添加,此时就有了文件描述符了。

这是第二个循环

我把对应新链接套接字添加到辅助数组里,连接获取就完成了,select未来把所有事件处理完了,下一次服务器会重新for循环时,整个辅助数组的fd增多了,所以此时就会把新的fd再添加到读文件描述符集rfds里,然后让select再进行关心。

谁规定新来了连接一定要立即处理它,所以我把链接放到数组里,下一次selcet之前我们会重新遍历数组,重新添加,重新关心对应的读事件,或者其他事件。

所以我们现在具备了让select不断获取更多fd的前提了,select当中fd变得越来越多了,可是有的是listen套接字,有的是正常的fd读事件就绪,有的是连接事件就绪了,你怎么区分是哪种事件?

我以前只有一个listen套接字,我直接就HandlerEvent了
可是今天随着不断运行,我们连接上来的文件描述符越来越多了,对我们来讲你怎么知道HandlerEvent,关心的事件越来越多,就绪的事件和fd你怎么知道只有一个listen套接字就绪呢??

在这里插入图片描述

有没有可能他也有其他套接字也就绪了呢?

目前可以看到对应辅助数组里的文件描述符一直在增多
在这里插入图片描述
意味着实际添加到select当中的文件描述符也越来越多了,所以HandlerEvent 的时候你怎么知道有多少个fd就绪了呢?你怎么知道是哪些文件描述符就绪了呢?
对不起你不知道。
那你怎么知道呢,所以只能HandlerEvent 的时候for循环遍历整个辅助数组,遍历所有文件描述符,如果数组中的值是-1就continue不玩了,说明这个fd不用管不合法。
在这里插入图片描述

走到下面就要判断了,判断文件描述符是否在select返回后输出的所有已经就绪的文件描述符rfds当中
告诉我们所有文件描述符都在数组里,而已经就绪了的fd在rfds里,根据你所有的fd来在读文件描述符集里去找,找所有就绪的fd有哪些。

判断所有文件描述符是否在对应的读文件描述集里,如果在就说明读事件就绪了。

今天要做比较关键的事情是, 此时后面做的事情就是获取新链接的事情,今天的判断条件应该是 :我是监听套接字并且我已经就绪了。

在这里插入图片描述
所以我们做的工作在下面就叫做获取新链接
在这里插入图片描述
还可以这么写,这么写放到里面更好
如果当前文件描述符在rfds集合中,说明读事件就绪了,读事件就绪分两种情况

第一种 如果对应的文件描述符 == listen套接字,那么你就叫做获取新链接,把新链接继续添加到辅助数组里。
在这里插入图片描述
HandlerEVent的for循环每次处理一个fd是否是连接就绪还是读写就绪,所以连接就绪中作相应的返回细节处理, accept获取失败continue继续处理下一个fd
在这里插入图片描述
在这么多fd里,你怎么知道哪一个fd是就绪了呢,我们通过FD_ISSET(fd,&rfds) 判断fd是在集合里就知道了。
他是listen套接字就处理获取新链接。
另一种情况就是其他文件描述符就绪了,读事件就绪了。
这时我们就可以进行读取了,read,从就绪的fd里读。
在这里插入图片描述
此时就读了一次,之后就把消息打出来,就认为读一次就OK了。
至此就把一个读事件处理完了。
他把这个fd处理完了,下次再进行循环时,如果再有事件他还会就绪他还会通知我。
在这里插入图片描述
当然会有读失败 read 返回0,对方把自己的写fd直接关了,关了属于正常现象,对方把链接关了,那服务器也要关,此时
fd_array[ i ] = defaultfd;
直接把辅助数组里的fd请了。
然后close(fd)
把HandlerEvent处理完,重新回到开始,下一次select的时候,我们对应文件描述符在辅助数组里就变为defaultfd了,他就不在被处理了,他就从select中被移除了。
在这里插入图片描述
当然呢read返回 < 0 说明read出错了
今天不做过多处理
在这里插入图片描述
至此就把读事件和连接到来事件处理了。

此时不要忘记我今天可是一个单进程服务器哦!

在这里插入图片描述


select本身 每一次都要循环辅助数组对fd重新设定。

这里的read也有bug,底层有读写事件到来时,你能保证对方给你发过来的buffer里面就是一个完整的报文吗?
在这里插入图片描述
TCP不一定把完整报文给到buffer。
我们还要后续写,去分析buffer内容够不够,再往上写就到了协议定制,但在这里暂时不做。

我们获取新链接的工作可以单独拎出来,也就是把获取工作单独放到Accepter()函数里。
我们管Accepter()函数叫连接管理器
在这里插入图片描述
下面正常读的工作呢,我们也可以拎出来到Recver()。
在这里插入图片描述

所以对应的select中的HandlerEvent 与其叫HandlerEvent 不如说他收到了多个就绪的fd,把就绪的fd派发给不同的,你是连接到了就给你获取连接,你是事件就绪了就给你读,所以这里呢给HandlerEvent 换个名字叫事件派发器,Dispatcher。

总结select

扭转思维方式,所有的等待过程都要交给select,你不能直接去read读,你一读服务器就阻塞住了,而我今天一旦有事件就绪就让select来告诉我们。
我们的select就会把哪些fd由数组来保存,上面哪些fd事件已经就绪rfds也能支持我们,我们对它就绪的事件进行派发,他就可以派发到有的进行连接管理,有的是用来进行IO处理的。
再往上写可以给select设置回调,当他Recver的时候我们把读上来的buffer交给上层由上层来处理。

目前为止发现了一个现象

select服务器有什么优缺点呢?
重点放到缺点上。
优点
多路转接方案,单进程处理多用户请求。
select一次等待多个fd,所以多个fd的事件是否就绪,select可以把所有等待时间重叠起来,这样的有任何多个就绪我们就可以知道这个事件就绪,然后把事件派发上来,让上层处理,要么获取新链接,要么读写数据。

缺点
1、等待的fd是有上限的。
因为它是以位图方式来交给select,一次交给他多个fd,他是一种fd_set类型,所以大小有上限,所以位图个数比特位个数有上限,所以select服务器同时监听一个类别的事件所对应的fd就有上限。

先别管OS有没有打开的文件的上限,进程打开的文件有没有上限一般是32/64个,充当服务器的时候一个进程能打开的fd开了65535个,有的开到10W个,普通服务器一般都是32/64
在这里插入图片描述
先别管OS有没有打开的文件的上限,进程打开的文件有没有上限,你select接口本身是有上限的,这是你的问题

2、select 后四个参数,输入输出型参数比较多,意味着频繁的调用select就需要频繁的从用户到内核,从内核到用户的数据拷贝。数据拷贝的频率比较高。而且是遍历是的修改
未来还有写,异常呢

3、因为select 后四个参数,输入输出型参数比较多,意味着每一次调用select之后,我们需要下一次每次都要对关心的fd进行事件重置

你一旦select调完之后,它上面所有fd其实到底有没有就绪你不清楚,所以它返回之后rfds可能已经被清空了,比如超时了,清空之后你下次调用select 你要关心的rfds 一个都没有了,所以你只能不断的进行重置。
重置不就是循环遍历所有的fd然后重新添加吗,这里就注定了需要有很多次遍历。
在这里插入图片描述

4、用户层,使用的是第三方数组管理用户的fd, 用户层需要很多次遍历,内核中检测fd事件就绪,也要遍历

a.重置的时候,我们要对所有fd进行遍历
你可以优化代码,拿个vector把所有fd排序,负数放最后正数放前面,你可以减少他遍历的次数。
但遍历就是常数级别的,提升不大。
在这里插入图片描述

检测事件就绪的时候还要遍历

在这里插入图片描述
获取完连接的时候,在重新寻找位置添加时还要遍历
在这里插入图片描述
因为它使用的是第三方数组管理用户的fd,用户层需要很多次遍历。

另外呢,OS在底层关心十个八个fd,他怎么知道哪些fd上面有读事件就绪了呢?
所以select是系统调用,他会在底层帮我们每次调用的时候都会去遍历所有你要让我关心的fd上面有哪些事件就绪了
你让我关心的话,那么select底层内核中也是遍历。
说白了就是遍历进程fd表,你是单进程,所有打开的进程的fd都在fd表中。
这个数组有点大,那么我要遍历大什么地方呢?
所以需要你传入最大maxfd+1,因为fd本身就是数组下标,所以+1就是遍历最大范围。

select长时间调用,如果fd越来越多,这四种缺点触发频率越高,但是一旦就绪了带来的结果可能发生很多次拷贝重置遍历

select一般在小型的嵌入式设备里,或者小型的通信场景中你不想使用线程池,最多满足小型应用。

select最大的问题就是 支持的fd数量太小。

select硬伤:
在这里插入图片描述

为了解决这样的问题
才有下一种解决方案

poll

只要听懂seledt,poll也一定能懂

接口

在这里插入图片描述
他作用和select一模一样,他只负责等待

他只不过是在select基础上解决一下select两个硬伤
在这里插入图片描述

poll返回值和select返回值一模一样

timeout 是一个整形,时间是毫秒,未来想让poll等待多少时间直接设置就可以
1000 每隔1Stimeout一次
0 非阻塞进行poll
-1 永久阻塞直接事件继续

poll 第一个参数
struct pollfd* fds
理解成是一个数组,这个指针是第一个元素的地址
nfds_t nfds代表数组有多少元素

在这里插入图片描述
struct pollfd结构体中
int fd

events

revents

多路转接无非就是要做到两点
在这里插入图片描述

1、用户告诉内核
2、内核告诉用户

所以poll的作用和select一样
只不过select用位图,poll用结构体数组

他传入的时候,是用户告诉内核,你要帮我关心fd上的events 事件哦
当他返回时,代表的是fd上的revents事件已经就绪了

poll 的结构体最大特点是,将输入和输出事件进行了分离!
不像select用同一张位图,比特位输入输出含义还不一样。

这里结构体只有一个fd,所以未来你要同时关心多个fd,你就传入包含多个fd及其关心事件的数组就可以了
一旦有就绪了你需要遍历数组然后检测revents
哪些文件描述符哪些事件就绪了你也就清楚了。
这里有个short类型代表关心的事件。
可是select人家还给我在参数层面上把事件类型做了区分
poll要关心events 关心读还是写还是异常
这里只有short,你怎么告诉内核呢?

那内核告诉用户时哪些fd就绪了,他确实可以通过revents去返回,可是revents只是short类型,到时是读还是写还是异常事件就绪了呢?

short类型 16个比特位
Linux内核中OS特别喜欢使用比特位传参
所以把事件events设计成位图的样子。
你要读就设置POLLIN
大写的全部都是宏
在这里插入图片描述

上面的宏对应的宏值本质是用short 16bit位来传递标记位

将来所有比特位只有一个比特位为1,且彼此之间互不影响。

这样就可以一次用short 16比特位向内核传递16个比特位
同样内核也可以给用户16比特位。

所以未来哪些事件要关心 设置进位图

哪些事件就绪直接检测位图的值就可以了。

poll的本质将读写事件分离,传入用户定的数组元素大小,通过event和revent s以位图的方式来传递就绪和关心标记位的解决方案。
POLLIN 读事件继续
POLLOUT 写事件就绪

在这里插入图片描述
这个数组多大随便写,你也可以用动态扩容的策略做。

刚开始初始化的时候,所有要关心的事件就是没有事件就是0
fd 一样初始化为-1
在这里插入图片描述
把listen套接字传进来
让OS关心的事件 等于 读事件
所以events = POLLIN就行
revents 你愿意你就设置,未来他就绪了由内核来填,设不设置都无所谓了。
因为revents这个字段我们用户都不改,我们只查它,是OS改的,所以revents是几不重要

在这里插入图片描述

如果结构体里文件描述符都是-1,poll就不关心这个fd , 只有常规的fd才会设置

timeout 设置为3000
在这里插入图片描述
每次进行调poll难道不用重新设置listen套接字重新设置最大文件描述符是几吗
不用了,往后poll底层就绪只会改revents,他不会改fd 和 events
我们再也不用poll之前重新对所有事件重新添加了。

派发的时候,我怎么知道有哪些事件就绪了呢?
把结构体数组和数组个数交给派发器
所有就绪的事件都在结构体数组里
在这里插入图片描述
其实可不传入这个结构体,因为结构体就在成员属性数组中。
在这里插入图片描述
在派发的时候,还是遍历数组里的fd

拿到合法fd之后,我怎么知道这个fd有没有读事件就绪了呢?

revents就是内核告诉我们哪些fd上面的读事件已经就绪了。
所以通过 revents & POLLIN 判断

在这里插入图片描述
如果fd == listensock
下面继续调Accepter连接管理器

在这里插入图片描述
listensock 读事件就绪,继续accept获取新链接
继续在结构体数组中找没有被占用的fd数组下标
如果找到数组最大下标说明达到设置上限,我们可以扩容,并把服务器的结构体数组设为指针,
在这里插入图片描述
不像select没办法直接就是满载了
不想扩容就直接定一个65535
今天还是达到设置fd上限就关闭fd

不是满了的情况那就是找到一个没有被占用的位置,我们继续让poll关心这个连接读事件。
在这里插入图片描述
所以最后Accpeter我们也是获取连接,然后找位置,找到之后直接把他添加到poll数组里。
下一次再poll他就又帮你关心了。

最后还有Recver正常读的函数
Close(sock)
结构体中的fd设置为-1
在这里插入图片描述
事件不用管一旦有就绪了他Accepter时重新分配会重新填写 ,我们把fd设为-1无效就可以
在这里插入图片描述

测试结果
在这里插入图片描述
建议直接用poll,比select简单

有人就说了,不对
poll解决了select 的 2个问题
1、fd有上限
2、输入输出型参数比较多,每次都要对关心的fd进行重置

确实调poll的时候已经没有之前那么蹩脚了,不用重心设置要关心fd,还有求maxfd
这个工作确实没做了。

fd有上限,poll不是还是数组吗?
怎么解决了fd有上限的问题???
凭的就是这个数组多大由谁说了算
我可以1000,10000或者干脆10W
,poll说100W你也可以设置哦。

站在旁边的OS和内存就开始摇手了,不行不行不行,你如果直接设置成100W我内存扛不住,OS说俺也一样
poll说和我有什么关系。
你扛不住是OS的问题,是你内存的问题,和我poll无关。

select站出来了,不对,我也是有上限的啊,因为我自己也有fd_set啊

poll白了一眼select
你不能和我比,你的问题是你自身造成的,我的问题是别人造成的,我给你提供了一个指针,一个大数组,我甚至都没说这是个数组,他是个指针,你到底要传多少个,我把参数给你,你自己定
可是你select呢,你是个位图,超过位图select还能用吗?
我poll由数组,由用户定,出问题效率低,或者是干脆文件描述符数组有上限了,那是用户编码的问题和poll无关

poll函数解决了fd有上限的问题。

所以poll确实解决了fd有上限,还有每次都要对关心的fd进行重置

既然我能很平滑的把select改成poll说明他们在不同的地方也一定有相同
1、遍历。
poll依旧需要再用户层遍历检测已经就绪的revents 哪些事件已经继续了。
在内核中poll也需要遍历来检测你所关心的众多fd哪些已经就绪了。
你敢想吗,poll提供了一个没有上限的数组,你可以动态扩容
那用户给poll传1W个fd,我再OS内部就由OS协助我去检测1W个fd哪些就绪全部收集一遍
收集一遍我就要遍历一次fd表,我可是个单进程哦,我可是有fd表的哦。

你有1W个就遍历1W次,那fd越来越多可能最后在内核层遍历一遍就很耗时,poll不是等多个fd吗,理论fd越多效率就应该越来越高,就跟赵六一样,可是随着fd越来越多,遍历就成主要矛盾了。

所以poll还有效率问题

所以为了解决这个问题怎么办呢??

更改底层数据结构帮助不大,因为遍历依旧是主要矛盾

epoll

改进的poll 但是他和poll完全是两个物种
差别太大了。

公认的Linux 内核2.6之后编写服务器时效率最高的多路转接方案。

目前主流服务器或者服务器框架libevent Muduo Memcached Redis Nginx
消息队列 大部分的涉及到服务器组件用的全都是epoll

快速认识epoll的接口

在这里插入图片描述

epoll_create

创建epoll模型
size随便写,已经废弃了。
返回值 也是一个fd

epoll create1是新标准,我们用epoll create

epoll_wait

在这里插入图片描述

通过epoll wait 获取已经就绪的fd
第一个参数epoll create 返回值

第二个,第三个参数
将来定义的用户级缓冲区,用来把已经就绪的fd及其事件返回

timeout 单位ms 和 poll一样
代表是否需要超时
3000 3S timeout
0 非阻塞检测所有就绪事件
-1 阻塞式等待
返回值大于 0 已经就绪的fd 的个数
等于0 超时
小于0 epoll waita error

struct epoll_event*& events 又是一个结构体
在这里插入图片描述

第一 events 哪些事件,就绪
以位图的形式,传递标记位

第二 epoll_data_t data
是一个联合体,可以选择使用上面联合体的字段的任意一个,用来保存用户级数据

epoll_ctl

在这里插入图片描述

向OS里新增一个fd 及其要关心的事件
想修改对特定一个fd进行关心的事件

在这里插入图片描述
第一个参数就是epoll_create的返回值

op代表的就是下面的三个选项

fd event
哪一个文件描述符上的哪一个事件

所以epoll你要使用的话,你要使用三个系统调用哦

select 或 poll 只有一个系统调用哦

三个各自代表什么意思呢?

无论是poll 管理所有文件描述符和事件是用数组来管理的,主要是用数据结构的遍历操作,大部分数据结构的遍历时间复杂度都差不多。
问题不在于数组,这里对应的数据结构是用户维护的!
这个特点对于poll是如此,select也是如此。

epoll有什么样特征呢?
为什么要有三个系统调用呢?

epoll的原理

在这里插入图片描述

select 和 poll 只有一个进程,他每次调的时候
都会帮你进行select去检测,检测的过程实际上是遍历你的当前进程的fd表,检测每一个数组下标所对应的file对象,file对象拿到,文件属性拿到,缓冲区状态拿到他就可以检测数据是否就绪了。
但是他毕竟还是要遍历
如果select或poll的时候底层一个fd都没就绪,而且等待方式是阻塞等待,此时进程就会被挂起到等待队列中,OS定期唤醒进程时,他不是要调度吗,时间片到了顺便帮你再检测fd有没有就绪
这样每次让 OS遍历,OS就绑在这里了。
有没有就绪OS都得自己去查。

在最底层叫做网卡设备
聊一个问题
OS网卡是个外设,OS在硬件层面,怎么知道网卡上有数据了呢?
他是个外设,所有外设一旦就绪了,对于硬件来讲会给OS会以硬件中断的方式来告诉,一旦事件就绪了,网卡就会给CPU触发硬件中断,由CPU根据中断号,查中断向量表,然后执行OS的代码,把网卡的数据搬到OS内部,与其说搬其实是调用网卡驱动层方法把数据从网卡 拷贝到数据链路层。接下来就开始向上交付了。

下层通过硬件中断来告诉网卡驱动有数据了,可是我们最终要数据是想从文件缓冲区里把读上来的
所以OS为了支持epoll , 他为我们提供了三种机制
第一种机制
OS自身内部帮我们维护一颗红黑树
这颗红黑树呢里面,红黑树有自己的染色规则,进行自适应调整。

这里一个个节点 是 struct结构体
struct rb_node
{
int fd // 内核要关心的fd
uint32_t event // 位图形式呈现 要关心的事件
// 链接字段
}
这个fd上面是否有数据就绪一会再说
每个节点都是fd 和 要关心的事件
这是OS做的第一件事情
在这里插入图片描述

第二件事情
在这里插入图片描述

OS为我们维护一个 就绪队列
队列可以用数组 或者 list 双链表
一旦红黑树中有特定的一个节点,比如这个节点的fd 的事件就绪了,此时就可以把该节点链入到就绪队列

一个Node可不可以既在红黑树 又在就绪队列里?
可以
但是为了我们方便表述,所以此时一旦事件就绪了,就会在OS内部为我们形成一个新的队列节点,会有很多就绪的节点
队列中的也是结构体 struct list_node
struct list_node
{
int fd 表示已经就绪的fd
uint32_t event // 已经就绪的事件
|
在这里插入图片描述

换句话说 如果今天红黑树里fd 是3
然后要关心的事件叫做EPOLLIN关心读
一旦事件就绪了,他会在队列中形成一个新的节点入队列,假设就是第二个。
其中已经就绪的fd 就是3
已经就绪的事件就是 EPOLLIN

这个工作完全由OS自己来做

这是第二种机制

第三种机制
在这里插入图片描述

OS底层的网卡,他是允许OS去注册一些回调机制
OS内部会提供一个回调函数
这个回调函数是干什么的呢?
网卡以中断方式把数据搬到网卡驱动层,一旦网卡驱动层中比如数据链路层中有数据就绪了,那么数据链路层自动会调用callback回调,调用回调函数中他要做什么呢?

void callback()
{

  1. 向上交付

  2. 数据已经到来了,解包然后交到TCP接受缓冲区(TCP接受队列)
    此时在文件角度这个数据你已经收到了
    然后第三步

  3. 用TCP接受队列关联的fd做键值查找rb_tree红黑树,确认是否关心过此次fd的事件
    fd (0 - n) 做键值天然合适,所以查找红黑树 确认 对应TCP接受队列和哪一个文件描述符或者struct file对象是关联的,还有第二他要不要关心有没有EPOLLIN、EPOLLOUT呢
    如果它查到了fd = 3 ,并且3号事件关心了EPOLLIN,并且这次到来的数据真的是我们对应的3号文件描述符对应的struct file,所以此时第四步

  4. 构建就绪节点,插入到就绪队列中
    }

解释2
在文件中有一个struct file包含一个void* private_data指针指向一个内核数据结构struct socket里继续指向一个结构叫做struct sock,sock里receive queue 和 send queue
在这里插入图片描述
所以在OS当中,你用epoll 的时候 他就会把回调函数注册到底层,然后底层数据一旦就绪,他就会自动回调执行回调函数里的方法1,2,3,4
所以最终一旦底层有数据就绪,从硬件中断,再到交给OS,再到数据接收到接受队列,再到他已经就绪,已经给我们全部放到就绪队列中,最后呢,哪些fd就绪,什么事件就绪已经在就绪队列里了。
未来对于用户他只需要从就绪队列中拿已经就绪的节点即可!!

整个上面一整套机制是由OS自动完成的。

收到了报文向上交付,其实就是解包,然后把sk_buff 连入到底层的sock结构体的receive queue中,数据就到了,然后再查红黑树确定是哪个fd 的,因为结构体对象你可以指向我 , 我可以指向你,底层指向上层,上层指向底层(receive queue指向struct file吧?),当我底层向上交付时,反向查指针最终就能找到文件对象,那么你的fd我也能确认,然后同时对应数据有没有就绪,有没有在红黑树里添加,有没有关心,我都知道
最后呢直接给你构建一个就绪节点放到就绪队列里。
在这里插入图片描述

我们把这三套机制就叫做epoll 模型

所以epoll_create 创建epoll模型就是:
创建红黑树(开始是空的)
创建就绪队列(开始是空的)
注册底层回调机制

epoll模型最终也是一个地址,这套结构是被统一管理起来的。

一个进程可不可以创建多次epoll模型呢?
如果一个进程创建很多epoll模型
OS就存在很多epoll模型
所以OS要不要把所有epoll模型管理起来呢?
怎么管理呢?
先描述在组织
所以整个epoll模型我画了三个结构,但其实三种结构在内核会被整合成一块,队列不是有头指针吗,红黑树不是有根节点吗,把两个指针放在一起,两个结构不就放在一起了。
所以内核会存在eventpoll结构
说白了它就是一个内核数据结构
在这里插入图片描述
最终epoll模型怎么被进程找到呢,此时我们再创建一个struct file 把他也当做文件,因为Linux中一切皆文件,把epoll模型也当文件,struct file当中也有指针指向epoll模型,所以未来我们把这个file对象添加到进程的文件描述符表里,只要找到fd那就找到struct file,再由struct file 找到epoll模型就可以访问所有东西了。

在这里插入图片描述
所以为什么epoll返回值也是个fd,因为最终epoll模型也统一被接入了struct file。

所以epoll和select和poll 完全不一样
差别大了
人家是为了epoll专门设计了三种机制
select 和 poll 无非就是遍历当前进程文件描述符表罢了。


三个接口各自的作用

int epoll_create(int size)
在这里插入图片描述

在OS内部创建struct file其中的指针指向整个epoll对象
然后对应的fd 就能挂接到进程的fd表,返回给用户,找到file,找到epoll模型。

epoll_ctl
在这里插入图片描述

所以为什么要把epoll_create 的返回值传递给epoll_ctl呢?
进程调epoll_ctl 根据fd找到struct file 找到红黑树,然后op 增加 修改 删除 ,干啥呢
在这里插入图片描述
修改红黑树,所以fd 和 事件其实包括增加删除和修改,其实ctl都是在修改这颗红黑树
所以epoll_ctl 匹配的就是对红黑树的增加删除和修改操作
没有查找,不需要查找,查是人家回调机制来做的,上层不需要查。
所以他只给我们暴露增加删除和修改。

epoll_wait
epoll_wait在干啥呢? 获取就绪队列
也是拿着自己的epfd找到文件描述符,找到
文件描述符表,找到struct file,找到epoll模型,找到之后 events maxevents是输入输出型参数
在这里插入图片描述
他会把就绪队列中所有就绪的节点一个一个的依次放到对应的struct epoll_event* events数组里,如果没有事件就绪就是队列为空,所以epoll_wait是在关联就绪队列。

最后

epoll 的优势:

实际上红黑树每个节点注册的时候,这个回调可以和每个节点对应的,他就不需要查,更高效

所以发现,从底层数据交到tcp的接受队列里,就是收到数据,到数据就绪通知整个过程是OS一路回调上来的。所以OS再也不用去轮询检测了。

获取就绪的时间复杂度是O(n)

  1. 上层用户通过epoll_wait检测是否有事件就绪O(1),因为检测就绪只要看队列是否为空就可以了。
    获取就绪事件是O(n),因为他要把就绪队列里的节点一个一个拷贝到用户层

  2. fd , event 没有上限 ,因为所有fd 和 事件 统一是在红黑树管理的,这颗红黑树多大你OS定,OS说内存就那么大那你找内存吧,和我epoll没关系。

  3. 你是如何看待这颗红黑树呢?
    这颗红黑树他不就是我们在写select和poll的时候对应的辅助数组吗
    在这里插入图片描述
    所以为什么说epoll是单独设计过的,因为用户再也不需要维护用户级的数据结构来管理所有的fd 及其要关心的事件了。
    OS说你还是别做了,我来帮你做吧
    我给你提供epoll_ctl 来对红黑树进行增删改,你以前数组不也是增删改吗,你不用做了你那个遍历贼低效,用我的把。

  4. epoll具有select和poll所有优势,他也解决了select和poll的所有缺点,更重要的是,epoll在获取就绪队列,epoll_wait 参数里的数组events可以依次方式一个一个的事件,所以events里面
    epoll_wait 返回值n ,表示有几个fd就绪了,就绪事件是连续的!! 有返回值个!!
    意味着从此往后上层用户处理所有已经就绪的事件不像我们之前还要检测过滤一下哪些fd是非法的,哪些是没有就绪的不用考虑了,你就拿着返回值,根据返回值个不断遍历对应的数组,你遍历几个他就给你处理几个。
    我们epoll 在上层就可以让用户的所有处理工作没有任何一个浪费的工作。


这里还有细节 还有问题,边写代码,边填充

快速写代码 — echo server

CMake

只要你懂makefile 了 CMake也会
cmake 比 make 简单的多
cmake 需要在当前目录新建txt
在这里插入图片描述
cmake 该怎么用呢?
文心一言
cmake使用样例
cmake是自动生成makefile工具
cmake_minimum_required(VERSION 3.10)
表示需要使用cmake的版本
cmake_minimum_required 代表关键字 或者函数

project( EpollServer ) 项目名称

add_executable( epoll_server Main,cc ) 表示用源文件形成可执行程序
如果你有多个源文件往后面直接跟

在这里插入图片描述
就这三行

接下来命令行 cmake .
在这里插入图片描述
打开makefile 是由cmke帮我们形成的
在这里插入图片描述
直接make 一下,运行服务器可执行文件
在这里插入图片描述
makefile 的取代方案 你只需要用cmake来生成就可以

一般cmake文件只有三四行可能就能自动形成makefile了

下来一个小时 ,把文心一言打出来的东西能看懂就会用了。


在这里插入图片描述
创建Epoller模型 – Epoller.hpp
在这里插入图片描述
epoll模型创建 和 epoll wait封装

nocopy 禁掉拷贝构造和赋值重载 ,让Epoller 继承他,也不让epoll模型拷贝

接下来就是服务器 for 循环 的逻辑
应不应该直接accept呢?
答案是不应该了,epoll只负责等待。

在这里插入图片描述

你让epoll等啥呢吗
最开始 的时候只有listen套接字
首先要将listensock添加到epoll中 本质是 将listensock和他关心的事件,添加到内核epoll模型中的红黑树中
你要是不等listen套接字那么fd就不可能增多

第一件事其实是将listen套接字添加的epoll模型中,然后让epoll去等
在这里插入图片描述
epoll类 是把epoll模型封装了。
epoll_ctl 的结构体 struct epoll_event ev 中为什么要设置ev.data.fd = sock呢?你不是已经在epoll_ctl 传入了第三个形参就是sock吗?

服务器层调用 , 将listen套接字添加到epoll中
在这里插入图片描述
epoll接下来会自动的帮我们关心listen套接字
在这里插入图片描述
此时我们只关心一个listen套接字,所以就绪队列拿上来数组里的就是0下标

在这里插入图片描述
事件就绪了不处理他会一直通知我

所以我们现在要HandlerEvent();
epoll把已经就绪的文件描述符和事件连续的放到了struct epoll_event revs[num]数组里
有多少个就绪了呢? 有epoll wait 返回值 n个就绪了。

细节一
你定义的struct epoll_event revs[num] num=64
在这里插入图片描述
在这里插入图片描述

捞已经就绪的事件时,num也就是最多捞64个

有没有可能底层的事件特别多,就绪队列里有无数个已经就绪的节点,不止64个呢,那我捞的时候就不够啊。
如果底层捞数据不够的时候,捞满了直接返回,还剩的下一批再捞。
这个缓冲区定多少,自己定无所谓,因为一旦有事件放到队列里,队列中就绪节点如果非常多没关系,他一次调epoll wait把底层就绪事件捞上来,没捞完还在队列里就把已经捞上来的处理完,处理完再捞下一批。

所以一个fd上是否有读事件就绪的本质,实际上就是看对应fd及其所关心的事件有没有放到就绪队列当中。

所以底层有数据,上层太忙没有取,我们就得通知对方让他的上层把数据尽快取走,所以什么叫做通知上层让上层把数据尽快取走呢?
本质上是哪一个fd有事件就绪了,最后一定要尽快把fd和就绪事件链入到就绪队列里,这样才可能被上层尽快取走,进而读取,所以在epoll中尽快让底层把数据取到了,比如我给对方通告的接受缓冲区为0,你不能再发了,对方说行吧,对方等了等对方觉得等不了了给我发送PSH,说你赶紧我已经受不了了,所以此时我们可以在底层强制调用回调让某一个fd上面的事件就绪,上层在读就能读走了,所以psh这样标志位的本质是让底层的读事件就绪。

所以psh他没这个能耐,让应用层必须读,psh在OS层面仅仅是让OS告诉上层说数据准备好了,在epoll对应的就是读 事件就绪,所以上层才能读走。

最后的结论呢就是不要担心缓冲区不够,不够下次再捞嘛。

下面继续HandlerEvent( revs , n );
所有就绪的都放到revs数组里,一共有n个
那就循环n次,拿到对应的事件及其fd

细节二
在这里插入图片描述

struct epoll_event 这个结构体设置关心fd和事件 还有 从就绪队列捞出来哪个fd及其事件就绪了,这个结构体,设置和输出都用了。

在这里插入图片描述
关于为什么一开始设置的时候要设置事件的sock用户级变量,目前,方便我们后期得知,是哪一个fd就绪了!
在这里插入图片描述
拿到对应的事件及其fd,接下来你的事件是什么事件呢?
在这里插入图片描述
那我们就查呗,有读事件就绪,有写事件就绪
在这里插入图片描述

当前读事件就绪因为我们只有一个文件描述符所以刚开始大概率就是listen套接字就绪了

而一旦读事件就绪 ,有两种情况,和select和poll一样
如果fd == listensock 说明获取了一个新链接
else
说明是其他fd上面的普通读取事件继续。

获取连接那就直接accept就可以了,我们accept不会被阻塞,因为listensock已经事件继续了。
在这里插入图片描述
获取成功我们能直接读取吗?
不能直接read
因为连接连上了并不代表我一定给你发了数据
这个连接上到底有没有数据到来你说了不算,只有epoll最清楚
我们就把sock新获取连接 添加到 epoll中,下一次epoll就会帮你关心对应的事件了,随着服务器获取的连接越来越多,最后统一由epoll来帮我们关心对应的事件了。
在这里插入图片描述
此时就将要关心的fd写入到了内核中,OS我又拿到了一个fd赶紧把它插入底层帮我去关心吧。
继续把连接管理拎出来到Accepter函数里
连接管理器
在这里插入图片描述
正常读写的fd就绪那就Recver 事件处理器

在这里插入图片描述
今天的HandlerEvent其实专业说话就应该叫Dispatcher 事件派发

在这里插入图片描述

事件处理器中 读的时候发现对方把链接关闭了, 我read返回0 了,那我epoll也不玩了
细节三
我们要在epoll当中把对应fd及其事件关心移除掉。
在这里插入图片描述

EPOLL_CTL_DEL 并且把事件event设为0,无所谓了
封装内部,要删除关心的fd和事件,事件是nullptr
在这里插入图片描述
把这个fd在epoll中被移走了
然后close(fd)
一般是先close 呢 还是 先 移除epoll 呢?
一般从epoll移除fd时,一定要保证fd是一个合法的fd,如果先关再移除,epoll就会报错,因为epoll识别fd是非法的。
所以先移除在关闭
在这里插入图片描述
我们读到了数据之后,要写回给客户端
但是写在多路转接要特殊处理
在这里插入图片描述

整个Recever 目前仅仅是为了测试
在这里插入图片描述
最后服务器就以Epoll的方式进行工作了。

Recver 你怎么知道你读到一个完整的请求呢?
没读到完整请求,那把buffer怎么办?
一旦事件就绪一次,Recver调用一次,如果他在底层把数据读上来了,可是没读到一个完整的请求,难道让recver直接结束吗,他没读到完整报文,上层就没法处理,那么buffer ,recver之后就让他直接释放吗,下次再读,下次再读的时候对不起上次的报文一半又不见了

还有很多没处理。

epoll的工作模式

LT:水平触发

ET: 边缘触发
在这里插入图片描述

Epoll默认模式:LT模式。
从哪可以看出来呢
select 和poll 处于什么模式呢?
select 和poll Epoll 一旦有新链接到来,上层不取走,底层就会一直通知你让你取走的模式,就叫做LT
就像示波器 的高电平 , 一直有效

ET指的是什么意思呢?
指的是数据或者连接,从无到有,从有到多,主要是变化的时候,才会通知我们一次

张三快递员每次有数据就一直给A同学打电话
这种叫做LT
李四快递员每次有数据变化的时候通知一次,A同学每次如果取走一部分,还剩一些数据没取走那也不再通知了,谁让A同学没全取走呢,要是不全取走而且没有新增快递的话可能数据就丢了,因为李四只通知你一次所以每次下楼都得把本次数据全取完,才能确保数据不丢失,这种叫做ET
张三每次有数据会问A同学你下来取快递吧,你不下来我就走了。
你就想那就去取吧,因为这货只通知我一次,可能你把快递取完了也可能没取完,但因为李四只通知你一次所以每次下楼都得把本次数据全取完,才能确保数据不丢失

你更喜欢谁的工作模式呢?同学们可能喜欢张三
你认为谁的工作效率比较高呢?
ET
为什么?
因为一个快递员 一个小时内能打出去电话的总数是有上限的。
张三打了50个电话,其中40个都是给你打的。
李四就不一样了,他可能一小时通知了50个人。
ET的通知效率更高。
ET的特征会让IO的效率也会变高,又该如何理解呢?
对于水平触发来讲呢,他一旦数据就绪了,你上层读一次就不会卡主,你读一次没读完下次还会通知你。
ET模式只有数据变化的时候才会通知我们一次,正是因为他有这种特点,倒逼程序员,每次通知都必须把本轮数据全部取走。
我怎么知道我把本轮的底层的数据取走了呢?
缓冲区有多少个报文,有多少个字节流,对我们来讲是黑盒的,我们不知道,我们站在上帝视角感觉到缓冲区有多少字节可以去读。
但其实一般在用缓冲区时里面有没有数据压根就不清楚。
我为了让同学们理解,问题
你爸对你特别好,每个月转的钱都会上交。
你妈每月会给你爸零花钱500
月中你并不清楚你爸还有多少钱,你怎么保证
你跟你爸要钱的时候把你爸本次手上的零花钱要完呢?
你爸手里的钱就相当于黑盒。
此时你找你爸要钱,你爸就给你100快
月中你也不清楚他还剩多少钱。
你怎么保证要完呢?
你爸就给你100快,第二天你要不要呢?
还要
第二天要多少你爸给多少
第三天你还要不要
还要
第四天继续要,你爸只给你43.5块钱
这个男人已经倾尽所有了。
我们一直读一直读,实际读到的数据如果比期望要的字节数要小,说明把缓冲区读完了
你即便要到43.5你还可以继续要
你再要你爸就会告诉你他没钱了。
所以边缘出发只会通知你一次,所以就倒逼着我们程序每次把数据全部取走,那你怎么保证你把数据全部取走呢?
我们要循环读取,一直读取出错,出错哪有那么容易。

当然LT就读一次,我不用循环读,下次如果底层还有数据他还会通知我,通知我我再读。

问题
我们今天可不是跟你爸要钱,今天是通过recve或者 read从网络中从缓冲区读数据,如果对应的向一个fd循环式的去读,当缓冲区已经没有数据时,我们再读我们的read会被怎么样?
我们的fd默认是阻塞的。
如果一直读到最后,导致无法继续读取了,read默认读取底层没数据,read就会阻塞,我不想让它阻塞,因为阻塞去读就把当前服务器就挂起了,万一他给我发了一个数据从此往后再也不发了,那我循环读最后我的服务不就挂起了吗,挂起了就响应不了了。
所以必须在ET模式下,所有的fd必须是non_block的 非阻塞的
这样的话呢,我们读到一旦底层没数据,非阻塞检测到没数据他会出错返回。
我们讲过设置一个fd非阻塞是出错返回,错误码EWOULDBLOCK
证明我们读完了。

所以一般在ET模式中,为什么需要将特定的fd要设置成非阻塞的?
理解链就是上面的。

所以边缘触发必须是每次都要把本轮数据全取走
这是一道面试题,只要上档次的公司问epoll 的话,必定会问此问题
思路要精准,而且要给人准确表达

ET和LT谁的模式更高效呢?
刚说是ET,因为ET通知效率更高
不仅仅如此,ET的IO效率也要更高!
这又该如何理解呢?
select poll Epoll 他们本质就是底层的就绪事件派发机制,一旦有哪些就绪了,就根据事件类别让他去获取连接,让他去读去写
你为什么说ET的IO效率更高呢?
因为ET每次通知一次要求上层程序员把数据全部取走。
这意味着什么?
意味着TCP会向对方通告一个更大的窗口,从而从概率上让对方一次给我发送更多的数据!!!
因为ET要求把数据取走,不就把接受缓冲区取走吗,缓冲区取走了那他的接受缓冲区剩余空间大小就变大了,不像LT只取一部分,那么ET变大了,那么在ACK的时候给对方在进行确认的时候就能给对方通告更大的窗口,对方进行流量控制时就可以一次向我发送更多数据。

( 为什么说每次通知,要把本轮数据全部取走,要把接受缓冲区的数据本轮全部取走,因为如果这次通知你fd上有读事件就绪,你只从接受缓冲区拿走了一半,也就是read了一半到buffer,然后就绪队列里他只放一次节点,你说你接受缓冲区还有数据但是不会再往就绪队列里放新节点了,也就不会再通知你了,如果后续数据不再发生变化,那这个请求数据还卡在缓冲区只有当对方继续发我才能继续处理吗?)

如果服务端写的代码是阻塞式的read, 并且一次只 read 1k 数据的话(read不能保证一次就把所有的数据都读出来,参考 man 手册的说明, 可能被信号打断), 剩下的9k数据就会待在缓冲区中
在这里插入图片描述
所以要求ET必须把所有对应的数据全部读完,循环读。

那么ET VS LT
ET的效率真的比LT要高吗??
不一定吧
你刚还说ET高,那是说的普遍的。

我就问你一句话,LT可不可以将所有的fd设置成为non_block,然后循环读取呢??
通知多次,那就通知第一次的时候,就全部取走,不就和ET一样了吗?
因为ET做的所有的工作我LT我也能做。
一旦就绪了他是会通知我多次,那我不要多次通知,我就直接把数据全干走,最后达到的效果就是我也只会通知一次,我也给对方通告更大的窗口。

所以ET LT效率谁高 ? 不一定,具体看代码怎么写。

最后
在这里插入图片描述

什么叫做底层有数据了来告知上层,每次通知是什么意思呢?
说到底就是底层数据就绪了,底层如果有数据没取走,是一直会回调,一直会把节点放到就绪队列里,还是只向就绪队列放一次?
如果你是ET模式,我只会向就绪队列中放一个新节点。对应特定的一个fd,特定的事件,我们只放一次,这不就是ET吗。
如果你是LT只要底层有数据,每一次你epoll我都把对应的特定的fd 及其 事件都给你放到队列里,这不就是每次通知吗?
所以所谓的通知本质上是向就绪队列里添加一次还是次次都添加,这就是LT和ET的区别。

结合代码中的问题,结合具体业务来谈谈
在这里插入图片描述

在读数据的时候呢,数据来了我就读,你只读了一次,因为我用的LT,没毛病
你只读了一次,在单纯字符串通信中你确实把数据发过来了,可你能保证读上来的信息一定是个完整的报文吗?
整个协议栈它读过来这么大的报文,你怎么保证你的buffer里一定就把一个完整的报文读完了呢?
这种情况下你的读可能就没读完。
没读完那就下一次呗,可是这个事件派发器每一个fd就绪了它都会调用Recver,而且buffer还是个临时空间,没读完下次再读,可是读每次都读buffer 的最开始,而且buffer是一个函数内临时空间,每次调用buffer被释放,如果不释放他就会被覆盖,而且对应的多个fd就绪大家都往同一个buffer里读,此时你这毫无疑问是无法满足上层的业务处理的。

所以该怎么办呢?

我有这么多文件描述符,每一个fd 就绪了它都会去读,都会向buffer 里去写,可是当前的buffer并不能保证读完一个完整报文。
之前网络版本计算器字符串还分析了,提取了报头没有完整报头还要下次读呢
所以今天代码中读取处理就是不合适的。

Reactor

故事从哪里开始呢?故事从TCPserver开始
其实我们要正确进行IO处理,一个服务器上在应用层一定会存在大量链接,每一个连接在应用层都叫fd,未来要给每一个fd,一个fd上的信息可能根本没读完,没读完就要把数据临时保存起来

所以我们自己写对应服务的时候,我们一定要保证每一个fd及其连接及其缓冲区,我们给每一个fd都要订一套输入输出缓冲区,还要有对应的结构体来描述它
这样呢我们对socket做封装之后呢然后我们再统一管理socket就可以了
这才是他的核心思路

class Connection
在这里插入图片描述

一个链接对应的套接字
每一个connection都要有对应的缓冲区
用string 充当缓冲区,是有问题的,不完善,因为string 是没有办法处理二进制流的,如果处理用vector更合适一些,但需要单独写缓冲区比较麻烦。

未来通过该套接字上读取数据时,我怎么知道我有没有把数据读完呢?有可能我根本就没读完,没读完不要紧,以前是buffer临时的公共的大家都用的,今天我们给每个套接字都设置string inbuffer缓冲区,当他有数据到来时,我们可以把所有读到的数据放到inbuffer里,未来就可以对inbuffer进行统一的管理,未来读到数据越来越多,后来读到一定程度可以让上层处理了,此时就把inbuffer交给上层,让上层分析处理。

除此之外,对应的connection内部尽量对IO方面不要做处理

定义一个函数对象类型func_t
在这里插入图片描述

用这个类型定义 读回调 和 写回调 和 异常回调

class TcpServer
我们需要再connection内部添加一个指向TcpServer的回指指针
在这里插入图片描述

接下来写class TcpServer
服务器里成员肯定要有Epoller,不然怎么等待多个fd呢?
在这里插入图片描述

未来会有多个fd 多个连接
我们采用哈希表_connections
构建一个从一个fd 到一个链接 这是服务器管理的所有连接

把所有的连接用哈希表管理起来
这不就是我们说的 先描述在组织吗
在这里插入图片描述
把listen套接字添加到epoll模型中
在这里插入图片描述

今天我想在做一件事情,我想让整个服务器以ET模式工作
怎么办呢?
就必须得是EPOLLET 设置选项

在这里插入图片描述

所以就需要EPOLLIN | EPOLLET
在这里插入图片描述

这就是为什么把EPOLLIN 单独拎出来的原因

在这里插入图片描述

此时传入的就是ET模式关心读事件

这还没完,你要关心ET模式你还必须得将fd设置为非阻塞。
fcntl

在这里插入图片描述

所以设置监听套接字fd为非阻塞
在这里插入图片描述

此时监听套接字fd 是非阻塞
同时也把监听套接字fd 添加到epoll中 又以ET模式关心读事件

作为一款服务器 while(!quit)循环

在这里插入图片描述
下面要做的就是事件派发喽
设置struct epoll_event revs[num]数组
num = 64
把数组设置为成员不用过多传参
在这里插入图片描述
我们最终想要的是如果listen套接字已经添加到epoll模型里,未来他一定会就绪,我就知道是哪个就绪了,我将来要读要写的话对应的listen的套接字他的读写在哪里呢?
在这里插入图片描述

所有的fd最终都要被哈希表进行管理,那这个问题就尴尬了,如果有底层套接字就绪了,你怎么读啊,读到哪里啊,你怎么写啊,写到哪里啊?

当我们创建了监听套接字,除了要把监听套接字添加到epoll模型中,我们添加对应的事件,除了要加到内核中,让内核帮我关注哪个fd及其事件,我们还要再做的是,如果按照这个写法的话未来读的时候一个fd 就是一个 裸的fd
在这里插入图片描述

我们未来是想通过一旦有fd 就绪,然后根据特定的fd找到它所匹配的connection, 然后通过connection中的回调来进行数据读取

在这里插入图片描述
首先就是还要将listenSock也建立一个connection对象,将listensock添加到connection里,同时将listensock 和connection 放入哈希表
一旦放入哈希表里,底层未来一旦就绪了,我就知道是哪个fd 就绪,根据哪个fd查表,查到你是哪个connection,然后根据connection执行读数据回调函数 recv_cb,读数据把数据读到inbuffer里
所以每一个fd 都会有一个属于自己的回调

听懂我的意思了吗
在这里插入图片描述

我们做服务器设计的时候,不仅仅说把一个fd添加到epoll里,你是不是在上层也要拿fd和一个connection对象关联起来,你之前epoll的时候一旦获取哪些fd就绪的时候,哪个fd我也知道,我们将来不像让他进行recver读取的时候
直接上来就从这里读
在这里插入图片描述
我们想让他把数据读到自己的缓冲区里,
在这里插入图片描述
所以我们需要给每一个fd 关联一个connection对象
在这里插入图片描述
这样的话未来只要把connection管理好,然后就能把所有连接管理好。

所以connection类需要再变化
在这里插入图片描述

把fd及其关心事件添加到epoll
再把fd 和 new 出来的connection对象 关联 然后 添加到哈希表里
设置connection的相关回调,并且把TcpServer对象的回指指针构造好
在这里插入图片描述
在Init中调用AddConnection设置监听套接字,回调暂时设为nullptr
在这里插入图片描述
AddConnection 构建了对象 设置了对应的回调
插入到哈希表 ,设置进epoll模型

再进行对应的EpollerWait() 的时候,他就会帮我们进行监测listen套接字了
一旦事件就绪了那我们就可以处理了。

接下来就是事件派发
我们把Start改为Loop循环
循环里只需要做事件派发就可以了,语义清楚
在这里插入图片描述

epoll wait的时候拿到就绪事件及其fd
我们统一把事件异常转换为读写问题,因为你对端关闭和出错那么读写一定出错,我们不用把异常处理扩散到很多地方,我们只要在读和 写处理异常就可以了。

你要保证安全的处理,你的connection对象必须得存在,所以我们查询哈希表看对象是否存在,存在,就用connection对象里面的读写回调函数
他会以这样的方式把所有的事件全处理

在这里插入图片描述
在这里插入图片描述
问题是你都有哪些事件呢?
目前只有一个listen套接字,他关心的是读

设置一个获取连接的回调Accepter

未来添加一个listen套接字时,他的读回调就不能是nullptr了,他的读回调就要绑定TcpServer内部的这个回调Accepter函数

我们AddConnection的时候就直接把对应的Accepter绑给了listen套接字他所对应的recv 回调
绑了之后呢,未来一旦有事件就绪了,读事件就绪做事件派发时就会执行他的recv回调,他就会执行Accepter,Accepter刚好是类内方法,他就可以直接进行在内部进行管理了
在这里插入图片描述
那Accepter获取连接该怎么获取呢?
那么你的连接套接字fd在connection对象中

一旦有对应的连接到来了,你怎么知道只有一个链接到来了呢?
我们链接一旦到来的时候,我们是ET模式下的,所以我们必须得while循环,不断的进行accept
我们这里使用原生系统调用accept获取连接

在这里插入图片描述
手册说明了accept 如果listen套接字设置了非阻塞,EWOULDBLOCK出错返回。
EINTR 读取时可能被信号中断,其实数据可读,只不过因为信号来了,导致阻塞被挂起了。
或者说 读的时候可能因为一些原因被信号唤醒此时也是出错返回,那我们就让他continue继续读accept
其他就是accept直接出错 就break了。
在这里插入图片描述
对应的Accepter就搞定了,他就不断的获取新链接,设置新链接的fd为非阻塞,添加对应的新的fd 到epoll模型中,让Epoll负责等。

从现在开始服务器一旦启动了,就会执行Loop帮我们派发,派发的时候已经把listen套接字AddConnection,绑定的是TcpServer的Accepter方法,所以它一旦事件派发了他执行他的recv 回调,就会反向的执行Accepter获取连接,并把该连接fd 用AddConnection哈希表管理和添加到epoll模型中。

随着不断的事件派发,还没完,后面新的连接到来了,进一步还要继续为获取新链接之后也要添加它的读,写,异常回调三个函数
在这里插入图片描述

对多路转接的代码进行一下设计和整合

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值