网络编程之IO模型
1.1 IO模型介绍
IO发生时涉及的对象和步骤。对于一个网络IO,它会涉及到两个系统对象,一个是调用IO的进程或者线程,另一个就是系统内核。
如当一个read操作发生时,会先等待数据准备,然后将数据从内核拷贝到进程中去
阻塞IO(blocking IO)特点:在执行IO的两个阶段(等待数据和拷贝数据两个阶段)都被阻塞了
实际上几乎所有的IO借口(包括socket端口)都是阻塞型的。如在调用recv(1024)的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。
解决方案:在服务端使用多线程(或者多进程),目的是让每个连接都拥有独立的线程(或者进程)。但是一旦客户端同一时间的连接请求过多的话,则会严重占用系统资源,降低系统对外界的响应效率。
那么该如何解决呢?最好的办法就是使用‘线程池’或者‘连接池’。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。这两种办法可以很好地降低系统开销,适用于大型系统。
上述方案只是一定程度缓解了频繁调用IO接口带来的资源占用,‘池’终究是有上限的,当响应规模过大时,必须考虑调整池的大小。
用解决该问题。可以使用非阻塞接口来解决
1.2 非阻塞IO模型(nonblocking IO)
多线程,多进程,进程池,线程池都可以实现并发,但是仍然没有解决IO问题,先看一下IO非阻塞模型(linux下)
从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是用户就可以在本次到下次再发起read询问的时间间隔内做其他事情,或者直接再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存(这一阶段仍然是阻塞的),然后返回。
也就是说非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。
所以,在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。
相关参数
server.setblocking() # 默认是True server.setblocking(False) # False的话就成非阻塞了,这只是对于socket套接字来说的 # 所以,在非阻塞式IO中,用户进程其实是需要不断的主动询问内核数据准备好了没有。 wait data # 等数据,这个阶段是不阻塞的 copy data # 这个阶段还是要阻塞的
server端
import socket sk = socket.socket() sk.bind(('127.0.0.1',9000)) sk.setblocking(False) sk.listen() conn_lst = [] del_lst = [] while True: try: conn,addr = sk.accept() # 非阻塞的模型 print(conn,addr) conn_lst.append(conn) except BlockingIOError as e: for conn in conn_lst: #[conn1,conn2,conn3] try: msg= conn.recv(1024) # 非阻塞 if not msg: # 判定是否为空 conn.close() del_lst.append(conn) continue print(msg) msg = msg.decode('utf-8').upper() conn.send(msg.encode('utf-8')) except BlockingIOError:pass # 阻塞IO报错,直接pass for conn in del_lst: conn_lst.remove(conn) del_lst.clear()
client端
import time import socket sk = socket.socket() sk.connect(('127.0.0.1',9000)) print(sk) for i in range(20): sk.send(b'hello') print(sk.recv(1024)) time.sleep(1) sk.close()
非阻塞IO模型优点:
能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在“”同时“”执行)。
非阻塞IO模型缺点:
1. 循环调用recv()将大幅度推高CPU占用率
2. 任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。
1.3 多路复用(IO multiplexing)
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
操作系统中的IO多路复用的机制select:
windows操作系统提供给你的一种监听接收数据IO的一个代理
select模块:
python使用操作系统select机制的功能
server端:
import socket import select sk = socket.socket() sk.bind(('127.0.0.1',9000)) sk.setblocking(False) sk.listen() l = [sk] # sk.accept while True: r,w,x = select.select(l,[],[]) # 阻塞 for obj in r: if obj is sk: conn,addr = obj.accept() l.append(conn) # conn.recv else: msg = obj.recv(1024) if not msg: obj.close() l.remove(obj) continue print(msg) obj.send(b'bye')
client端:
import socket sk = socket.socket() sk.connect(('127.0.0.1',9000)) while True: sk.send(b'hello') print(sk.recv(1024)) sk.close()
该模型优点:
相比其他模型,使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。
该模型缺点:
首先select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄。
对于IO多路复用(select)
select检测的是哪个套接字准备好了(检测的时候等待了,变成阻塞了)
select之所以比阻塞IO好,就是因为select可以检测多个套接字
多个链接下select才能发挥它的优势
但是你的套接字特别多,你怎么知道哪个好了呢,那么就得用循环去遍历一下
那么如果特别多的时候,效率也就不咋高了
epoll:只支持linux系统(就是为了解决select效率低的问题)
epoll比pool,select效率高
selectors 更好用,解决了上面select,epoll,pool的问题
socketserver用这个模块IO问题也解决了,实现并发也解决了(见链接文章最底部实例)
1.4异步IO:
参考链接:http://www.cnblogs.com/Eva-J/articles/8324837.html