文章目录

  • 阻塞IO
  • 非阻塞IO
  • 非阻塞的TCP传输
  • IO多路复用
  • select机制
  • poll机制
  • epoll机制
  • selector模块
  • 异步IO


阻塞IO

我们用一副图来描述阻塞IO的过程

python 非阻塞定时器50ms python非阻塞io_python


阻塞IO会阻塞两段时间:

  • 等待数据准备好
  • 复制数据到进程中

非阻塞IO

python 非阻塞定时器50ms python非阻塞io_IO模型_02


和阻塞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多路复用

python 非阻塞定时器50ms python非阻塞io_数据_03


多路服用和阻塞复用的实现很相似,不同的是,应用程序会调用系统底层的代理来进行监听可收取数据的对象,比如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

python 非阻塞定时器50ms python非阻塞io_非阻塞_04


异步IO在发起一次系统调用后,就会执行其他程序中的其他功能,并且不会等待,直到内核准备好数据后,会直接发送给应用程序,是效率最高的一种IO模型。