概念:
首先列一下,sellect、poll、epoll三者的区别
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()时便得到通知。
select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
这里玩一下select和poll的实例,不过这个例子对于理解异步和解决问题的办法都会有很大的帮助。一个串行的方法怎么可能实现并行的效果那?
1 #!/usr/bin/env python 2 # -*- coding: utf-8 -*- 3 4 import select 5 import socket 6 import sys 7 import Queue 8 9 10 server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 11 #socket本身是串行处理数据的,当A连接的时候B是无法进行"连接"的,A的会话完毕才可以进行B的连接。如果加上setblocking(0) B用户可以连接,但是也必须等待A处理完事才能处理B 12 #的事情。 13 server.setblocking(0) 14 server_address=('10.64.8.92',10000) 15 print >> sys.stderr,"starting up !!!!!" 16 server.bind(server_address) 17 18 #A连接后还可以在连接4个人,在多的人就无法连接。一共5个人可以排队 19 server.listen(5) 20 21 22 ''' 23 select 有三个表(自定义),input接受消息,output 回消息,error 错误信息 24 select,回循环遍历每一个列表。不停的遍历默认1024文件描述符。不管有没有对象真的占用描述符都会一直循环,并且一个对象都没有的时候 25 将进入阻塞状态 26 ''' 27 #这里必须这样写把server放进去,新的连接信息其实并不会加入这个列表中去。这个列表始终只有server 28 inputs=[server] 29 30 #这个链列表是存储要发消息的连接对象 31 outputs=[] 32 33 #这个队列是存放每个对象发过来的消息 34 message_queues={} 35 36 #开始循环监听 37 while inputs: 38 print >>sys.stderr, ' waiting for the next event' 39 #inputs列表只会有server,个人理解select.select就是监控server 文件描述符是否有变化 40 readable, writable, exceptional = select.select(inputs, outputs, inputs) 41 42 #开始循环readable 只要有链接对象过来那么这里就开始执行。 43 for s in readable: 44 #注意这里只有新创建的时候才为true,当连接之后在发消息过来这里为false(如果中途断开重新连接那算新的连接) 45 if s is server: 46 print "s:",s 47 print "server:",server 48 print "readable:",readable 49 print "ser filen:",server.fileno() 50 print "s filen:",s.fileno() 51 # A "readable" server socket is ready to accept a connection 52 #connection这里我理解就是socket已经分配了一个文件描述符 53 connection, client_address = s.accept() 54 print >>sys.stderr, 'new connection from', client_address 55 #这里的意思可能是不阻塞其它对象连接这个server,把这个注释掉也没有阻塞新客户端连接 56 connection.setblocking(0) 57 #将套接字对象放到inputs列表中,inputs过程就完事了 58 inputs.append(connection) 59 60 # Give the connection a queue for data we want to send 61 #将套接字注册一个队列,存储消息用。就是通过这个队列的方法(先进先出)将消息发给接受的对象 62 message_queues[connection] = Queue.Queue() 63 else:#这个地方就是套接字对象已经在inpus列表中存在了 64 data = s.recv(1024) 65 if data: 66 # A readable client socket has data 67 print >>sys.stderr, 'received "%s" from %s' % (data, s.getpeername()) 68 #将消息放到对应的套接字队列里面 69 message_queues[s].put(data) 70 # Add output channel for response 将套接如果不在outputs队列中那么就追加进去 71 if s not in outputs: 72 outputs.append(s) 73 else: 74 # Interpret empty result as closed connection 75 print >>sys.stderr, 'closing', client_address, 'after reading no data' 76 # Stop listening for input on the connection 77 if s in outputs: 78 outputs.remove(s) #既然客户端都断开了,我就不用再给它返回数据了,所以这时候如果这个客户端的连接对象还在outputs列表中,就把它删掉 79 inputs.remove(s) #inputs中也删除掉 80 s.close() #把这个连接关闭掉 81 82 # Remove message queue 83 del message_queues[s] 84 85 #这里循环writable,就是处理output队列 86 for s in writable: 87 try: 88 #取套接字对象下的队列消息 89 next_msg = message_queues[s].get_nowait() 90 except Queue.Empty: 91 # No messages waiting so stop checking for writability. 92 print >>sys.stderr, 'output queue for', s.getpeername(), 'is empty' 93 outputs.remove(s) 94 else:#当消息没有报错那就说明队列中有消息,这时把消息发送出去,这个消息发给谁s中有记录所以不怕找不到人 95 print >>sys.stderr, 'sending "%s" to %s' % (next_msg, s.getpeername()) 96 s.send(next_msg) 97 #这里循环出错列表,将出错的套接字在三个列表和队列中全部清除掉 98 for s in exceptional: 99 print >>sys.stderr, 'handling exceptional condition for', s.getpeername() 100 # Stop listening for input on the connection 101 inputs.remove(s) 102 if s in outputs: 103 outputs.remove(s) 104 s.close() 105 106 # Remove message queue 107 del message_queues[s]
poll的方法 跟select差不多只不过列表不用大家自己创建了
poll flags 状态,其实这个就相当于select自己建立的列表。poll就是不断的改变flags 状态然后做出相应的动作
1 #!/usr/bin/env python 2 # -*- coding: utf-8 -*- 3 import select 4 import socket 5 import sys 6 import Queue 7 8 # Create a TCP/IP socket 9 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 10 server.setblocking(0) 11 12 # Bind the socket to the port 13 server_address = ('10.64.8.92', 10000) 14 print >>sys.stderr, 'starting up on %s port %s' % server_address 15 server.bind(server_address) 16 17 # Listen for incoming connections 18 server.listen(5) 19 20 # Keep up with the queues of outgoing messages 21 message_queues = {} 22 23 24 25 TIMEOUT = 1000. 26 # Commonly used flag setes 27 READ_ONLY = select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLERR 28 READ_WRITE = READ_ONLY | select.POLLOUT 29 30 31 poller = select.poll() 32 #注册下服务器,当收到新连接的时候出发事件 33 poller.register(server, READ_ONLY) 34 35 # 返回文件描述符和对象的一个映射关系 36 fd_to_socket = { server.fileno(): server, 37 } 38 while True: 39 40 # Wait for at least one of the sockets to be ready for processing 41 print >>sys.stderr, ' waiting for the next event' 42 events = poller.poll(TIMEOUT) 43 44 for fd, flag in events: 45 46 # 检测套接字文件描述符 47 s = fd_to_socket[fd] 48 if flag & (select.POLLIN | select.POLLPRI):#新连接和消息接受在这里处理 49 if s is server: 50 # "readable" 开始接受新的连接 51 connection, client_address = s.accept() 52 print >>sys.stderr, 'new connection from', client_address 53 connection.setblocking(0) 54 fd_to_socket[ connection.fileno() ] = connection 55 poller.register(connection, READ_ONLY) 56 # Give the connection a queue for data we want to send 57 message_queues[connection] = Queue.Queue() 58 else:#如果不是新的连接这里传递过来的一定是数据。这里就接受一下 59 data = s.recv(1024) 60 if data:# 61 # A readable client socket has data 62 print >>sys.stderr, 'received "%s" from %s' % (data, s.getpeername()) 63 message_queues[s].put(data) 64 # Add output channel for response,,修改状态 这个就是select中的output列表 65 poller.modify(s, READ_WRITE) 66 else: 67 # Interpret empty result as closed connection 68 print >>sys.stderr, 'closing', client_address, 'after reading no data' 69 # Stop listening for input on the connection 70 poller.unregister(s) 71 s.close() 72 73 # Remove message queue 74 del message_queues[s] 75 elif flag & select.POLLHUP: #客户端挂了或者客户端关闭了 76 # Client hung up 77 print >>sys.stderr, 'closing', client_address, 'after receiving HUP' 78 # Stop listening for input on the connection 79 poller.unregister(s) 80 s.close() 81 elif flag & select.POLLOUT:#回复数据 82 # Socket is ready to send data, if there is any to send. 83 try: 84 next_msg = message_queues[s].get_nowait() 85 except Queue.Empty: 86 # No messages waiting so stop checking for writability. 87 print >>sys.stderr, 'output queue for', s.getpeername(), 'is empty' 88 poller.modify(s, READ_ONLY) 89 else: 90 print >>sys.stderr, 'sending "%s" to %s' % (next_msg, s.getpeername()) 91 s.send(next_msg) 92 elif flag & select.POLLERR: 93 print >>sys.stderr, 'handling exceptional condition for', s.getpeername() 94 # Stop listening for input on the connection 95 poller.unregister(s) 96 s.close() 97 98 # Remove message queue 99 del message_queues[s]
客户端连接代码:这个手动测试的代码方便理解异步中的每个步骤
1 #!/usr/bin/env python 2 import socket 3 import sys 4 5 HOST, PORT = "10.64.8.92",10000 6 7 8 # Create a socket (SOCK_STREAM means a TCP socket) 9 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 10 sock.connect((HOST, PORT)) 11 while 1: 12 # Connect to server and send data 13 data=raw_input("echo server:") 14 sock.sendall(bytes(data)) 15 16 # Receive data from the server and shut down 17 received = str(sock.recv(1024)) 18 print received
原文:
https://pymotw.com/2/select/