一、IO模型:这里所说的是网络IO,也就是套接字中的IO操作
1、应用背景:为了能实现自己去检测并处理IO阻塞
(事实上,IO阻塞无论是多核单核都无法真正解决,程序总是要等的,但是如何利用好阻塞的时间来实现并发或者如何尽可能的避免隐藏自己的IO阻塞来获得最大程度上的执行权限,这就是我们所要考虑的问题,之前讲过的协程gevent模块也是实现自动检测IO阻塞并且切换到别的任务,但是当前任务仍然是IO阻塞的状态,那为了实现真正的能够自己去检测IO阻塞和处理IO阻塞的程序,这就是IO模型)
2、IO模型的基础:很重要,IO模型就是基于这两个步骤实现的
(1)IO发生时的两个步骤:这里针对的是read操作,从内存中拿数据(accept,recv)
》等待数据准备:也就是等待socket对象需要的信息在内存中
》将数据从操作系统的内存中拷贝到进程的地址空间里:这一过程是由操作系统来操控的
补充》写操作,比如send,send发生时会把数据先拷贝到操作系统,此时程序的工作就做完了,什么时候发和发多长时间是由操作系统和网络延迟决定的,应用程序决定不了,所以执行send看上去是不阻塞的,因为数据量很小的化拷贝到内存中是不需要花多少时间的,但是数据量很大的话还是要花费一定时间
3、IO模型的分类
(1)阻塞IO:就是2个步骤全都阻塞,都在等
(2)非阻塞IO:
》工作过程:在等待数据准备阶段(1步骤)干别的任务,不会在原地等数据(解决阻塞IO的问题)
(当进程遇到recv或者accept时,会向操作系统请求数据,若这时没有数据在内存中,操作系统就会直接返回一个ERROR,不会让进程停在原地等待,进程就可以利用处理异常这个时间段去干别的事,过一段时间再向操作系统请求数据,若还没有继续返回error,若有则直接拷贝给进程(其实拷贝的时候进程仍是阻塞的))
(我在等客户端连接请求的时候若内存中没有数据就去等着接受别的客户端发过来的数据,而若这时有新的连接请求来的话无法及时处理因为在循环检测其它客户端是否发过来数据,这一过程结束后才会去检测是否有新的连接请求在)
》代码实现:服务端在listen下面写server.setblock(Fasle)就变成非阻塞IO
若没有数据来,就会抛出BlockingIOError异常,所以要加异常处理
服务端:
from socket import * import time s=socket(AF_INET,SOCK_STREAM) s.bind(('127.0.0.1',8083)) s.listen(5) s.setblocking(False) conn_l=[] while True: try: conn,addr=s.accept() #若没有数据,操作系统就会立刻返回一个error print('%s:%s' %(addr[0],addr[1])) conn_l.append(conn) except BlockingIOError: del_l=[] print('没有数据来') #基于建立好的连接收发消息 print(len(conn_l)) for conn in conn_l: try: data=conn.recv(1024) # if not data: #Linux系统的处理方法,Linux系统中若接受为空则会一直打印空 # del_l.append(conn) # continue conn.send(data.upper()) except BlockingIOError: pass except ConnectionResetError: #客户端断开连接时若不加异常处理就会报错的,也不能直接删列表中的元素,否则会影响for循环 conn.close() del_l.append(conn) #所以引入一个新列表专门装断开的连接 for conn in del_l: conn_l.remove(conn)
客户端:
from socket import * c=socket(AF_INET,SOCK_STREAM) c.connect(('127.0.0.1',8083)) while True: msg=input('>>: ').strip() if not msg:continue c.send(msg.encode('utf-8')) data=c.recv(1024) print(data.decode('utf-8'))
》特点:同一时间只能监测一种IO操作
》优点:最大程度避免了IO阻塞,效率是会提升
》缺点:占用cpu利用率过高,cpu始终处于计算的状态;任务响应延迟增大,无法及时处理存在内存中的数据
(3)IO多路复用:可以同时监测多个套接字(socket对象)的IO操作
》select模型:使用select模块循环一个一个遍历去检测是否有数据存在
》poll模型:也是一个一个循环遍历去检测数据是否存在,只是比select模型能监测的套接字个数多了点
》epoll模型(Windows不支持):使用回调函数,当内存中存在数据时自动通知应用程序,不需要再一个一个循环遍历
———》select模型:
obj = select.select(rlist,wlist,xlist,timeout=None)
——>参数:我们只需要传读操作列表就可以了,不用的参数用空列表传参,列表的元素是socket对象或者是连接
rlist是读操作列表(也就是只检测读操作的IO,比如recv,accept)
wlist是写操作列表(只检测写操作的IO,比如send,这个我们应该也用不着)
xlist用不着
timeout是检测时间,这个一般也用不着设置
——>工作过程:会循环遍历列表中的每个元素对应的IO操作是否有数据在内存中,如果有数据obj就有值了,obj输出的是元组类型,元组的元素是三个列表,依次对应参数中的3个列表,第一个就是rlist,里面存储着有数据在内存中的socket对象或是连接,接下来就直接进行accept和recv操作就可以了(这一步就是直接从内存中拷贝数据了(2步骤),花费的时间不会很多),也就是等待数据准备(1步骤)这个过程由select模块去完成
——>优点是善于处理多个连接请求,不适用于处理单个连接
服务端:
from socket import * import select import time s=socket(AF_INET,SOCK_STREAM) s.bind(('127.0.0.1',8085)) s.listen(5) s.setblocking(False) read_list=[s,] while True: print('检测的套接字数%s' %len(read_list)) r_l,_,_=select.select(read_list,[],[]) # print('准备好数据的套接字数%s' %len(r_l)) for obj in r_l: if obj == s: conn,addr=obj.accept() read_list.append(conn) print('客户端ip:%s,端口:%s' %(addr[0],addr[1])) else: try: data=obj.recv(1024) if not data: obj.close() read_list.remove(obj) continue obj.send(data.upper()) except ConnectionResetError: obj.close() read_list.remove(obj)
客户端:
from socket import * c=socket(AF_INET,SOCK_STREAM) c.connect(('127.0.0.1',8085)) while True: msg=input('>>: ').strip() if not msg:continue c.send(msg.encode('utf-8')) data=c.recv(1024) print(data.decode('utf-8'))
(4)异步IO:应用程序发起异步IO后就不管了,操作系统会来完成等待数据准备,数据来了就拷贝到应用程序的内存中整个过程,拷贝完了应用程序就可以直接拿来用了,因为整个过程是由操作系统来完成的,所以从应用程序层面很难单纯的完成异步IO操作
(5)信号驱动IO:不常用
注意:非阻塞IO 和 IO多路复用都是利用的等待数据准备这个阶段(1步骤)