文章目录
- 阻塞IO
- 非阻塞IO
- 非阻塞的TCP传输
- IO多路复用
- select机制
- poll机制
- epoll机制
- selector模块
- 异步IO
阻塞IO
我们用一副图来描述阻塞IO的过程
阻塞IO会阻塞两段时间:
- 等待数据准备好
- 复制数据到进程中
非阻塞IO
和阻塞IO不同的是,在系统内核还未准备好数据的时候,应用程序会往下执行,但程序每隔一段时间,会不断向内核发起系统调用询问数据的准备情况,效率仍然不高。
直到有一次数据准备好之后,就会进行数据的复制操作将数据提交给应用程序进行使用
非阻塞的TCP传输
import socket
ip_port = ("127.0.0.1",8005)
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sk.bind(ip_port)
sk.setblocking(False)
sk.listen()
conn_lst = []
del_lst = []
while True:
try:
conn,addr = sk.accept()
print("建立连接了",addr)
conn_lst.append(conn)
except BlockingIOError:
for i in conn_lst:
try:
msg = i.recv(1024)
if msg == b'':
del_lst.append(conn)
continue
print(msg)
except BlockingIOError:
pass
for x in del_lst:
conn_lst.remove(x)
del_lst.clear()
设置了setblocking(False)的参数之后,socket连接就会变为非阻塞的连接了,当没有及时收到消息的时候就会报出一个BlockingIOError的报错
socket连接默认是阻塞的,就是没收到消息会一直阻塞
我们来分析一下这串代码,进入第一层while循环后,它会不断的等待建立连击,如果一个新的连接生成了,那就把它放入连接池中,也就是conn_lst列表
接着遍历连接池,每一个conn就会等待接收消息,而对端消息没有及时发出,那么就会报BlockingIOError,因此我们用try方法来规避,使得程序整体不会在任何地方进行进行堵塞。
在这里,如果这个连接的对端已经关闭,那么socket就会recv一条空的消息,因此我们判断如果收到的消息为空,那么就把这个conn加入到待删除的连接池del_lst中,在代码的最后,再把这个连接池进行清空的操作。
IO多路复用
多路服用和阻塞复用的实现很相似,不同的是,应用程序会调用系统底层的代理来进行监听可收取数据的对象,比如socket中的accpet和conn对象等。
代理会代替程序向系统发起系统调用,当内核把数据准备好之后,就会通知代理来取数据,之后内核就会把准备好的数据直接放送给应用程序。
代理就像餐厅的服务生,他替顾客向厨师下单,并最后把菜品给端上来。
由于加了一层代理,实际上单个连接的效率并不如前两种模型,但是在高并发的情况下就很有用,因为应用程序可以不用等待数据的返回,就去干其他的工作。
select机制
select就是实现多路IO模型的一种,在windows系统和linux系统上都可以使用
看一例select模块编写的并发编程的例子,这里只写出客户端的代码
import select
import socket
sk = socket.socket()
sk.bind(("127.0.0.1",8005))
sk.setblocking(False)
sk.listen()
read_lst = [sk] #可读监听对象列表
while True:
r_lst,w_lst,x_lst = select.select(read_lst,[],[]) #select可以监听三类对象,可读对象,可写对象以及可执行对象
#print(r_lst)
for i in r_lst:
#print(i)
if i is sk:
conn,addr = i.accept()
read_lst.append(conn)
if i is conn:
msg = i.recv(1024)
if msg == b'':
i.close()
read_lst.remove(i)
continue
print(msg)
首先,还是先生成一个非阻塞的socket对象,不同的是我们会把socket对象放入一个列表中,这就是代理需要监听的对象的列表
在监听的过程中,哪个对象接收到了数据,那么select方法就会返回哪个对象,就是代码中的r_lst列表。
第一次循环的时候,客户端发起了连接,那列表中的socket接收到了消息因此返回的值就是sk,因此可以accpet建立连接,我们把conn对象也加入监听的列表中。
在第二次循环,conn对象接收到了对端发来的消息,因此select代理发起了系统调动,开始接收消息
后续的循环都是类似的,直到对端断开连接,发送空消息的时候,服务端进行判断后,主动断开连接。
poll机制
poll机制是linux系统特有的,poll机制和select机制的实现原理是相同的,poll的优势在于,可监听的对象比select的更多
epoll机制
epoll机制就比较不一样了,epoll会给每一个监听的对象绑定一个回调函数,当监听对象收到了消息,就会触发回调函数而应用进程反馈,因为省去了select每一次遍历列表的操作,因此epoll的效率是非常高的。
selector模块
import selectors
import socket
def accept(sk,mask):
conn,addr = sk.accept() #相当于sk.accept()
sel.register(conn,selectors.EVENT_READ,read) #在selector的读列表当中存入conn的监听对象,并绑定自定义read回调函数
def read(conn,mask):
try:
data = conn.recv(1024)
if not data:
sel.unregister(conn)
conn.close()
return
except Exception:
sel.unregister()
conn.close()
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sk.bind(("127.0.0.1,8005"))
sk.listen()
#sk.setsockopt()
sk.setblocking(False)
sk.accept()
sel = selectors.DefaultSelector() #选择适合当前的系统的IO多路复用的机制
sel.register(sk,selectors.EVENT_READ,accept) #在selector的读列表当中存入sk的监听对象,并绑定自定义accept回调函数
while True:
events = sel.select() #会等待监听列表中是否有人发起连接或者发送的消息的事件
for sel_obj,mask in events: #遍历events对象
callback = sel_obj.data #这里sel_obj.data相当于就是accept函数
callback(sel_obj.fileobj,mask) #执行回调函数并传入对象accept(sk)
异步IO
异步IO在发起一次系统调用后,就会执行其他程序中的其他功能,并且不会等待,直到内核准备好数据后,会直接发送给应用程序,是效率最高的一种IO模型。