python--io多路复用之select实现
1、I/O多路复用指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
2、I/O多路复用避免阻塞在io上,原本为多进程或多线程来接收多个连接的消息变为单进程或单线程保存多个socket的状态后轮询处理。
select
select是通过系统调用来监视一组由多个文件描述符组成的数组,通过调用select()返回结果,数组中就绪的文件描述符会被内核标记出来,然后进程就可以获得这些文件描述符,然后进行相应的读写操作
select的实际执行过程如下:
1、select需要提供要监控的数组,然后由用户态拷贝到内核态。
2、内核态线性循环监控数组,每次都需要遍历整个数组。
3、内核发现文件描述符状态符合操作结果,将其返回。
所以对于我们监控的socket都要设置为非阻塞的,只有这样才能保证不会被阻塞。
优点
基本各个平台都支持
缺点
1、每次调用select,都需要把fd集合由用户态拷贝到内核态,在fd多的时候开销会很大
2、单个进程能够监控的fd数量存在最大限制,因为其使用的数据结构是数组。
3、每次select都是线性遍历整个数组,当fd很大的时候,遍历的开销也很大
python使用select
语法:r_list, w_list, e_list = select.select( rlist, wlist, errlist [,timeout] )
说明详解:
rlist,wlist和errlist均是waitable object; 都是文件描述符,就是一个整数,或者一个拥有返回文件描述符的函数fileno()的对象。
rlist: 等待读就绪的文件描述符数组
wlist: 等待写就绪的文件描述符数组
errlist: 等待异常的数组
在linux下这三个列表可以是空列表,但是在windows上不行
当rlist数组中的文件描述符发生可读时(调用accept或者read函数),则获取文件描述符并添加到r数组中。
当wlist数组中的文件描述符发生可写时,则获取文件描述符添加到w数组中
当errlist数组中的文件描述符发生错误时,将会将文件描述符添加到e队列中
当超时时间没有设置时,如果监听的文件描述符没有任何变化,将会一直阻塞到发生变化为止
当超时时间设置为1时,如果监听的描述符没有变化,则select会阻塞1秒,之后返回三个空列表。 如果由变化,则直接执行并返回。
一、基于select实现的IO多路复用的基础实例:
io_server.py
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 """ 4 IO多路复用服务器端 5 """ 6 import socket 7 8 sk1 = socket.socket() 9 sk1.bind(('127.0.0.1', 8001)) 10 sk1.listen(5) 11 12 sk2 = socket.socket() 13 sk2.bind(('127.0.0.1', 8002)) 14 sk2.listen(5) 15 16 sk3 = socket.socket() 17 sk3.bind(('127.0.0.1', 8003)) 18 sk3.listen(5) 19 20 inputs = [sk1, sk2, sk3] 21 import select 22 23 while True: 24 #[sk1, sk2, sk3],select内部启动监听sk1, sk2, sk3三个对象,一旦某个句柄发生变化 25 #如果有人用sk1 26 #r_list = [sk1, sk2, sk3] 27 r_list, w_list, e_list = select.select(inputs, [], [], 1) 28 print(r_list) 29 for sk in r_list: 30 #每一个连接对象 31 conn, address = sk.accept() 32 conn.sendall(bytes('Hello', encoding='utf-8')) 33 conn.close()
io_client.py
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 """ 4 客户端1,请求8001端口 5 """ 6 import socket 7 8 ck = socket.socket() 9 ck.connect(('127.0.0.1', 8001)) 10 11 content = str(ck.recv(1024), encoding='utf-8') 12 print(content)
二、IO多路复用服务器端升级改造后的代码实现
io_server2.py
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 """ 4 IO多路复用服务器端升级改造 5 """ 6 7 import socket 8 import select 9 10 sk = socket.socket() 11 12 sk.bind(('127.0.0.1', 8001)) 13 sk.listen() 14 15 inputs = [sk,] 16 while True: 17 r_list, w_list, e_list = select.select(inputs, [], [], 1) 18 print('正在监听的socket对象:%d' % len(inputs)) 19 for sk_or_conn in r_list: 20 #每一个连接对象 21 if sk_or_conn == sk: 22 #表示有新用户来连接 23 conn, address = sk.accept() 24 inputs.append(conn) 25 else: 26 #有老用户发消息了 27 try: 28 data_bytes = sk_or_conn.recv(1024) 29 except Exception as ex: 30 #如果有用户终断连接,则移除句柄 31 inputs.remove(sk_or_conn) 32 else: 33 #用户正常发送信息 34 data_str = str(data_bytes, encoding='utf-8') 35 sk_or_conn.sendall(bytes(data_str + '好', encoding='utf-8')) 36 37 for sk in e_list: 38 inputs.remove(sk)
三、IO多路复用服务器端升级改造,读、写分离
io_server3.py
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 """ 4 IO多路复用服务器端升级改造,读、写分离 5 """ 6 7 import socket 8 import select 9 10 sk = socket.socket() 11 sk.bind(('127.0.0.1', 8001)) 12 sk.listen() 13 14 inputs = [sk,] 15 outputs = [] 16 message_dict = {} 17 18 while True: 19 20 r_list, w_list, e_list = select.select(inputs, outputs, inputs, 1) 21 22 print('正在监听的socket对象:%d' % len(inputs)) 23 for sk_or_conn in r_list: 24 #每一个连接对象 25 if sk_or_conn == sk: 26 #表示有新用户来连接 27 conn, address = sk.accept() 28 inputs.append(conn) 29 #将连接的用户添加到字典中 30 message_dict[conn] = [] 31 else: 32 #有老用户发消息了 33 try: 34 data_bytes = sk_or_conn.recv(1024) 35 except Exception as ex: 36 #如果有用户终断连接,则移除句柄 37 inputs.remove(sk_or_conn) 38 else: 39 # 用户正常发送信息 40 data_str = str(data_bytes, encoding='utf-8') 41 message_dict[sk_or_conn].append(data_str) #将用户发送过来的信息存在在字典中 42 # sk_or_conn.sendall(bytes(data_str + '好', encoding='utf-8')) 43 outputs.append(sk_or_conn) 44 45 #写操作 46 for sk_out in w_list: 47 recv_data = message_dict[sk_out][0] #从字典中获取信息数据 48 del message_dict[sk_out][0] #获取数据后,清空字典,等待存储下次的数据 49 sk_out.sendall(bytes(recv_data + '好', encoding='utf-8')) 50 outputs.remove(sk_out) 51 52 for sk in e_list: 53 inputs.remove(sk)