基本概念
IO多路复用是指内核(线性扫描)一旦发现进程指定的一个或者多个IO条件准备就绪,它就通知该进程,执行定义的操作。
适用场景
1、当客户处理多个描述符时(一般是交互式输入和网络套接字),必须使用I/O复用。
2、当一个客户同时处理多个套接字时,而这种情况是可能的,但很少出现。
3、如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字,一般也要用到I/O复用。
4、如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
5、如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
与多线程或多进程比较
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
select方法
readable , writable , exceptional = select.select(inputs, outputs, inputs, timeout)
select函数要求三个必须序列作为参数和一个可选的以秒为单位的超时值。序列中是表示文件描述符的整数值,它们是我们要等待的连接。这三个序列是关于 输入、输出和异常条件的。如果超时值没有给出的话,select将处于阻塞状态(也就是等待)直到有文件描述符准备动作。如果超时值给出了,那么 select只阻塞给定的时间。如果超时值是0的话,那么将不阻塞。select返回的值是一个由三个序列组成的元组,它们分别代表相应参数的活动的子 集。例如,第一个序列返回的是用于读的输入文件描述符构成的序列。
select,poll,epoll
select,poll,epoll都是IO多路复用的机制。所谓I/O多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般
是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
Python Select Server,可监控事件数量有限制(1024):
server代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | #!/usr/bin/python # -*- coding: utf-8 -*- import select import socket import Queue server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.setblocking( False ) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR , 1 ) server_address = ( '127.0.0.1' , 8888 ) server.bind(server_address) server.listen( 10 ) #select轮询等待读socket集合 inputs = [server] #select轮询等待写socket集合 outputs = [] message_queues = {} #select超时时间 timeout = 20 while True : print "等待活动连接......" readable , writable , exceptional = select.select(inputs, outputs, inputs, timeout) if not (readable or writable or exceptional) : print "select超时无活动连接,重新select...... " continue ; #循环可读事件 for s in readable : #如果是server监听的socket if s is server: #同意连接 connection, client_address = s.accept() print "新连接: " , client_address connection.setblocking( 0 ) #将连接加入到select可读事件队列 inputs.append(connection) #新建连接为key的字典,写回读取到的消息 message_queues[connection] = Queue.Queue() else : #不是本机监听就是客户端发来的消息 data = s.recv( 1024 ) if data : print "收到数据:" , data , "客户端:" ,s.getpeername() message_queues[s].put(data) if s not in outputs: #将读取到的socket加入到可写事件队列 outputs.append(s) else : #空白消息,关闭连接 print "关闭连接:" , client_address if s in outputs : outputs.remove(s) inputs.remove(s) s.close() del message_queues[s] for s in writable: try : msg = message_queues[s].get_nowait() except Queue.Empty: print "连接:" , s.getpeername() , '消息队列为空' outputs.remove(s) else : print "发送数据:" , msg , "到" , s.getpeername() s.send(msg) for s in exceptional: print "异常连接:" , s.getpeername() inputs.remove(s) if s in outputs: outputs.remove(s) s.close() del message_queues[s] |
client端:
1 2 3 4 5 6 7 8 9 10 11 12 | #!/usr/bin/env python # -*- coding:utf-8 -*- import socket ip_port = ( '127.0.0.1' ,8888
) sk = socket.socket() sk.connect(ip_port) while True : inp = raw_input ( 'please input:' ) sk.sendall(inp) sk.close() |
Python Poll Server,Select升级版,无可监控事件数量限制,还是要轮询所有事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | #!/usr/bin/python # -*- coding: utf-8 -*- import socket import select import Queue server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setblocking( False ) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 ) server_address = ( "127.0.0.1" , 8888
) server.bind(server_address) server.listen( 5 ) print "服务器启动成功,监听IP:" , server_address message_queues = {} #超时,毫秒 timeout = 5000 #监听哪些事件 READ_ONLY = ( select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLERR) READ_WRITE = (READ_ONLY|select.POLLOUT) #新建轮询事件对象 poller = select.poll() #注册本机监听socket到等待可读事件事件集合 poller.register(server,READ_ONLY) #文件描述符到socket映射 fd_to_socket = {server.fileno():server,} while True : print "等待活动连接......" #轮询注册的事件集合 events = poller.poll(timeout) if not events: print "poll超时,无活动连接,重新poll......" continue print "有" , len (events), "个新事件,开始处理......" for fd ,flag in events: s = fd_to_socket[fd] #可读事件 if flag & (select.POLLIN | select.POLLPRI) : if s is server : #如果socket是监听的server代表有新连接 connection , client_address = s.accept() print "新连接:" , client_address connection.setblocking( False ) fd_to_socket[connection.fileno()] = connection #加入到等待读事件集合 poller.register(connection,READ_ONLY) message_queues[connection] = Queue.Queue() else : #接收客户端发送的数据 data = s.recv( 1024 ) if data: print "收到数据:" , data , "客户端:" , s.getpeername() message_queues[s].put(data) #修改读取到消息的连接到等待写事件集合 poller.modify(s,READ_WRITE) else : # Close the connection print " closing" , s.getpeername() # Stop listening for input on the connection poller.unregister(s) s.close() del message_queues[s] #连接关闭事件 elif flag & select.POLLHUP : print " Closing " , s.getpeername() , "(HUP)" poller.unregister(s) s.close() #可写事件 elif flag & select.POLLOUT : try : msg = message_queues[s].get_nowait() except Queue.Empty: print s.getpeername() , " queue empty" poller.modify(s,READ_ONLY) else : print "发送数据:" , data , "客户端:" , s.getpeername() s.send(msg) #异常事件 elif flag & select.POLLERR: print " exception on" , s.getpeername() poller.unregister(s) s.close() del message_queues[s] |
Python Epoll Server,基于回调的事件通知模式,轻松管理大量连接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | #!/usr/bin/python # -*- coding: utf-8 -*- import socket, select import Queue serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 ) server_address = ( "127.0.0.1" , 8888
) serversocket.bind(server_address) serversocket.listen( 1 ) print "服务器启动成功,监听IP:" , server_address serversocket.setblocking( 0 ) timeout = 10 #新建epoll事件对象,后续要监控的事件添加到其中 epoll = select.epoll() #添加服务器监听fd到等待读事件集合 epoll.register(serversocket.fileno(), select.EPOLLIN) message_queues = {} fd_to_socket = {serversocket.fileno():serversocket,} while True : print "等待活动连接......" #轮询注册的事件集合 events = epoll.poll(timeout) if not events: print "epoll超时无活动连接,重新轮询......" continue print "有" , len (events), "个新事件,开始处理......" for fd, event in events: socket = fd_to_socket[fd] #可读事件 if event & select.EPOLLIN: #如果活动socket为服务器所监听,有新连接 if socket = = serversocket: connection, address = serversocket.accept() print "新连接:" , address connection.setblocking( 0 ) #注册新连接fd到待读事件集合 epoll.register(connection.fileno(), select.EPOLLIN) fd_to_socket[connection.fileno()] = connection message_queues[connection] = Queue.Queue() #否则为客户端发送的数据 else : data = socket.recv( 1024 ) if data: print "收到数据:" , data , "客户端:" , socket.getpeername() message_queues[socket].put(data) #修改读取到消息的连接到等待写事件集合 epoll.modify(fd, select.EPOLLOUT) #可写事件 elif event & select.EPOLLOUT: try : msg = message_queues[socket].get_nowait() except Queue.Empty: print socket.getpeername() , " queue empty" epoll.modify(fd, select.EPOLLIN) else : print "发送数据:" , data , "客户端:" , socket.getpeername() socket.send(msg) #关闭事件 elif event & select.EPOLLHUP: epoll.unregister(fd) fd_to_socket[fd].close() del fd_to_socket[fd] epoll.unregister(serversocket.fileno()) epoll.close() serversocket.close() |
Linux中的 select,poll,epoll 都是IO多路复用的机制。
select
select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。
select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。
poll
poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。
poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。
epoll
直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。