I/O 多路复用
I/O多路复用指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合:
- 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
- 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
- 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
- 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
- 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
Linux
Linux中的 select,poll,epoll 都是IO多路复用的机制。
 
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 从而实现IO多路复用。
| 1 2 3 4 5 6 | Windows Python:    提供: selectMac Python:    提供: selectLinux Python:    提供: select、poll、epoll | 
注意:网络操作、文件操作、终端操作等均属于IO操作,对于windows只支持Socket操作,其他系统支持其他IO操作,但是无法检测 普通文件操作 自动上次读取是否已经变化。
对于select方法:
| 1 2 3 4 5 6 7 8 9 10 11 | 句柄列表11, 句柄列表22, 句柄列表33=select.select(句柄序列1, 句柄序列2, 句柄序列3, 超时时间)参数: 可接受四个参数(前三个必须)返回值:三个列表select方法用来监视文件句柄,如果句柄发生变化,则获取该句柄。1、当 参数1序列中的句柄发生可读时(accetp和read),则获取发生变化的句柄并添加到 返回值1序列中2、当 参数2序列中含有句柄时,则将该序列中所有的句柄添加到 返回值2序列中3、当 参数3序列中的句柄发生错误时,则将该发生错误的句柄添加到 返回值3序列中4、当 超时时间 未设置,则select会一直阻塞,直到监听的句柄发生变化   当 超时时间 = 1时,那么如果监听的句柄均无任何变化,则select会阻塞 1秒,之后返回三个空列表,如果监听的句柄有变化,则直接执行。 | 
 
#!/usr/bin/env python # -*- coding:utf-8 -*- import select import threading import sys while True: readable, writeable, error = select.select([sys.stdin,],[],[],1) if sys.stdin in readable: print 'select get stdin',sys.stdin.readline() 利用select监听终端操作实例
 
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 4 import socket 5 import select 6 7 sk1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 8 sk1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 9 sk1.bind(('127.0.0.1',8002)) 10 sk1.listen(5) 11 sk1.setblocking(0) 12 13 inputs = [sk1,] 14 15 while True: 16 readable_list, writeable_list, error_list = select.select(inputs, [], inputs, 1) 17 for r in readable_list: 18 # 当客户端第一次连接服务端时 19 if sk1 == r: 20 print 'accept' 21 request, address = r.accept() 22 request.setblocking(0) 23 inputs.append(request) 24 # 当客户端连接上服务端之后,再次发送数据时 25 else: 26 received = r.recv(1024) 27 # 当正常接收客户端发送的数据时 28 if received: 29 print 'received data:', received 30 # 当客户端关闭程序时 31 else: 32 inputs.remove(r) 33 34 sk1.close() 35 36 利用select实现伪同时处理多个Socket客户端请求:服务端
 
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 import socket 4 5 ip_port = ('127.0.0.1',8002) 6 sk = socket.socket() 7 sk.connect(ip_port) 8 9 while True: 10 inp = raw_input('please input:') 11 sk.sendall(inp) 12 sk.close() 13 14 利用select实现伪同时处理多个Socket客户端请求:客户端
此处的Socket服务端相比与原生的Socket,他支持当某一个请求不再发送数据时,服务器端不会等待而是可以去处理其他请求的数据。但是,如果每个请求的耗时比较长时,select版本的服务器端也无法完成同时操作。
 
1 #!/usr/bin/env python 2 #coding:utf8 3 4 ''' 5 服务器的实现 采用select的方式 6 ''' 7 8 import select 9 import socket 10 import sys 11 import Queue 12 13 #创建套接字并设置该套接字为非阻塞模式 14 15 server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 16 server.setblocking(0) 17 18 #绑定套接字 19 server_address = ('localhost',10000) 20 print >>sys.stderr,'starting up on %s port %s'% server_address 21 server.bind(server_address) 22 23 #将该socket变成服务模式 24 #backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5 25 #这个值不能无限大,因为要在内核中维护连接队列 26 27 server.listen(5) 28 29 #初始化读取数据的监听列表,最开始时希望从server这个套接字上读取数据 30 inputs = [server] 31 32 #初始化写入数据的监听列表,最开始并没有客户端连接进来,所以列表为空 33 34 outputs = [] 35 36 #要发往客户端的数据 37 message_queues = {} 38 while inputs: 39 print >>sys.stderr,'waiting for the next event' 40 #调用select监听所有监听列表中的套接字,并将准备好的套接字加入到对应的列表中 41 readable,writable,exceptional = select.select(inputs,outputs,inputs)#列表中的socket 套接字 如果是文件呢? 42 #监控文件句柄有某一处发生了变化 可写 可读 异常属于Linux中的网络编程 43 #属于同步I/O操作,属于I/O复用模型的一种 44 #rlist--等待到准备好读 45 #wlist--等待到准备好写 46 #xlist--等待到一种异常 47 #处理可读取的套接字 48 49 ''' 50 如果server这个套接字可读,则说明有新链接到来 51 此时在server套接字上调用accept,生成一个与客户端通讯的套接字 52 并将与客户端通讯的套接字加入inputs列表,下一次可以通过select检查连接是否可读 53 然后在发往客户端的缓冲中加入一项,键名为:与客户端通讯的套接字,键值为空队列 54 select系统调用是用来让我们的程序监视多个文件句柄(file descrīptor)的状态变化的。程序会停在select这里等待, 55 直到被监视的文件句柄有某一个或多个发生了状态改变 56 ''' 57 58 ''' 59 若可读的套接字不是server套接字,有两种情况:一种是有数据到来,另一种是链接断开 60 如果有数据到来,先接收数据,然后将收到的数据填入往客户端的缓存区中的对应位置,最后 61 将于客户端通讯的套接字加入到写数据的监听列表: 62 如果套接字可读.但没有接收到数据,则说明客户端已经断开。这时需要关闭与客户端连接的套接字 63 进行资源清理 64 ''' 65 66 for s in readable: 67 if s is server: 68 connection,client_address = s.accept() 69 print >>sys.stderr,'connection from',client_address 70 connection.setblocking(0)#设置非阻塞 71 inputs.append(connection) 72 message_queues[connection] = Queue.Queue() 73 else: 74 data = s.recv(1024) 75 if data: 76 print >>sys.stderr,'received "%s" from %s'% 77 (data,s.getpeername()) 78 message_queues[s].put(data) 79 if s not in outputs: 80 outputs.append(s) 81 else: 82 print >>sys.stderr,'closing',client_address 83 if s in outputs: 84 outputs.remove(s) 85 inputs.remove(s) 86 s.close() 87 del message_queues[s] 88 89 #处理可写的套接字 90 ''' 91 在发送缓冲区中取出响应的数据,发往客户端。 92 如果没有数据需要写,则将套接字从发送队列中移除,select中不再监视 93 ''' 94 95 for s in writable: 96 try: 97 next_msg = message_queues[s].get_nowait() 98 99 except Queue.Empty: 100 print >>sys.stderr,' ',s,getpeername(),'queue empty' 101 outputs.remove(s) 102 else: 103 print >>sys.stderr,'sending "%s" to %s'% 104 (next_msg,s.getpeername()) 105 s.send(next_msg) 106 107 108 109 #处理异常情况 110 111 for s in exceptional: 112 for s in exceptional: 113 print >>sys.stderr,'exception condition on',s.getpeername() 114 inputs.remove(s) 115 if s in outputs: 116 outputs.remove(s) 117 s.close() 118 del message_queues[s] 119 120 基于select实现socket服务端
select模块(实现伪并发)
Python中有一个select模块,其中提供了:select、poll、epoll三个方法,分别调用系统的 select,poll,epoll 从而实现IO多路复用
| 1 2 3 4 5 6 7 8 | select 模块Windows Python:    提供: selectMac Python:    提供: selectLinux Python:    提供: select、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.select方法:
select函数需要3个序列作为它的必选参数,此外还有一个可选的以秒单位的超时时间作为第4个参数。3个序列用于输入、输出以及异常情况(错误);如果没有给定超时时间,select会阻塞(也就是处于等待状态),知道其中的一个文件描述符以及为行动做好了准备,如果给定了超时时间,select最多阻塞给定的超时时间,如果超时时间为0,那么就给出一个连续的poll(即不阻塞);select的返回值是3个序列,每个代表相应参数的一个活动子集。第一个序列用于监听socket对象内部是否发生变化,如果有变化表示有新的连接,下面直接看程序代码
select.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 | importsocketimportselectsk =socket.socket()sk.bind(('127.0.0.1',8002))sk.listen(5)sk.setblocking(0)  #不阻塞inputs =[sk,]messages ={}outputs =[]whileTrue:    readable_list, writeable_list, error_list =select.select(inputs, outputs, [], 1)    # readable_list 监听服务端对象,当inputs列表有变化时,变化的值会赋值给readable_list中    # 如果有新的连接进来,sk会发生变化,此时readable_list—的值为sk    # 如果conn对象发生变化,表示客户端发送了新的消息过来,此时readable_list的值为客户端连接    # writeable_lists实现读写分离,需要回复信息的conn对象添加到里面    print(len(inputs),len(readable_list),len(writeable_list),len(outputs))    forr inreadable_list:        # 当客户端第一次连接服务端时,未在inputs里        ifr ==sk:            print('accept')            conn, address =r.accept()            conn.sendall("hello".encode())            inputs.append(conn)     #添加到inputs            messages[conn]=[]          #设置messages key值r为列表        # 当客户端连接上服务端之后,再次发送数据时,已经存在inputs        else:            try:                received =r.recv(1024)                # 当正常接收客户端发送的数据时                ifnotreceived:                    raiseException("断开连接")                else:                    messages[r].append(received)                    outputs.append(r)                # 当客户端关闭程序时            exceptException as e:                    inputs.remove(r)                    delmessages[r]    forw inwriteable_list:        msg =messages[w].pop()        rest =msg +"response".encode()        w.sendall(rest)        outputs.remove(w)sk.close() | 
select.select伪并发程序客户端
| 1 2 3 4 5 6 7 8 9 10 11 12 | importsocketsk =socket.socket()sk.connect(("127.0.0.1",8002))print(sk.recv(1024).decode())whileTrue:    command =input("--->>>")    sk.sendall(command.encode())    res =sk.recv(1024)    print(res.decode())sk.close() | 
select.poll方法:
poll方法使用起来比select简单。在调用poll时,会得到一个poll对象。然后就可以使用poll的对象的register方法注册一个文件描述符(或者是带有fileno方法的对象)。注册后可以使用unregister方法移出注册的对象。注册了一些对象(比如套接字)以后,就可以调用poll方法(带有一个可选的超时时间参数)并得到一个(fd,event)格式列表(可能为空),其中fd是文件描述符,event则告诉你发生了什么。这是一个位掩码(bitmask),意思是它是一个整数,这个整数的每个位对应不同的事件。那些不同的事件是select模块的常量,为了验证是否设置了一个定位(也就是说,一个给定的事件是否发生了),可以使用按位与操作符(&):if event & select.POLLIN
select模块中的polling事件常量:
| 1 2 3 4 5 6 7 8 | 事件名                                描述POLLIN                              读取来自文件描述符的数据POLLPRT                             读取来自文件描述符的紧急数据POLLOUT                             文件描述符已经准备好数据,写入时不会发生阻塞POLLERR                             与文件描述符有关的错误情况POLLHUP                             挂起,连接丢失POLLNVAL                            无效请求,连接没有打开 | 
poll的简单程序服务端(linux)
| 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 | #poll 异步I/Oimportsocket,selects =socket.socket()host ="127.0.0.1"port =8002s.bind((host,port))fdmap ={s.fileno():s}      #文件描述符到套接字对象的映射s.listen(5)p =select.poll()           #poll对象p.register(s)               #注册一个文件描述符(带有fileno方法的对象)whileTrue:    events =p.poll()    forfd,event inevents:        iffd ==s.fileno():        #新的连接进来            c,addr =s.accept()            print("Got connectins from",addr)            p.register(c)           #注册一个文件描述符(带有fileno方法的对象)            fdmap[c.fileno()] =c   #添加到fdmap        elifevent & select.POLLIN:     #读取来自文件描述符的数据            data =fdmap[fd].recv(1024)            ifnotdata:                #表示客户端断开                print(fdmap[fd].getpeername(),"disconnected")                p.unregister(fd)        #清除文件描述符                delfdmap[fd]           #删除fdmap对应的key值            else:                print(data.decode()) | 
poll程序客户端
| 1 2 3 4 5 6 7 8 9 10 11 | #poll 异步I/Oimportsocketsk =socket.socket()sk.connect(("127.0.0.1",8002))whileTrue:    command =input("--->>>")    sk.sendall(command.encode())sk.close() | 
epoll方法:
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
一 epoll操作过程
epoll操作过程需要三个接口,分别如下:
| 1 2 3 | intepoll_create(intsize);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大intepoll_ctl(intepfd, intop, intfd, structepoll_event *event);intepoll_wait(intepfd, structepoll_event * events, intmaxevents, inttimeout); | 
1. int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。
当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数是对指定描述符fd执行op操作。
- epfd:是epoll_create()的返回值。
- op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
- fd:是需要监听的fd(文件描述符)
- epoll_event:是告诉内核需要监听什么事
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待epfd上的io事件,最多返回maxevents个事件。
参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
 
1 #_*_coding:utf-8_*_ 2 __author__ = 'Alex Li' 3 4 import socket, logging 5 import select, errno 6 7 logger = logging.getLogger("network-server") 8 9 def InitLog(): 10 logger.setLevel(logging.DEBUG) 11 12 fh = logging.FileHandler("network-server.log") 13 fh.setLevel(logging.DEBUG) 14 ch = logging.StreamHandler() 15 ch.setLevel(logging.ERROR) 16 17 formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 18 ch.setFormatter(formatter) 19 fh.setFormatter(formatter) 20 21 logger.addHandler(fh) 22 logger.addHandler(ch) 23 24 25 if __name__ == "__main__": 26 InitLog() 27 28 try: 29 # 创建 TCP socket 作为监听 socket 30 listen_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) 31 except socket.error as msg: 32 logger.error("create socket failed") 33 34 try: 35 # 设置 SO_REUSEADDR 选项 36 listen_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 37 except socket.error as msg: 38 logger.error("setsocketopt SO_REUSEADDR failed") 39 40 try: 41 # 进行 bind -- 此处未指定 ip 地址,即 bind 了全部网卡 ip 上 42 listen_fd.bind(('', 2003)) 43 except socket.error as msg: 44 logger.error("bind failed") 45 46 try: 47 # 设置 listen 的 backlog 数 48 listen_fd.listen(10) 49 except socket.error as msg: 50 logger.error(msg) 51 52 try: 53 # 创建 epoll 句柄 54 epoll_fd = select.epoll() 55 # 向 epoll 句柄中注册 监听 socket 的 可读 事件 56 epoll_fd.register(listen_fd.fileno(), select.EPOLLIN) 57 except select.error as msg: 58 logger.error(msg) 59 60 connections = {} 61 addresses = {} 62 datalist = {} 63 while True: 64 # epoll 进行 fd 扫描的地方 -- 未指定超时时间则为阻塞等待 65 epoll_list = epoll_fd.poll() 66 67 for fd, events in epoll_list: 68 # 若为监听 fd 被激活 69 if fd == listen_fd.fileno(): 70 # 进行 accept -- 获得连接上来 client 的 ip 和 port,以及 socket 句柄 71 conn, addr = listen_fd.accept() 72 logger.debug("accept connection from %s, %d, fd = %d" % (addr[0], addr[1], conn.fileno())) 73 # 将连接 socket 设置为 非阻塞 74 conn.setblocking(0) 75 # 向 epoll 句柄中注册 连接 socket 的 可读 事件 76 epoll_fd.register(conn.fileno(), select.EPOLLIN | select.EPOLLET) 77 # 将 conn 和 addr 信息分别保存起来 78 connections[conn.fileno()] = conn 79 addresses[conn.fileno()] = addr 80 elif select.EPOLLIN & events: 81 # 有 可读 事件激活 82 datas = '' 83 while True: 84 try: 85 # 从激活 fd 上 recv 10 字节数据 86 data = connections[fd].recv(10) 87 # 若当前没有接收到数据,并且之前的累计数据也没有 88 if not data and not datas: 89 # 从 epoll 句柄中移除该 连接 fd 90 epoll_fd.unregister(fd) 91 # server 侧主动关闭该 连接 fd 92 connections[fd].close() 93 logger.debug("%s, %d closed" % (addresses[fd][0], addresses[fd][1])) 94 break 95 else: 96 # 将接收到的数据拼接保存在 datas 中 97 datas += data 98 except socket.error as msg: 99 # 在 非阻塞 socket 上进行 recv 需要处理 读穿 的情况 100 # 这里实际上是利用 读穿 出 异常 的方式跳到这里进行后续处理 101 if msg.errno == errno.EAGAIN: 102 logger.debug("%s receive %s" % (fd, datas)) 103 # 将已接收数据保存起来 104 datalist[fd] = datas 105 # 更新 epoll 句柄中连接d 注册事件为 可写 106 epoll_fd.modify(fd, select.EPOLLET | select.EPOLLOUT) 107 break 108 else: 109 # 出错处理 110 epoll_fd.unregister(fd) 111 connections[fd].close() 112 logger.error(msg) 113 break 114 elif select.EPOLLHUP & events: 115 # 有 HUP 事件激活 116 epoll_fd.unregister(fd) 117 connections[fd].close() 118 logger.debug("%s, %d closed" % (addresses[fd][0], addresses[fd][1])) 119 elif select.EPOLLOUT & events: 120 # 有 可写 事件激活 121 sendLen = 0 122 # 通过 while 循环确保将 buf 中的数据全部发送出去 123 while True: 124 # 将之前收到的数据发回 client -- 通过 sendLen 来控制发送位置 125 sendLen += connections[fd].send(datalist[fd][sendLen:]) 126 # 在全部发送完毕后退出 while 循环 127 if sendLen == len(datalist[fd]): 128 break 129 # 更新 epoll 句柄中连接 fd 注册事件为 可读 130 epoll_fd.modify(fd, select.EPOLLIN | select.EPOLLET) 131 else: 132 # 其他 epoll 事件不进行处理 133 continue 134 135 epoll socket echo server 136 137 epoll socket echo server
selectors模块
selectors模块已经封装了epoll,select方法;epoll优先级大于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 | importselectorsimportsocket sel =selectors.DefaultSelector() defaccept(sock, mask):    conn, addr =sock.accept()  # Should be ready    print('accepted', conn, 'from', addr)    conn.setblocking(False)    sel.register(conn, selectors.EVENT_READ, read) defread(conn, mask):    data =conn.recv(1000)  # Should be ready    ifdata:        print('echoing', repr(data), 'to', conn)        conn.send(data)  # Hope it won't block    else:        print('closing', conn)        sel.unregister(conn)        conn.close() sock =socket.socket()sock.bind(('localhost', 10000))sock.listen(100)sock.setblocking(False)sel.register(sock, selectors.EVENT_READ, accept) whileTrue:    events =sel.select()    forkey, mask inevents:        callback =key.data        callback(key.fileobj, mask) | 
