事件驱动模型

五十岚2022年9月18日大约 31 分钟

事件驱动: 属于一种编程的范式:说白了一种编程的思想,一种编程的风格 传统的编程风格:(是控制流)比如代码块 A 实现了一个功能,代码块 B 实现了一个功能,之后通过一种顺序的执行,跑出一个结果。 线性的:代码块 A-->代码块 B-->代码块 C-->代码块 D-->...-->结束 但是事件驱动是和传统的编程风格完全不同的。而且在以后的公司里大多数情况下都是这种事件驱动的编程方式。

什么是事件驱动:
    所有的反应都是未知的,都是根据某个事件来触发的。假如网页上的操作,所有的操作对于服务器都是未知的。(类似Android的onclicklistenner,或是
    html的表单button之类的,只要点击就会触发某个函数)

    对于事件驱动型程序模型,它的流程大致如下:
        开始--->初始化--->等待

在UI编程中,常常要对鼠标点击进行相应,首先如何获得鼠标点击呢? 两种方式:

    1创建一个线程循环检测是否有鼠标点击:(* 一个线程、死循环)
        那么这个方式有以下几个缺点:
            1.CPU资源浪费,可能鼠标点击的频率非常小,但是扫描线程还是会一直循环检测,这会造成很多的CPU资源浪费;如果扫描鼠标点击的接口是阻塞的呢?
            2.如果是阻塞的,又会出现下面这样的问题,如果我们不但要扫描鼠标点击,还要扫描键盘是否按下,由于扫描鼠标时被阻塞了,那么
                可能永远不会去扫描键盘;
            3.如果一个循环需要扫描的设备非常多,这又会引来响应时间的问题;
            所以,该方式是非常不好的。

    2 就是事件驱动模型:
        目前大部分的UI编程都是事件驱动模型,如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件。事件驱动模型大体思路如下:
            1.有一个事件(消息)队列;
            2.鼠标按下时,往这个队列中增加一个点击事件(消息);
            3.有个循环,不断从队列取出事件(处理线程提取任务),根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;
            4.事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数;
        (简单来说就是每次点击时,把对应函数添加到队列,等待处理线程来提取调用)

事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。
另外两种常见的编程范式是(单线程)同步以及多线程编程。

下面来比较一下这三种模型:

Single-threaded:
    顺序执行(串行),遇到阻塞时其他任务只能等待阻塞完成,哪怕任务之间没有相互依赖的关系也要互相等待,此时就使程序降低了运行速度。

Multi-threaded:
    多个线程或进程(非python,如java等)同时执行(并行),单个子线程遇到阻塞时阻塞,其他的线程不会受到干扰,能够继续执行。与单线程
    相比更有效率,但程序员要写代码来保护共享资源,防止被多个线程同时访问。多线程程序更加难以推断,因为这类程序不得不通过线程同步机制如锁、
    可重入函数、线程局部存储或者其他机制来处理线程安全问题,如果实现不当就会导致出现微妙且令人痛不欲生的bug。

Asynchronous:(事件驱动编程模型)
    异步方式交错执行,但仍然在一个单独的线程控制中。看似无阻塞,当要IO阻塞时会被其它任务填充(类似协程原理,当遇到IO阻塞时执行其他函数,
    从而使效率提高)。处理IO时,会注册一个回调到事件循环中,事件循环轮询所有事件,当事件到来时分配给等待处理事件的回调函数。事件驱动型
    程序比多线程程序更容易推断出行为,因为程序员不需要关心线程安全问题。

1.要理解事件驱动和程序,就需要与非事件驱动的程序进行比较。实际上,现代的程序大多是事件驱动的,比如多线程的程序,肯定是事件驱动的。早期则存
    在许多非事件驱动的程序,这样的程序,在需要等待某个条件触发时,会不断地检查这个条件,直到条件满足,这是很浪费cpu时间的。而事件驱动的程
    序,则有机会释放cpu从而进入睡眠态(注意是有机会,当然程序也可自行决定不释放cpu),当事件触发时被操作系统唤醒,这样就能更加有效地使用cpu.

2.再说什么是事件驱动的程序。一个典型的事件驱动的程序,就是一个死循环,并以[一个线程]的形式存在,这个死循环包括两个部分,第一个部分
    是按照一定的条件接收并选择一个要处理的事件,第二个部分就是事件的处理过程。程序的执行过程就是选择事件和处理事件,而当没有任何事件触
    发时,程序会因查询事件队列失败而进入睡眠状态,从而释放cpu。

3.事件驱动的程序,必定会直接或者间接拥有一个事件队列,用于存储未能及时处理的事件。

4.事件驱动的程序的行为,完全受外部输入的事件控制,所以,事件驱动的系统中,存在大量这种程序,并以事件作为主要的通信方式。

5.事件驱动的程序,还有一个最大的好处,就是可以按照一定的顺序处理队列中的事件,而这个顺序则是由事件的触发顺序决定的,这一特性往往
    被用于保证某些过程的原子化。

6.目前windows,linux,nucleus,vxworks都是事件驱动的,只有一些单片机可能是非事件驱动的。

注意,事件驱动的监听事件是由操作系统调用的cpu来完成的

—————————————————————————————————############################################———————————————————————————————————————————————

IO 多路复用: 前面是用协程实现的 IO 阻塞自动切换,那么协程又是怎么实现的,其原理是怎么实现的。如何去实现事件驱动的情况下 IO 的自动阻塞的切换,这个学名 叫什么呢? => IO 多路复用 比如 socketserver,多个客户端连接,单线程下实现并发效果,就叫多路复用。

一、IO模型前戏准备:
    在进行解释之前,首先要说明几个概念:

    1.用户空间和内核空间:
        现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。
        操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。
        为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
        针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节
        (从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。

        注:用户的进程,不能访问操作系统的内核空间。用户空间和操作系统的内核空间,彼此是独立分开的。不然就全乱套了

    2.进程切换:
        为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换,这种切换
        是由操作系统来完成的。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。
        从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:
            保存处理机上下文,包括程序计数器和其他寄存器。
            更新PCB信息。
            把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
            选择另一个进程执行,并更新其PCB。
            更新内存管理的数据结构。
            恢复处理机上下文。

        注:就是说进程有一个保存上下文的管理机制,切换是非常消耗资源的(有一定的时间消耗)因此大量进程切换的话是一种极度消耗资源的表现。

    3.进程的阻塞:(accept、recv)
        正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执
        行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),
        才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。

        注:进程是申请CPU资源的,挂起时,如accept是进程自己进行等待具体的信息,然后把CPU交出去,阻塞住。(进程便释放了控制CPU的权限)

    4.文件描述符:(fd)
        文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
        文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个
        现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。
        但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。(windows没有)

        注:用户区的文件描述符表(索引值)通过指针来指向————>内核区的系统文件表,因此在进行socket数据传输时,都是通过fd来工作的

    5.缓存 I/O:(重要!!)
        缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据
        缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷
        贝到应用程序的地址空间。用户空间没法直接访问内核空间的,内核态到用户态的数据拷贝(结合注理解)

        思考:为什么数据一定要先到内核区,直接到用户内存不是更直接吗?
        缓存 I/O 的缺点:

        数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

        注:socket一收一发,其中接收信息时(对面发送数据会先到接收方的内核区,然后操作系统再把数据从内核去拷到用户区,因为用户程序没有访问内
            核区的权限)。为什么要这样转一步到内核区:信息是在物理层接收的(硬件:网卡),因此只有操作系统才能去调用这些物理设备(自己写的用
            户程序是无法调用这些硬件的),因此经过内核区这是必须的。(这一步是操作系统必须做的)

同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不同的人在不同的上下文下给出的答案是不同的。所以先限定一下本文的上下文。
本文讨论的背景是Linux环境下的network IO。(先理解什么是同步、异步、阻塞、非阻塞这独立的四个概念,不要组合)

IO Model 共分了五种IO:
    1.blocking IO:阻塞IO
    2.nonblocking IO:非阻塞IO
    3.IO multiplexing:IO多路复用
    4.signal driven IO:信号驱动
    5.asynchronous IO:异步IO

其中信号驱动在实际中并不常用,因此只提及4种IO模型

首先,要了解的就是————数据拷贝的过程(上文5.缓存中提到的)
     对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统
     内核(kernel)。当一个read操作发生时,它会经历两个阶段:
         1 等待数据准备 (Waiting for the data to be ready)(例如:accept等待连接,address等待接收client端的ip+端口,conn等待client
            端的socket,也就是fd文件描述符即socket对象)
         2 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)(即从内核态拷贝到用户态的过程)

    记住这两点很重要,因为这些IO Model的区别就是在两个阶段上各有不同的情况。


blocking IO (阻塞IO):
    在linux中,默认情况下所有的socket都是blocking

    当用户进程调用了recvfrom(用来让进程发系统调用)这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候
    数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被
    阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel(内核态)中拷贝到用户内存(用户态),然后kernel返回结果(return OK),
    用户进程才解除block的状态,重新运行起来。(recvfrom是accept、recv之类的控制发送系统调用)

    所以,blocking IO的特点就是在IO执行的两个阶段都被block了。(效率低)


non-blocking IO(非阻塞IO):
    linux下,可以通过设置socket使其变为non-blocking。(sk.setblocking(bool),设置Flase为非阻塞IO)

    当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它
    发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次
    发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。

    所以,用户进程其实是需要不断的主动询问kernel数据好了没有。

    注意:
        在网络IO时候,非阻塞IO也会进行recvform系统调用,检查数据是否准备好,与阻塞IO不一样,”非阻塞将大的整片时间的阻塞分成N多的小
        的阻塞, 所以进程不断地有机会 ‘被’ CPU光顾”。即每次recvform系统调用之间,cpu的权限还在进程手中,这段时间是可以做其他事情的,

        (简而言之,用户程序不是单线的往下执行,它不会放弃IO,而是轮询发送系统调用,一会走一段回来发个调用,直到数据准备好)
        生气了吗?————没有哦,生气了吗?————没有哦,生气了吗?————没有哦,生气了吗?————!!!

        也就是说非阻塞的recvform系统调用 调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程
        在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之
        为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。

        (然而这种一直问一直问的方式,资源是占的,而且消耗很大。但不考虑消耗的话,机制要比阻塞IO好得多)不仅如此,还有一个弊端:数据无法
            及时的拿到,有延迟。(询问的频率倒是可以自己设置)


IO multiplexing(IO多路复用):
    IO multiplexing这个词可能有点陌生,但是如果我说select,epoll(nginx的内部实现机制就是通过epoll来完成的),大概就都能明白了。有些地方
    也称这种IO方式为event driven IO(事件驱动IO)。我们都知道,select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基
    本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。(为什么nginx这么屌是因
    为它的实现机制epoll这么屌,也就是IO多路复用这么屌)

    IO多路复用的思想就是基于事件驱动模型来做的,下面用俩个阶段来解释这个过程

    阶段一:(select阶段)
        从上层(两步中的第一步)来说select就是函数调用,当我们调用select的时候,他也会发送一个system call系统调用,之后kernel等待数据。
        此时能够得出的结论是,这个过程也是阻塞的,但是不一样(因为之前的阻塞是通过recvform导致的,现在确实select)。一旦数据准备好响应
        时(数据到了内核区),会return一个readable(可读)告诉给进程,表示数据已经到了(前面两个都是没有返回的,也就是进程不知道内核什么
        时候准备好数据,当询问时一旦准备好了就直接让内核进行数据拷贝)。
        select————>system call————>kernel————>no_data————>wait_data————>data_ready————>return readable————>process
    阶段二:(recvfrom阶段)
        那么下一个动作谁来发生:进程。
        这时进程会发送一个system call系统调用,让内核(操作系统)把数据拷过来。内核拷贝完后return OK。(和阻塞IO不同的是,当数据准备好后,
        还需要再发送一个系统调用给内核,内核才开始复制数据)
        recvfrom————>system call————>kernel————>copy_data————>copy_complete————>return OK————>process

    select阶段:
        1此时也是阻塞状态CPU干不了别的,相当于卡住了(不做别的事情,就监听数据),kernel会“监视”所有select负责的socket。
        2.当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

    而第二步不用说,和之前相同,毋庸置疑也是阻塞状态。
    那么除了比阻塞IO额外多了一步返回之外,看似相同,似乎没什么好处。(其实真的是:在这个表现形式上,真的是没啥好处)但是它就解决了一个问题,
    一个别的方式都解决不了的问题,就是select————可以监听多个描述符!(waiting等待多个连接的描述符fd)然而这个时候就可以通过它来实现并发效果。

    (多说一句。所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server
    性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)

    在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block
    的。只不过process是被select这个函数block,而不是被socket IO给block。

    注意1:select函数返回结果中如果有文件可读了,那么进程就可以通过调用accept()或recv()来让kernel将位于内核中准备到的数据copy到用户区。

    注意2: select的优势在于可以处理多个连接,不适用于单个连接
        select和epoll机制相同(select在所有平台下都有,这也是它唯一的优点————跨平台,但select不好,现在基本都用epoll,下面讨论)


Asynchronous I/O(异步IO):(其实这个才是最屌哒)
    linux下的asynchronous IO其实用得很少

    步骤:
        首先,aio_read发一个system call(系统调用)然后立刻得到一个return值(有点像非阻塞IO)可以立马拿到一个结果。下面如果有数据就直接
        拿走了,没数据之后也有一个wait_data让操作系统在这里等数据,等完数据在copy过去(看似没区别啊)但区别在于,这个过程中,操作系统不再
        阻塞了,此时进程完全可以做其他事情。
        从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。

        最后它做其他事情的同时会收到一个通知,告诉它之前的数据已经准备好了,可以用它了。就相当于当内核
        全部做完了之后,告诉进程就好了,因此进程发了一遍系统调用便解决了。(即有数据立马返回,没数据把数据接收过来之后告诉进程直接用)
        aio_read————>kernel————>no_data————>return
        kernel————>wait_data————>data_ready————>copy_data————>copy_complete————>deliver signal/specified in aio_read————>process

        因此异步IO在整个过程中没有一点的阻塞,很多人认为epoll是一个异步IO,虽然它很强大,实现了很多机制,在select上进行完善,用处也很广,
        效果也很明显,但其实它不是异步IO,只是IO多路复用里的一种。问题就在于epoll其中有阻塞(epoll伪异步)

    用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返
    回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程
    发送一个signal,告诉它read操作完成了。

同步IO和异步IO的区别就在于:
    同步IO会被阻塞(或是说有阻塞就是同步IO),异步IO全程无阻塞

例如:排队等票就是阻塞IO。每十分钟看票到了没有是非阻塞IO。打电话买票(开了个挂,找了个黄牛)我什么时候上火车,你什么时候准备好票是异步IO。
    非阻塞IO看似也很自由,但是通知你票到了的时候,去前台领票这个过程也要阻塞你。IO多路复用则也要一直等着,什么时候叫到你就去取票,但是不
    同的窗口可以通知你,票可能不从一个窗口出。

异步虽然好,但是内部实现起来却很麻烦(天下就是这个道理,你用起来简单之后内部实现就要麻烦,用起来麻烦,内部实现肯定要简单一点)。因此也不是
所有问题都要用异步IO去解决。

—————————————————————————————————############################################——————————————————————————————————————————————— select 是怎么玩的?到底 select 为什么要叫 event driven IO(事件驱动 IO)?

select poll epoll IO 多路复用介绍:(监听多个文件描述符并实现并发)

1、select,poll,epoll都是IO多路复用的机制。
2、I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
3、但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,
    而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

select:
    select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当哪一个文件描述符有数据后,select()返回,
    该数组中就绪的文件描述符便会被内核修改标志位。监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,
    直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。
    当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。使得进程可以获得这些文件描述符从而进行后续的读写操作。
    select目前几乎在所有的平台上支持。
     
    select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译
    内核的方式提升这一限制。
     
    另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的
    延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。

    假设监听10个文件描述符,select会轮询监听(队列)。监听这10个描述符有没有数据更新,若有活动,就把内容返回回来(内核区搞到用户区)
    也就是睡眠——>监听无限循环,但有个问题就是他会依次询问每个数据好没好。当第9个文件描述符准备好后,还是要从第一个开始询问(低效)。


poll:
    它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。

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

    一般也不用它,相当于过渡阶段(就只解决了select监听数量的限制问题,还是低效)

epoll:(真正解决了select的轮询问题,可以理解为event poll)
    直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll。被公认为Linux2.6下性能最好的多路I/O就绪通知方法。windows不支持

    没有最大文件描述符数量的限制。
    比如100个连接,有两个活跃了,epoll会告诉用户这两个活跃了,直接取就ok了(epoll就像给了所有文件描述符一个灯,当某几个fd准备好了就
    亮灯,提示进程来取数据,就像回调一样),而select是循环一遍。

    (了解)epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没
    有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
    另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符
    进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速
    激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

    所以市面上见到的所谓的异步IO,比如nginx、Tornado、等,我们叫它异步IO,实际上是IO多路复用(也就是同步IO)。
    epoll强大在于copy的过程通常来说并不是很耗时,并且它优化select的轮询。它的内部实现就是基于回调函数callback来实现的。

IO多路复用的触发方式:
    在linux的IO多路复用中有水平触发,边缘触发两种模式,这两种模式的区别如下:

    水平触发:如果文件描述符已经就绪可以非阻塞的执行IO操作了,此时会触发通知.允许在任意时刻重复检测IO的状态,
    没有必要每次描述符就绪后尽可能多的执行IO。select,poll就属于水平触发。

    边缘触发:如果文件描述符自上次状态改变后有新的IO活动到来,此时会触发通知.在收到一个IO事件通知后要尽可能
    多的执行IO操作,因为如果在一次通知中没有执行完IO那么就需要等到下一次新的IO活动到来才能获取到就绪的描述
    符.信号驱动式IO就属于边缘触发.
    注:epoll即有水平触发也有边缘触发

    举例说明:
        一个管道收到了1kb的数据,epoll会立即返回,此时读了512字节数据,然后再次调用epoll。这时如果是水平触发的,epoll会立即返回,因为有
        数据准备好了.如果是边缘触发的不会立即返回,因为此时虽然有数据可读但是已经触发了一次通知,在这次通知到现在还没有新的数据到来,
        直到有新的数据到来epoll才会返回,此时老的数据和新的数据都可以读取到(当然是需要这次你尽可能的多读取).

    从电子的角度来解释一下:
        水平触发:也就是只有高电平(1)或低电平(0)时才触发通知(设定只要为[高/低]电平就触发),只要在这两种状态就能得到通知.上面提到的只要
    有数据可读(描述符就绪)那么水平触发的epoll就立即返回.(简而言之,假设高电平为有数据,则有数据未进行copy时就一直有高电平,只有
    当数据copy完毕后变成低电平时才不会发送触发通知)

        边缘触发:只有电平发生变化(高电平到低电平,或者低电平到高电平)的时候才触发通知.上面提到即使有数据
    可读,但是没有新的IO活动到来,epoll也不会立即返回.

    这也就解释了为什么在select下,没有.accept()会一直陷入死循环(就是由于select是水平触发,每次都立即返回,若不用accept进行读
    写(让内核区copy到用户区)就会每次循环都立即进行水平触发直到做出响应改变电平)

select模型代码设计即注意:(见代码,前面难以理解)
    1.导入select包:(select可以监听多个文件描述符并实现并发)
        select.select(r,w,e,t)分别是读、写列表,异常列表和超时时间,它会一直阻塞住并循环监听文件描述符有没有数据更新。
        t指的就是select在这里工作多少秒,假设5就是工作5秒钟,若期间没有数据更新程序就会向下继续执行。

    2.select监听:(只要有客户端变化(连接、发消息)select就能监听到,之后赋值给r)
        监听不仅是socket,凡是同一类的对象都可监听,因此程序出现一次性通信是由于select监听的是连接时活跃的socket,我再无连接便不会
        进行触发,因此连接后要把conn也一并放入监听列表中(select完成了监听动作,剩下的accpet完成copy动作)

    3.当多个client连接发消息后,和之前不同的是,server端会按顺序(按照监听队列的顺序)进行等待响应。(类似android按钮监听事件一样,
        当多个事件触发时,在事件队列会按顺序一个个执行)因此没用多线程、进程仅用select这种IO多路复用的方式也实现了并发的效果。

    4.为什么监听对应conn的不会混乱(比如要发给conn1却发给了conn2)。当只有一个事件触发时,每次select获取的返回对象只有一个,即r为1。
        而conn是最后一次连接的socket对象,用于加入监听列表。而每次的监听循环返回的则是触发事件的对象是r,遍历r用的却是obj,此时的obj
        就是响应的事件对象socket(因此不能把obj写为conn)。

    5.列表中的第一个对象是sk对象(即监听的是server端的socket),当开启多个客户端时,每次都会把对应的conn给append列表中。当多个客户端
        发送数据时,每次发送数据都会通知select。假设现在开了五个客户端发送数据,这五个客户端不是一块发送数据的,服务端执行也不是等到五
        个数据一块过来才走的。当第三个数据先过来时,server端会立即执行客户端三,当执行完客户端三是发现其他的四个数据都过来了(四个高电
        平),便会去遍历其余的四个数据(其实就是遍历整响应列表)遍历的顺序却是按照客户端连接时的顺序。(当32154时——会变为31245,第一个
        也就是3执行后遍历)。除非每次执行响应的动作和通知是一一对应的,不然有多个通知就会进行遍历。

    6.这里示例的select并没有体现出select自己轮询的过程(因为这是内部实现好的,看不见)。而epoll则与select结果的顺序从理论来讲应该不同,
        因为select是多个高电平响应,顺序读的是列表的顺序,而epoll可以边缘触发,那么顺序也可以再新添到一个队列当中(不会遍历导致乱序)。

    如何在某一个client端退出后,不影响server端和其它客户端正常交流(windows下的代码以写明)
        linux下:
        if not data:
            inputs.remove(obj)
            continue
    注:以后找错(逻辑出错)要在对应的哪一个逻辑的点上下手,比如这次是开了两个客户端返回之最后一个客户端,就要去返回的地方下手,因此就找
        到了server端的send方法,之后就找到前面应该是obj,而不是conn

IO 多路复用进一步封装 selector 库的应用: 1.让其自动根据操作系统判断最优选择:用 select、poll 还是 epoll -- win 上只有 select sel = selectors.DefaultSelector():

2.绑定、监听、设置非阻塞、注册!!,重点是注册
    sel.register(sock, selectors.EVENT_READ, accept) 让sock和accept(函数)做一个绑定

3.进入死循环,开启监听
    events = sel.select() 之前监听文件描述符通过r、w、e。现在则用sel这个核心对象进行select监听

4.流程:激活sk(第一次进行连接),调用callback(即accept函数)传参,再绑定conn和read。下次发送信息时调用callback(绑定的conn)
    回调调用read函数,通信。


用这种模块的方式去写,才真正有用,虽然麻烦了点。
上次编辑于: 2022/10/8 01:58:06