一、 IO模型介绍
对于一个网络通信,IO涉及到两个阶段
1.操作系统等数据来
2.进程或线程等操作系统拷贝数据
记住这两点很重要,因为这些IO模型的区别就是在两个阶段上各有不同的情况。
二、阻塞IO(blocking IO)
例子:
1 from socket import * 2 s=socket(AF_INET,SOCK_STREAM) 3 s.bind((‘127.0.0.1‘,8080)) 4 s.listen(5) 5 print(‘starting..‘) 6 while True: 7 conn,addr=s.accept() # accept就是IO 8 print(addr) 9 while True: 10 try: 11 data=conn.recv(1024) #recv 也是IO 12 if not data : break 13 conn.send(data.upper()) 14 except Exception: 15 break 16 conn.close() 17 s.close()
socket 通信服务端
1 from socket import * 2 c=socket(AF_INET,SOCK_STREAM) 3 c.connect((‘127.0.0.1‘,8080)) 4 5 while True: 6 cmd=input(‘ss‘).strip() 7 if not cmd:continue 8 c.send(cmd.encode(‘utf-8‘)) 9 data=c.recv(1024) 10 print(data.decode(‘utf-8‘))
socket 通信客户端
所以,blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。
一个简单的解决方案:
#在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。
该方案的问题是:
#开启多进程或都线程的方式,在遇到要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而且线程与进程本身还是没有解决IO问题,只是换了一种方式
改进方案:
#很多程序员可能会考虑使用“线程池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。这种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如websphere、tomcat和各种数据库等。 这种方式确实是好了些,但是还没有解决IO 问题
对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,“线程池”或许可以缓解部分压力,但是不能解决所有问题。总之,多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题。
三 非阻塞IO(non-blocking IO)
1 from socket import * 2 import time 3 s=socket(AF_INET,SOCK_STREAM) 4 s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) 5 s.bind((‘127.0.0.1‘,8085)) 6 s.listen(5) 7 s.setblocking(False) #套接字里面的所有阻塞操作都会变成非阻塞 8 print(‘starting...‘) 9 l=[] 10 d_l=[] 11 while True: 12 try: 13 print(l) 14 conn,addr=s.accept() 15 l.append(conn) 16 except BlockingIOError: #捕捉 accept收不到数据的时候抛出的异常 17 for conn in l: 18 try: 19 data=conn.recv(1024) 20 conn.send(data.upper()) 21 except BlockingIOError: #捕捉 recv收不到数据的时候抛出的异常 22 pass 23 except ConnectionResetError: #捕捉 客户端突然断开链接的时候抛出的异常 24 d_l.append(conn) 25 for j in d_l: 26 j.close() 27 l.remove(j) 28 d_l=[]
服务端
1 from socket import * 2 c=socket(AF_INET,SOCK_STREAM) 3 c.connect((‘127.0.0.1‘,8085)) 4 5 while True: 6 cmd=input(‘ss‘).strip() 7 if not cmd:continue 8 c.send(cmd.encode(‘utf-8‘)) 9 data=c.recv(1024) 10 print(data.decode(‘utf-8‘))
客户端
但是非阻塞IO模型绝不被推荐。
缺点:
#1. 循环调用recv()将大幅度推高CPU占用率;容易出现卡机情况 #2. 任务完成的响应延迟增大了,因为每过一段时间才去轮询一次accept操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。
四 多路复用IO(IO multiplexing) select 模块
1 from socket import * 2 import time 3 import select #自动监听多个套接字 谁好了就可以来取了 4 server=socket(AF_INET,SOCK_STREAM) 5 server.bind((‘127.0.0.1‘,8085)) 6 server.listen(5) 7 print(‘starting...‘) 8 reads_l=[server,] 9 while True: 10 r_l,_,_=select.select(reads_l,[],[]) #select.select返回的是 读列表里已经准备好的套接字 11 print(r_l) 12 for obj in r_l: 13 if obj == server: 14 conn,addr=obj.accept() #obj=server 15 reads_l.append(conn) 16 else: 17 try: 18 data=obj.recv(1024) #obj=conn 19 obj.send(data.upper()) 20 except ConnectionResetError: # 捕捉 客户端突然断开链接 造成的异常 21 pass
服务端
1 from socket import * 2 c=socket(AF_INET,SOCK_STREAM) 3 c.connect((‘127.0.0.1‘,8085)) 4 5 while True: 6 cmd=input(‘ss‘).strip() 7 if not cmd:continue 8 c.send(cmd.encode(‘utf-8‘)) 9 data=c.recv(1024) 10 print(data.decode(‘utf-8‘))
客户端
select 从流程上分析:效率不如 阻塞IO 的效率高,但是阻塞不能实现并发,
如果select只检测一个套接字,效率肯定不如阻塞IO,但是它可以同时监测多个套接字,而阻塞IO 远远不行。
select 和 非阻塞IO 比较: 非阻塞IO 是自己捕捉信号,自己处理多个套接字的,并且非常占用CPU,而select 是自动捕捉的
select监听的套接字个数不是无限多的
五、selectors模块
select,poll,epoll
epoll模型可以解决套接字个数非常多的情况(因为它的内部检测哪个套接字好了的机制和select不同(select是遍历每个套接字,看有没有好了的,而epoll是 如果哪个套接字好了,就自动跑出来)),但是windows不支持epoll模型
这三种IO多路复用模型在不同的平台有着不同的支持,而epoll在windows下就不支持,好在我们有selectors模块,帮我们默认选择当前平台下最合适的
from socket import * import selectors #导入selectors后,会根据你的系统 自动选择当前系统下最合适的IO模型