二十五、Python -- I/O 多路复用
I/O 多路复用指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
Linux
Linux 中的 select, poll,epoll 都是I/O 多路复用的机制。
摘抄老师博客:
1 select 2 3 select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。 4 select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。 5 select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。 6 另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。 7 8 poll 9 10 poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。 11 poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。 12 另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。 13 14 epoll 15 16 直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。 17 epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。 18 epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。 19 另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
Python
Python 中有一个select 模块,其中提供了 select、poll、epoll三个方法,分别调用系统的 select、poll、epoll 从而实现I/O多路复用。
Window Python :
提供:select
Mac Python:
提供:select
Linux Python:
提供:select poll epoll
注意:网络操作、文件操作、终端操作都属于I O 操作,对于windows 只支持socket操作。其他操作系统支持其他I/O 操作。但是无法检测到普通文件操作 自上次读取是否已经变化。
select
1 对于select 方法 2 3 句柄列表A, 句柄列表B,句柄列表C = select.select(句柄序列1,句柄序列2,句柄序列3,超时时间) 4 5 参数: 可接受四个参数(前三个必须) 6 返回值:三个列表 7 8 select 方法用来监视文件句柄,如果句柄发生变化,则获取该句柄。 9 10 1:当参数1 序列中的句柄发生可读时(accept和read),则获取发生变化的句柄,并添加到 句柄列表A中 11 2:当参数2 序列中含有句柄时,则该序列中的所有的句柄添加到 句柄列表B中 12 3:当参数3 序列中的句柄发生错误时,则将发生该错误的句柄添加到 句柄列表C 中。 13 4:当超时时间未设置,则select 会一直阻塞,一直到监听的句柄发生变化 14 档超时时间 = 1时,那么如果监听的句柄无任何变化,则select 会阻塞1秒,之后返回3个空列表,如果监听的句柄有变化,则直接执行。
使用select 实现伪处理多个socket请求
select 内部自动监听socket对象,一旦socket 变换,就能感知到添加到select 中
服务端: server_obj 只用来接受用户的连接
conn, address = server_obj.accept()
conn.send conn用来处理和用户的交互,所以只有conn 才能感知到用户发送消息
客户端:
c_obj
1、服务端:仅仅使用readable list
1 import socket 2 import select 3 4 sk1 = socket.socket(socket.AF_INET ,socket.SOCK_STREAM) 5 sk1.bind(('127.0.0.1',9090)) 6 sk1.listen(5) 7 sk1.setblocking(0) #设置为非阻塞,在没有accept 或者recv 到数据的时候,报错 8 9 inputs = [sk1,] #初始化只把服务端sk1 添加到列表 10 11 while True: 12 readable,writeable,error = select.select(inputs,[],inputs,1)
print('正在监听 socket 对象 %d' % len(readable))
print(readable)
14 for r in readable: 15 #当客户端第一次连接服务端的时候 sk1 发生变化,会添加到readable 16 if sk1 == r: 17 print('连接成功') 18 request, address = r.accept() #接受请求 19 20 request.setblocking(0) 21 inputs.append(request) #将连接到服务器的客户端socket 也添加到input中,监视是否有读写操作 22 23 else: #如果不是用户的首次连接信息,就是用户的新的读写操作 24 try: 25 rec = r.recv(1024) 26 if rec: #正常接收用户发送的消息 27 print(str(rec,encoding = 'utf-8')) 28 except Exception as e: 29 inputs.remove(r)
2、服务端: 使用readable list 和error list
1 import socket 2 import select 3 4 sk1 = socket.socket(socket.AF_INET ,socket.SOCK_STREAM) 5 sk1.bind(('127.0.0.1',9090)) 6 sk1.listen(5) 7 sk1.setblocking(0) #设置为非阻塞,在没有accept 或者recv 到数据的时候,报错 8 9 inputs = [sk1,] #初始化只把服务端sk1 添加到列表 10 11 while True: 12 readable,writeable,error = select.select(inputs,[],inputs,1) 13 14 for r in readable: 15 #当客户端第一次连接服务端的时候 16 if sk1 == r: 17 print('连接成功') 18 request, address = r.accept() #接受请求 19 20 request.setblocking(0) 21 inputs.append(request) #强连接到的客户端socket 也添加到input中,监视是否有读写操作 22 23 else: #如果不是用户的连接信息,就是用户的新的读写操作 24 rec = r.recv(1024) 25 if rec: #正常接收用户发送的消息 26 print(str(rec,encoding = 'utf-8'))
r.sendall(bytes('hello',encoding = 'utf-8')) 27 28 for sk in error: 29 input.remove(sk)
3、服务端:读写分离 使用 readable list writeable list
1 import socket 2 import select 3 4 sk1 = socket.socket(socket.AF_INET ,socket.SOCK_STREAM) 5 sk1.bind(('127.0.0.1',9090)) 6 sk1.listen(5) 7 sk1.setblocking(0) #设置为非阻塞,在没有accept 或者recv 到数据的时候,报错 8 9 inputs = [sk1,] #初始化只把服务端sk1 添加到列表 10 output = [] #用于输出 11 12 message_dict = {} #用于存放用户的消息 13 14 while True: 15 readable,writeable,error = select.select(inputs,output,inputs,1) 16 print('正在监听 socket 对象 %d' % len(readable)) 17 print(readable) 18 for r in readable: 19 #当客户端第一次连接服务端的时候 20 if sk1 == r: 21 print('连接成功') 22 request, address = r.accept() #接受请求 23 24 request.setblocking(0) 25 inputs.append(request) #强连接到的客户端socket 也添加到input中,监视是否有读写操作 26 27 message_dict[request] = [] #针对每个sockt 建立一个列表 28 29 else: #如果不是用户的连接信息,就是用户的新的读写操作 30 try: 31 32 rec = r.recv(1024) 33 message_dict[r].append(str(rec,encoding = 'utf-8')) #将用户发送的消息放到dict 34 print(str(rec,encoding = 'utf-8')) 35 output.append(r)#将发过消息的用户,添加到output 36 except Exception as e: 37 print(e) 38 inputs.remove(r) 39 40 41 # wlist 仅仅保存谁发送了消息 42 for conn in writeable: 43 me = message_dict[conn][0] 44 print(me) #打印用户发送的第一条信息 45 del message_dict[conn][0] 46 conn.sendall(bytes(me + 'nice',encoding= 'utf-8')) 47 output.remove(conn) 48 49 for i in error: 50 inputs.remove(i)
客户端
1 #!/usr/bin/evn python 2 #-*- coding:utf-8 -*- 3 import socket 4 5 ip_port = ('127.0.0.1',9090) 6 sk = socket.socket() 7 sk.connect(ip_port) #连接到sever端 8 sk.sendall(bytes('我来连接你',encoding = 'utf-8') )#发送请求 9 server_replay = sk.recv(1024) #收到server 发送的回
print(str(server_replay,encoding='utf-8'))
10 sk.close()
用 select 来监听终端操作
摘抄老师博客,未验证正确性
1 import select 2 import threading 3 import sys 4 5 while True: 6 readable,writeable, error = select.select([sys.stdin,],[],[],1) 7 8 if sys.stdin in readable: 9 print('select get stdin' ,sys.stdin.readline())

浙公网安备 33010602011771号