python 网络编程 IO模型(IO model)

参考:https://www.cnblogs.com/yuanchenqi/articles/5722574.html
https://blog.csdn.net/zhuangzi123456/article/details/84400108

一.事件驱动模型(一种编程范式)
协程:遇到IO切换
但何时切回去?如何确定IO操作结束?—>通过回调函数

传统的编程是如下线性模式的:

开始--->代码块A--->代码块B--->代码块C--->代码块D--->......--->结束

每一个代码块里是完成各种各样事情的代码,但编程者知道代码块A,B,C,D…的执行顺序;唯一能改变这个流程的是数据,输入不同的数据,根据条件语句判断,流程或许就改为A—>C—>E…—>结束;每次程序运行顺序或许都不同,但流程是由输入数据和程序决定的;如果知道这个程序当前的运行状态(包括输入数据和程序本身),那就知道接下来甚至一直到结束的运行流程

对于事件驱动型程序模型,流程大致如下:

开始--->初始化--->等待

与上面传统编程模式不同,事件驱动程序在启动之后,就等待被事件触发;传统编程下也有“等待”的时候,比如在代码块D中定义了一个input(),需要用户输入数据;但这与下面的等待不同:传统编程的“等待”,作为程序编写者是知道或强制用户输入某个东西的,如果用户输入错误,还需要提醒他,并要求重新输入;事件驱动程序的等待则是完全不知道,也不强制用户输入或者干什么;只要某一事件发生,程序就会做出相应的“反应”;这些事件包括:输入信息,鼠标点击,敲击键盘上某个键,系统内部定时器触发

1.事件驱动模型介绍
通常写服务器处理模型的程序时,有以下几种模型:

每收到一个请求,创建一个新的进程,来处理该请求
每收到一个请求,创建一个新的线程,来处理该请求
每收到一个请求,放入一个事件列表,让主进程通过非阻塞IO方式来处理请求

第3种即协程/事件驱动的方式,是大多数网络服务器采用的方式

2.论事件驱动模型

#鼠标点击事件注册:
#1个HTML文件,用浏览器解析
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<p onclick="fun()">点我呀</p>   #发生onclick事件时执行fun()函数
<script type="text/javascript">
    function fun() {
          alert('约吗?')
    }
</script>
</body>
</html>

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

  1. 创建一个线程循环检测是否有鼠标点击
CPU资源浪费:可能鼠标点击的频率非常小,但扫描线程还是会一直循环检测
如果扫描鼠标点击的接口是阻塞的:如果该事件不但要扫描鼠标点击,还要扫描键盘是否按下,由于扫描鼠标时被堵塞了,可能永远不会扫描键盘 
如果一个循环要扫描的设备非常多,又会引来响应时间的问题
  1. 事件驱动模型
目前UI编程大多是事件驱动模型,如很多UI平台提供onClick()事件(鼠标按下事件)

大体思路如下:

  1. 有一个事件(消息)队列(先进先出)
  2. 鼠标按下时,往队列中增加一个点击事件(消息)
  3. 有个循环,不断从队列取出事件;根据不同的事件,调用不同的函数,如onClick()/onKeyDown()
  4. 事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数
    在这里插入图片描述
    事件驱动编程是一种编程范式,程序的执行流由外部事件决定,包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理
    另外两种常见的编程范式是(单线程)同步,多线程编程

下图展示了随着时间的推移,这三种模式下程序所做的工作
这个程序有3个任务需要完成,每个任务都在等待I/O操作时阻塞自身;阻塞在I/O操作上所花费的时间用灰色框标示
在这里插入图片描述
事件驱动注解:

1.要理解事件驱动和程序,就需要与非事件驱动的程序进行比较:
现代的程序大多是事件驱动的,比如多线程的程序,肯定是事件驱动的
早期则存在许多非事件驱动的程序,在需要等待条件触发时,会不断地检查这个条件,直到满足,浪费cpu时间
而事件驱动的程序则有机会释放cpu从而进入睡眠态(注意是有机会,程序也可自行决定不释放cpu)
当事件触发时被操作系统唤醒,更加有效地使用cpu
2.再说什么是事件驱动的程序:
一个典型的事件驱动的程序,就是一个死循环,并以一个线程的形式存在
该死循环包括两部分:第一个部分按一定的条件接收并选择一个要处理的事件
第二个部分就是事件的处理过程
程序的执行过程就是选择事件和处理事件
而当没有任何事件触发时,程序会因查询事件队列失败而进入睡眠状态,释放cpu
3.事件驱动的程序,必定会直接或间接拥有一个事件队列,用于存储未能及时处理的事件
4.事件驱动的程序的行为,完全受外部输入的事件控制
所以事件驱动的系统中存在大量这种程序,并以事件作为主要的通信方式。
5.事件驱动的程序可以按一定的顺序处理队列中的事件
而这个顺序由事件的触发顺序决定,这一特性常用于保证某些过程的原子化
6.目前windows,linux,nucleus,vxworks都是事件驱动的
只有一些单片机可能是非事件驱动的。

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

二.IO模型准备
1.用户空间和内核空间:
现在操作系统都是采用虚拟存储器
对32位操作系统而言,寻址空间(虚拟存储空间)为4G(2的32次方)
操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限
为了保证用户进程不能直接操作内核(kernel),保证内核安全,操作系统将虚拟空间划分为两部分:内核空间和用户空间
对linuxOS来说,最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)供内核使用,称内核空间;较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF)供各进程使用,称用户空间
实际上是CPU中寄存器的状态位不同(0为内核态,1为用户态),状态位不同时,依靠CPU指令集的不同进行操作上的限制(内存本身并没有进行分割)

2.进程切换:
为控制进程的执行,内核需能挂起在CPU上运行的进程;并恢复以前挂起的某进程的执行(即进程切换),这种切换由操作系统完成
因此任何进程都是在OS内核的支持下运行的,与内核紧密相关
从一进程的运行转到另一进程的运行经过以下变化:

保存处理机上下文,包括程序计数器和其他寄存器。
更新PCB信息
把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列
选择另一个进程执行,并更新其PCB
更新内存管理的数据结构
恢复处理机上下文

注:进程切换很耗资源

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

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

三.缓存IO:又称标准I/O
大多数文件系统的默认IO操作都是缓存IO

1.如何运行:

#在Linux的缓存IO机制中:
OS会将IO的数据缓存在文件系统的页缓存(page cache)中
也就是说:数据先被拷贝到OS内核的缓冲区,然后再拷贝到应用程序的地址空间
用户空间无法直接访问内核空间---内核态到用户态的数据拷贝
  • 为什么数据要先到内核区,直接到用户内存不是更直接吗?
    只有操作系统可以操作硬件

2.缺点:数据在传输过程中需在应用程序地址空间和内核进行多次拷贝,这些拷贝操作带来的CPU以及内存开销极大

3.五种IO Model

  • 同步(synchronous)IO和异步(asynchronous)IO,阻塞(blocking)IO和非阻塞(non-blocking)IO分别是什么,有什么区别?
  • 这个问题不同的人给出的答案可能不同,比如wiki认为asynchronous IO和non-blocking IO是一个东西
  • 这其实是因为不同人的知识背景不同,并且在讨论这个问题时上下文(context)也不同

为了更好的回答这个问题,先限定一下本文的上下文:Linux环境下的network IO

Stevens在文章中一共比较了五种IO Model:

blocking IO:阻塞IO                |
nonblocking IO:非阻塞IO      |synchronous:同步IO
IO multiplexing:IO多路复用   |
#signal driven IO:信号驱动IO
asynchronous IO:异步IO

具体介绍见后文
在这里插入图片描述
signal driven IO在实际中不常用,这里只提及剩下的四种IO Model

一个network IO(这里以read举例)会涉及两个系统对象:调用这个IO的process (or thread)和系统内核(kernel);当一个read操作发生时,会经历两个阶段:

  1. 等待数据准备(Waiting for the data to be ready)
  2. 将数据从内核拷贝到进程中(Copying the data from the kernel to the process)

这些IO Model的区别就是在这两个阶段上有不同情况

四.五种IO Model详解
1.blocking IO(阻塞IO):
在linux中,默认情况下所有的socket都是blocking

import socket

sk=socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen(5)

while True:
    conn,addr=sk.accept()   #发送系统调用,见下图
    while True:
        conn.send(b'`W!2d8h;o12e“21d@$Efw/e2‘')   #发送系统调用
        data=conn.recv(1024)   #发送系统调用

在这里插入图片描述

  • 当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据;对network IO来说,很多时候数据在开始还没有到达(比如,还没有收到一个完整的数据包),这时kernel就要等待足够的数据到来;而在用户进程这边,整个进程会被阻塞;当kernel等到数据准备好了,就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来

缺点:效率低下
特点:在IO执行的两个阶段都被block;只发生一次系统调用;同步

2.non-blocking IO(非阻塞IO)
linux下可以通过.seblocking设置socket使其变为non-blocking

import socket,time

sk=socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen(5)
sk.setblocking(False)

while True:
    try:
        conn,addr=sk.accept()   #进行主动轮询,见下图
        pass
        conn.close()
    except Exception as e:   #如果系统调用返回error,执行其中代码
        print(e)
        time.sleep(5)   #每次返回error,暂停5s
        
#返回error时打印:'[WinError 10035] 无法立即完成一个非阻止性套接字操作。'

在这里插入图片描述

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

特点:用户进程需要不断主动询问kernel

  • 在网络IO时,非阻塞IO也会进行recvform系统调用;与阻塞IO不同,非阻塞将整片时间的阻塞分成许多小的阻塞,所以进程不断有机会"被CPU光顾";即每次recvform系统调用之间,cpu的权限还在进程手中,这段时间可以做其他事情
  • 也就是说非阻塞的recvform系统调用后,进程没有被阻塞,内核马上返回给进程;如果数据还没准备好,返回一个error;进程在返回后,可以做别的事,然后再发起recvform系统调用
  • 重复上面的过程,循环往复进行recvform系统调用,这个过程通常被称之为轮询;轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理
  • 需要注意,拷贝数据的过程中,进程仍然是属于阻塞的状态

缺点:系统调用多;如果在做其他事时收到数据,数据无法及时处理

3.IO multiplexing(IO多路复用):也称event driven IO
利用select模块,该模块提供select/poll/epoll三个方法;Windows只支持select;Linux均支持;MacOS支持slect/poll

(1)基本介绍:

  • 好处:单个process就可以同时处理多个网络连接的IO
  • 基本原理:select()/epoll()函数会不断轮询负责的所有socket;当某个socket有数据到达,就通知用户进程
  • 在IO multiplexing Model中,对每个socket,一般都设成non-blocking;但整个用户的process是一直被block的,不过是被select给block,而不是被socket IO给block

(2)使用select(效率最低):进程指定内核监听哪些文件描述符(最多1024个)的哪些事件,当没有文件描述符事件发生时,进程被阻塞;当一/多个文件描述符事件发生时,进程被唤醒;为水平触发

原理:监听一些端口,如其中某一个有新任务则发送一个信号,epoll接收到信号后得知有新任务(但不知道是谁的任务),开始轮询每一个端口(可能有多个端口有任务)

参见:https://www.cnblogs.com/zhiyong-ITNote/p/7553694.html

fd_r_list,fd_w_list,fd_e_list=select.select(rlist, wlist, xlist,timeout)
  #参数说明:
	rlist:wait until ready for reading(需要监听可读的socket对象)
	wlist:wait until ready for writing(需要监听可写的socket对象)
	xlist:wait for an "exceptional condition"(需要监听异常的socket对象)
	timeout:最常监听秒数;默认为无限长
	  #如果超时,继续执行之后代码(不包括本进程之后的代码)
	fd_r_list:满足可读条件的socket对象的列表(有任务要处理的socket对象)
	  #对conn而言,有新数据在内核态(recv前)时为高电平,入本列表
	  #对sk而言,有新连接请求在内核态时(accept前)为高电平,入本列表;初始为高电平(本身是一个新连接)
	  #获得连接后Fd才被从fd_r_list中移除
	fd_w_list:满足可写条件的socket对象的列表
	fd_e_list:发生错误的socket对象的列表

在这里插入图片描述

import socket,select

sk=socket.socket()
sk.bind(('127.0.0.1',8050))
sk.listen(5)
inp=[sk,]

while True:
    r,w,e=select.select(inp,[],[],5)#监听是否有给sk的连接#最多监听5s
    for i in r:#r为监听对象中状态发生变化的对象构成的列表
    #每次对方发送数据时conn都发生变化(往socket中放入数据)
    #出现新的连接请求时sk发生变化
        #conn,add=i.accept()
        #应该有分支结构,此处省略
        #print(conn)   #注释掉这两行后,sk不被接入用户态,发生死循环print('hello')
        print('hello')
        #inp.append(conn)
    print('>>>')#超时则从此次处开始继续执行

在这里插入图片描述

  • 当用户进程调用了select,那么整个进程会被block;而同时kernel会"监视"所有select负责的socket;当任何一个socket中的数据准备好了,select就会返回;这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程
  • 这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些:这里使用了两个system call(select和recvfrom),而blocking IO只调用了一个system call(recvfrom);用select的优势在于可以在一个线程中同时处理多个connection
#server端并发聊天:
import socket,select

sk=socket.socket()
sk.bind(('127.0.0.1',8050))
sk.listen(5)
inputs=[sk,]

while True:
    r,w,e=select.select(inp,[],[],5)
    for obj in r:
        if obj == sk:
            conn,add=onj.accept()
            inputs.append(conn)
        else:
            data=obj.recv(1024).decode('utf-8')
            inp=input('回答%s号用户>>>'%inputs.index(obj))
            obj.sendall(inp.encode('utf-8'))
    print('>>>')
  • 所以,如果处理的连接数不多,使用select/epoll的web server不一定比使用multi-threading+blocking IO的web server性能好,可能延迟还更大;select/epoll的优势不是对于单个连接处理得更快,而在于能处理更多的连接
  • select函数返回结果中如果有文件可读了,进程就可以通过调用accept()/recv()来让kernel将位于内核中准备到的数据copy到用户区

(3)使用poll
在这里插入图片描述
(4)epoll(效率最高):要求提交请求的同时提交身份,只去有任务的端口获取数据;既可采用水平触发,也可采用边缘触发
在这里插入图片描述
市面上上见到的所谓的异步IO,如nginx/Tornado,就是利用epoll来实现的;我们叫它异步IO,实际上是IO多路复用

select与epoll:

# 首先我们来定义流的概念,一个流可以是文件,socket,pipe等等可以进行I/O操作的内核对象。
# 不管是文件,还是套接字,还是管道,我们都可以把他们看作流。
# 之后我们来讨论I/O的操作,通过read,我们可以从流中读入数据;通过write,我们可以往流写入数据。现在假
# 定一个情形,我们需要从流中读数据,但是流中还没有数据,(典型的例子为,客户端要从socket读如数据,但是
# 服务器还没有把数据传回来),这时候该怎么办?
# 阻塞。阻塞是个什么概念呢?比如某个时候你在等快递,但是你不知道快递什么时候过来,而且你没有别的事可以干
# (或者说接下来的事要等快递来了才能做);那么你可以去睡觉了,因为你知道快递把货送来时一定会给你打个电话
# (假定一定能叫醒你)。
# 非阻塞忙轮询。接着上面等快递的例子,如果用忙轮询的方法,那么你需要知道快递员的手机号,然后每分钟给他挂
# 个电话:“你到了没?”
# 很明显一般人不会用第二种做法,不仅显很无脑,浪费话费不说,还占用了快递员大量的时间。
# 大部分程序也不会用第二种做法,因为第一种方法经济而简单,经济是指消耗很少的CPU时间,如果线程睡眠了,
# 就掉出了系统的调度队列,暂时不会去瓜分CPU宝贵的时间片了。

# 为了了解阻塞是如何进行的,我们来讨论缓冲区,以及内核缓冲区,最终把I/O事件解释清楚。缓冲区的引入是为
# 了减少频繁I/O操作而引起频繁的系统调用(你知道它很慢的),当你操作一个流时,更多的是以缓冲区为单位进
# 行操作,这是相对于用户空间而言。对于内核来说,也需要缓冲区。
# 假设有一个管道,进程A为管道的写入方,B为管道的读出方。
# 假设一开始内核缓冲区是空的,B作为读出方,被阻塞着。然后首先A往管道写入,这时候内核缓冲区由空的状态变
# 到非空状态,内核就会产生一个事件告诉B该醒来了,这个事件姑且称之为“缓冲区非空”。
# 但是“缓冲区非空”事件通知B后,B却还没有读出数据;且内核许诺了不能把写入管道中的数据丢掉这个时候,A写
# 入的数据会滞留在内核缓冲区中,如果内核也缓冲区满了,B仍未开始读数据,最终内核缓冲区会被填满,这个时候
# 会产生一个I/O事件,告诉进程A,你该等等(阻塞)了,我们把这个事件定义为“缓冲区满”。
# 假设后来B终于开始读数据了,于是内核的缓冲区空了出来,这时候内核会告诉A,内核缓冲区有空位了,你可以从
# 长眠中醒来了,继续写数据了,我们把这个事件叫做“缓冲区非满”
# 也许事件Y1已经通知了A,但是A也没有数据写入了,而B继续读出数据,知道内核缓冲区空了。这个时候内核就告
# 诉B,你需要阻塞了!,我们把这个时间定为“缓冲区空”。
# 这四个情形涵盖了四个I/O事件,缓冲区满,缓冲区空,缓冲区非空,缓冲区非满(注都是说的内核缓冲区,且这四
# 个术语都是我生造的,仅为解释其原理而造)。这四个I/O事件是进行阻塞同步的根本。(如果不能理解“同步”是
# 什么概念,请学习操作系统的锁,信号量,条件变量等任务同步方面的相关知识)。

# 然后我们来说说阻塞I/O的缺点。但是阻塞I/O模式下,一个线程只能处理一个流的I/O事件。如果想要同时处理多
# 个流,要么多进程(fork),要么多线程(pthread_create),很不幸这两种方法效率都不高。
# 于是再来考虑非阻塞忙轮询的I/O方式,我们发现我们可以同时处理多个流了(把一个流从阻塞模式切换到非阻塞
# 模式再此不予讨论):
# while true {
# for i in stream[]; {
# if i has data
# read until unavailable
# }}
# 我们只要不停的把所有流从头到尾问一遍,又从头开始。这样就可以处理多个流了,但这样的做法显然不好,因为
# 如果所有的流都没有数据,那么只会白白浪费CPU。这里要补充一点,阻塞模式下,内核对于I/O事件的处理是阻
# 塞或者唤醒,而非阻塞模式下则把I/O事件交给其他对象(后文介绍的select以及epoll)处理甚至直接忽略。

# 为了避免CPU空转,可以引进了一个代理(一开始有一位叫做select的代理,后来又有一位叫做poll的代理,不
# 过两者的本质是一样的)。这个代理比较厉害,可以同时观察许多流的I/O事件,在空闲的时候,会把当前线程阻
# 塞掉,当有一个或多个流有I/O事件时,就从阻塞态中醒来,于是我们的程序就会轮询一遍所有的流(于是我们可
# 以把“忙”字去掉了)。代码长这样:
# while true {
# select(streams[])
# for i in streams[] {
# if i has data
# read until unavailable
# }}
# 于是,如果没有I/O事件产生,我们的程序就会阻塞在select处。但是依然有个问题,我们从select那里仅仅知
# 道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,
# 找出能读出数据,或者写入数据的流,对他们进行操作。
# 但是使用select,我们有O(n)的无差别轮询复杂度,同时处理的流越多,每一次无差别轮询时间就越长。再次
# 说了这么多,终于能好好解释epoll了
# epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知我
# 们。此时我们对这些流的操作都是有意义的。
# 在讨论epoll的实现细节之前,先把epoll的相关操作列出:
# epoll_create 创建一个epoll对象,一般epollfd = epoll_create()
# epoll_ctl (epoll_add/epoll_del的合体),往epoll对象中增加/删除某一个流的某一个事件
# 比如
# epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//有缓冲区内有数据时epoll_wait返回
# epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//缓冲区可写入时epoll_wait返回
# epoll_wait(epollfd,...)等待直到注册的事件发生
# (注:当对一个非阻塞流的读写发生缓冲区满或缓冲区空,write/read会返回-1,并设置errno=EAGAIN。
# 而epoll只关心缓冲区非满和缓冲区非空事件)。
# 一个epoll模式的代码大概的样子是:
# while true {
# active_stream[] = epoll_wait(epollfd)
# for i in active_stream[] {
# read or write till unavailable
# }}

# 举个例子:
#    select:
#          班里三十个同学在考试,谁先做完想交卷都要通过按钮来活动,他按按钮作为老师的我桌子上的灯就会变红.
#          一旦灯变红,我(select)我就可以知道有人交卷了,但是我并不知道谁交的,所以,我必须跟个傻子似的轮询
#          地去问:嘿,是你要交卷吗?然后我就可以以这种效率极低地方式找到要交卷的学生,然后把它的卷子收上来.

#    epoll:
#         这次再有人按按钮,我这不光灯会亮,上面还会显示要交卷学生的名字.这样我就可以直接去对应学生那收卷就
#         好了.当然,同时可以有多人交卷.

4.Asynchronous I/O(异步IO)—用户效率最高,但实现机制复杂

在linux下asynchronous IO用得很少

在这里插入图片描述

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

特点:全程无阻塞

5.触发方式:水平触发&边缘触发

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

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

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

# epoll既可以采用水平触发,也可以采用边缘触发.

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

# 下面我们还从电子的角度来解释一下:
 
#     水平触发:也就是只有高电平(1)或低电平(0)时才触发通知,只要在这两种状态就能得到通知.上面提到的只要
# 有数据可读(描述符就绪)那么水平触发的epoll就立即返回.
# 
#     边缘触发:只有电平发生变化(高电平到低电平,或者低电平到高电平)的时候才触发通知.上面提到即使有数据
# 可读,但是没有新的IO活动到来,epoll也不会立即返回.

在这里插入图片描述
6.IO Model比较
在non-blocking IO中,虽然进程大部分时间都不会被block,但仍要求进程主动check,且当数据准备完成后,也需要进程再次主动调用recvfrom拷贝数据到用户内存;而asynchronous IO就像是用户进程将整个IO操作交给了kernel来完成,做完后发信号通知,用户进程无需检查IO操作的状态,也无需主动拷贝数据
在这里插入图片描述
五.selectors模块:用于IO多路复用
1.该模块定义了1个BaseSelector抽象基类及其子类:SelectSelector/PollSelector/EpollSelector/DevpollSelector/KqueueSelector,分别使用对应的select方法

2.另外还有一个DefaultSelector类,是以上其中某个子类的别名,自动选择当前环境中最有效的Selector,平时用该类即可

3.模块定义了两个常量,用于描述event Mask(注册一个文件对象时,等待的events)

EVENT_READ:表示可读的,值其实是1
EVENT_WRITE:表示可写的,值其实是2

4.块定义了一个 SelectorKey类,一般用这个类的实例来描述一个已经注册的文件对象的状态,这个类的常用属性包括:

fileobj:表示已经注册的文件对象
fd:表示文件对象的描述符;是个整数;是文件对象的 fileno()方法的返回值
events:表示注册一个文件对象时,等待的events,即上面的event Mask
data:表示注册一个文件对象是绑定的data

5.抽象基类中的方法:

.register(fileobj, events, data):注册一个文件对象,返回1个SelectorKey类的实例
#将fileobj放入监听对象列表;将fileobj与data绑定
#fileobj:即可以是fd也可以是一个拥有fileno()方法的对象,如socket对象
#events:上面的event Mask常量
#data:fileobj有活动时调用的函数;默认为None
.unregister(fileobj):注销一个已注册过的文件对象,返回1个SelectorKey类的实例
#将fileobj移出监听对象列表
.modify(fileobj,events,data):修改1个注册过的文件对象,如监听可读->监听可写
#其实就是register()+unregister(),但更高效
#参数说明同register()
.select(timeout=None):选择满足监听event的对象,返回1(key,events)的元组
#开始监听
#key是一个SelectorKey类的实例,key.data为与fileobj绑定的函数
#events是event Mask(EVENT_READ/EVENT_WRITE/二者的组合)
.close():关闭selector
#最后一定要记得调用它,要确保所有的资源被释放;
.get_key(fileobj):返回注册文件对象的key(返回1个SelectorKey类的实例)

6.应用:FTP等

import selectors,socket

sel=selectors.DefaultSelector()
#创建1个DefaultSelectors对象sel,所有监听相关操作都围绕sel展开
#会根据操作系统自动选择1个最好的IO多路复用的方式(Windows下只有select)

def accept(sock,mask):
    conn,addr=sock.accept()#接收请求
    conn.setblocking(False)
    sel.register(conn,selectors.EVENT_READ,read)
      #将conn和read()绑定,开始监听conn(conn是某个用户的连接)

'''适用于Linux
def read(conn,mask):
    data=conn.recv(1024)
    if data:
        conn.send(data)
    else:#如果client断开了连接,则data为空
        sel.unregister(conn)#将conn移出监听对象列表
        conn.close()
'''

def read(conn,mask):
    try:
        data=conn.recv(1024)
        conn.send(data)
    except Exception as e:#在Windows下,如果client断开了连接,则发生异常
        sel.unregister(conn)#将conn移出监听对象列表
        conn.close()

sock=socket.socket()
sock.bind(('127.0.0.1',8080))
sock.listen(5)
sock.setblocking(False)

sel.register(sock,selectors.EVENT_READ,accept)
#将sock与accept()绑定(sock有活动就调用accept),开始监听sock

while True:
    events=sel.select()
    #events是1个list,包含所有有活动的socket对象的相关信息
    #events的元素是1个个tuple,每个tuple包括key和mask这2部分
    for key,mask in events:
      #key为SelectorKey对象;mask为event Mask,是1个掩码,值为0或1
        callback=key.data#callback为与监听对象绑定的函数,此处就是accept()
        callback(key.fileobj,mask)#执行callback
          #key.fileobj为有活动的socket对象
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值