python之IO多路复用
1. 多路复用概念:
监听多个描述符(文件描述符(windows下暂不支持)、网络描述符)的状态,如果描述符状态改变 则会被内核修改标志位,进而被进程获取进而进行读写操作
I/O多路复用是用于提升效率,单个进程可以同时监听多个网络连接IO
I/O是指Input/Output
I/O多路复用,通过一种机制,可以监视多个文件描述符,一旦描述符就绪(读就绪和写就绪),能通知程序进行相应的读写操作。
I/O多路复用避免阻塞在io上,原本为多进程或多线程来接收多个连接的消息变为单进程或单线程保存多个socket的状态后轮询处理.
2. 多路复用两种触发方式(epool):
水平触发 level-triggered,epoll对于fd的默认事件模型就是水平触发,即监控到fd可读写时,就会触发并且返回fd,例如fd可读时,但是使用recv没有全部读取完毕,那下次还会将fd触发返回,相对而言,这个更安全一些
边缘触发 edge-triggered, epoll可以对某个fd进行边缘触发,边缘触发的意思就是每次只要触发一次我就会给你返回一次,即使你处理完成一半,我也不会给你返回了,除非他下次再次发生一个事件。
使用例子:epoll.register(serversocket.fileno(), select.EPOLLIN | select.EPOLLET)
3. 阻塞/非阻塞 模式:
阻塞:
如果阻塞模式则等待数据
非阻塞:
如果非阻塞模式有数据返回数据、无数据直接返回报错
4. I/O模型:
同步I/O:
一问一答 等待数据(阻塞模式)或 不管有没有数据都返回(非阻塞模式)
异步I/O:
用户进程问完之后干别的处理结果出来之后告知用户进程
> SELECT
select是通过系统调用来监视一组由多个文件描述符组成的数组,通过调用select()返回结果,数组中就绪的文件描述符会被内核标记出来,然后进程就可以获得这些文件描述符,然后进行相应的读写操作
select的实际执行过程如下:
select需要提供要监控的数组,然后由用户态拷贝到内核态
内核态线性循环监控数组,每次都需要遍历整个数组
内核发现文件描述符状态符合操作结果,将其返回
所以对于我们监控的socket都要设置为非阻塞的,只有这样才能保证不会被阻塞
优点:基本各个平台都支持
缺点:
每次调用select,都需要把fd集合由用户态拷贝到内核态,在fd多的时候开销会很大
单个进程能够监控的fd数量存在最大限制,因为其使用的数据结构是数组
每次select都是线性遍历整个数组,当fd很大的时候,遍历的开销也很大
使用:
r, w, e = 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秒,之后返回三个空列表。 如果由变化,则直接执行并返回。
3个list中可接收的参数,可以是Python的file对象,例如sys.stdin,os.open,open返回的对象等等。socket对象将会返回socket.socket(),也可以自定义类,只要由合适的fileno函数即可,前提是真实的文件名描述符
1 #sync_server
2 importsocket, select3 importqueue4
5 server =socket.socket()6 server.bind(('localhost', 9000))7 server.listen(100)8
9 server.setblocking(False) #不阻塞
10
11 #监控的队列
12 input_list =[server, ]13 output_list =[]14
15 #返回的消息词典
16 message_dic ={}17
18 whileTrue:19 #开始监听
20 rList,wList,eList =select.select(input_list, output_list, input_list)21 print(rList)22
23 for r inrList:24 #如果监听到的是server,则说明有新连结
25 if r isserver:26 conn, addr =server.accept()27 print('建立了新的连接,', addr)28 #监听列表中把新连接进来的添加
29 input_list.append(conn)30 #建立消息队列
31 message_dic[conn] =queue.Queue()32 #如果不是server,则说明收到的是连接发来了消息
33 else:34 try:35 recv_data = r.recv(1024)36 ifrecv_data:37 print('收到消息', recv_data)38 #添加到监听的队列output_list
39 output_list.append(r)40 #消息队列中增加消息
41 message_dic[r].put(recv_data)42 exceptConnectionResetError as e:43 print('连接断开')44 if e inoutput_list:45 output_list.remove(r)46 input_list.remove(r)47 delmessage_dic[r]48 continue
49
50 for m inwList:51 #消息队列中取消息
52 send_data =message_dic[m].get()53 m.send(send_data)54 #从队列output_list中移除
55 output_list.remove(m)
1 #client
2 importsocket3
4 client =socket.socket()5 client.connect(('localhost',9000))6
7 whileTrue:8 send_date = input('>>')9 client.send(send_date.encode())10 data = client.recv(1024)11 print(data)12
13 client.close()
> pool(只适用于Unix/Linux操作系统)
poll本质上与select基本相同,只不过监控的最大连接数上相较于select没有了限制,因为poll使用的数据结构是链表,而select使用的是数组,数组是要初始化长度大小的,且不能改变
触发方式:水平触发
poll原理
将fd列表,由用户态拷贝到内核态
内核态遍历,发现fd状态变为就绪后,返回fd列表
poll状态
POLLIN 有数据读取
POLLPRT 有数据紧急读取
POLLOUT 准备输出:输出不会阻塞
POLLERR 某些错误情况出现
POLLHUP 挂起
POLLNVAL 无效请求:描述无法打开
缺点:
每次调用select,都需要把fd集合由用户态拷贝到内核态,在fd多的时候开销会很大
每次select都是线性遍历整个列表,当fd很大的时候,遍历的开销也很大
poll方法
1. register,将要监控的文件描述符注册到poll中,并添加监控的事件类型
2. unregister,注销文件描述符监控
3. modify, 修改文件描述符监控事件类型
4. poll([timeout]),轮训注册监控的文件描述符,返回元祖列表,元祖内容是一个文件描述符及监控类型(POLLIN,POLLOUT等等),如果设置了timeout,则会阻塞timeout秒,然后返回控列表,如果没有设置timeout 微秒,则会阻塞到有返回值为止
1 #-*- coding: utf-8 -*-
2
3 importselect4 importsocket5 importdatetime6
7 sock =socket.socket()8 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)9 sock.bind(("localhost", 10000))10 sock.listen(5)11 #设置为非阻塞
12 sock.setblocking(0)13
14 poll =select.poll()15 poll.register(sock, select.POLLIN)16
17 connections ={}18
19 whileTrue:20 #遍历被监控的文件描述符
21 print(datetime.datetime.now())22 for fd, event in poll.poll(10000):23 if event ==select.POLLIN:24 if fd ==sock.fileno():25 #如果是当前的sock,则接收请求
26 con, addr =sock.accept()27 poll.register(con.fileno(), select.POLLIN)28 connections[con.fileno()] =con29 else:30 #如果是监听的请求,读取其内容,并设置其为等待写监听
31 con =connections[fd]32 data = con.recv(1024)33 ifdata:34 print("%s accept %s" %(fd, data))35 poll.modify(fd, select.POLLOUT)36 else:37 con =connections[fd]38 try:39 con.send(b"Hello, %d" %fd)40 print("con >>>", con)41 finally:42 poll.unregister(con)43 connections.pop(fd)44 con.close()
>epoll(只适用于Unix/Linux操作系统)
epoll相当于是linux内核支持的方法,而epoll主要是解决select,poll的一些缺点
1.数组长度限制
解决方案:fd上限是最大可以打开文件的数目,具体数目可以查看/proc/sys/fs/file-max。一般会和内存有关
2.需要每次轮询将数组全部拷贝到内核态
解决方案:每次注册事件的时候,会把fd拷贝到内核态,而不是每次poll的时候拷贝,这样就保证每个fd只需要拷贝一次。
3.每次遍历都需要列表线性遍历
解决方案:不再采用遍历的方案,给每个fd指定一个回调函数,fd就绪时,调用回调函数,这个回调函数会把fd加入到就绪的fd列表中,所以epoll只需要遍历就绪的list即可。
触发方式:边缘触发
>selector 模块使用举例:
1 __author__ = 'Administrator'
2
3 import selectors #基于select模块实现的IO多路复用
4 importsocket5 sel = selectors.DefaultSelector() #根据平台选择最佳的IO多路机制,比如linux就会选择epoll windows会选择select
6
7 defaccept(sock, mask):8 conn, addr =sock.accept()9 print('accept', conn, 'from', addr, 'mask', mask)10 conn.setblocking(False)11 sel.register(conn, selectors.EVENT_READ, read)12
13 defread(conn, mask):14 data = conn.recv(1024)15 ifdata:16 print('echoing', repr(data), conn)17 conn.send(data)18 else:19 print('closing', conn)20 sel.unregister()21 conn.close()22
23 server=socket.socket()24 server.bind(('localhost',9002))25 server.listen(10)26
27 server.setblocking(False)28 sel.register(server, selectors.EVENT_READ, accept) #注册sock,有连接进来则调用accept
29
30 whileTrue:31 events = sel.select() #默认阻塞,有活动连接就返回活动的连接列表
32 for key,mask inevents:33 #key.data 有活动的绑定函数
34 #key.fileobj 有活动的文件描述符
35 callback =key.data36 callback(key.fileobj, mask)
执行过程: