python IO多路复用之select
python作用域
python中无块级作用域
1 if 1 == 1: 2 name = ‘jabe‘ 3 4 print(name) 5 #可以正常输出jabe 6 7 #在python中无块级作用域 8 #在c#或者java中是不能这样使用的,提示name未定义的
python中以函数为作用域
name = ‘jabe‘ def f1(): # name = ‘a‘ def f2(): # name = ‘b‘ print(name) f2() f1() #输出 jabe
函数执行前,作用域(链)已经确定
name = ‘jabe‘ def f1(): print(name) def f2(): name = ‘ljb‘ f1() f2() 输出: jabe #=============================== name = ‘jabe‘ def f1(): print(name) def f2(): name = ‘ljb‘ return f1 ret = f2() ret() 输出: jabe
lambda函数作用域
li = [lambda :x for x in range(10)] r = li[0]() print(r) print(‘===================‘) #输出为9,上面的例子等同于下面的代码 li = [] for i in range(10): def f1(): return i li.append(f1)
#li 为列表,内部的元素为相同功能的函数
print(li[0]()) print(li[1]()) print(li[2]()) print(li[3]()) #输出: # 9 # =================== # 9 # 9 # 9 # 9
由于函数为被调用时是未被执行的,所以当循环结束时i为9,最后调用时候显示均为9
上面的函数我们稍作更改就可以打印的不为9了
li = [] for i in range(10): def f1(x=i): return x li.append(f1) #li 为列表,内部的元素为相同功能的函数 print(li[0]()) print(li[1]()) print(li[2]()) print(li[3]()) #输出: # 0 # 1 # 2 # 3
上面的例子中因为每次执行循环,将i值赋给x,因此打印的为0,1,2,3
python IO 多路复用 select poll epoll
select
select 原理
select 是通过系统调用来监视着一个由多个文件描述符(file descriptor)组成的数组,当select()返回后,数组中就绪的文件描述符会被内核修改标记位(其实就是一个整数),使得进程可以获得这些文件描述符从而进行后续的读写操作。select饰通过遍历来监视整个数组的,而且每次遍历都是线性的。
select 优点
select目前几乎在所有的平台上支持,良好跨平台性。
select 缺点
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多的时候会很大
- 单个进程能够监视的fd数量存在最大限制,在linux上默认为1024(可以通过修改宏定义或者重新编译内核的方式提升这个限制)
- 并且由于select的fd是放在数组中,并且每次都要线性遍历整个数组,当fd很多的时候,开销也很大
python select
调用select的函数为r, w, e = select.select(rlist, wlist, xlist[, timeout]),前三个参数都分别是三个列表,数组中的对象均为waitable object:均是整数的文件描述符(file descriptor)或者一个拥有返回文件描述符方法fileno()的对象;
rlist: 等待读就绪的listwlist: 等待写就绪的listerrlist: 等待“异常”的list
select方法用来监视文件描述符,如果文件描述符发生变化,则获取该描述符。1、这三个list可以是一个空的list,但是接收3个空的list是依赖于系统的(在Linux上是可以接受的,但是在window上是不可以的)。 2、当 rlist 序列中的描述符发生可读时(accetp和read),则获取发生变化的描述符并添加到 r 序列中 3、当 wlist 序列中含有描述符时,则将该序列中所有的描述符添加到 w 序列中 4、当 errlist序列中的句柄发生错误时,则将该发生错误的句柄添加到 e序列中 5、当 超时时间 未设置,则select会一直阻塞,直到监听的描述符发生变化 当 超时时间 = 1时,那么如果监听的句柄均无任何变化,则select会阻塞 1 秒,之后返回三个空列表,如果监听的描述符(fd)有变化,则直接执行。 6、在list中可以接受Ptython的的file对象(比如sys.stdin,或者会被open()和os.open()返回的object),socket object将会返回socket.socket()。也可以自定义类,只要有一个合适的fileno()的方法(需要真实返回一个文件描述符,而不是一个随机的整数)。
python中的IO操作有三种
file、socket和stdin。select可以通过监测这三种IO操作的文件句柄的变化,来感知客户端的是否接入。
#!usr/bin/env python # coding:utf-8 import socket import select ip_port=(‘127.0.0.1‘,8888) #创建一个socket实例,那这个实例的句柄就是sk sk=socket.socket() sk.bind(ip_port) sk.listen(5) sk.setblocking(False) #把句柄存入列表中 inputs=[sk,] while True: """ select检测的是inputs列表里的句柄的变动,如果句柄有变动(例如 新的客户端连进来或者 已经连进来的客户端发了消息),就会把有变动的句柄赋值给rList,因此rList同一时间只会 等于一个句柄(例如rList=sk或rList=conn) """ #三个参数是检测列表中执行过程是否有错误,有错误的就把错误信息赋值给e """ 第4个参数表示阻塞时间,意思是阻塞多少秒之后就继续向下执行。默认不填的话select是 会阻塞住的,但是如果阻塞住就变成同步阻塞模式,那就没意义了。所以一般都是要写个阻塞 时间阻塞时间让程序继续向下执行的。 """ rList,w,e = select.select(inputs,[],[],0.05) import time time.sleep(2) print ‘input:‘,inputs print ‘result‘,rList for r in rList: #如果检测到sk句柄变动,表示是有新客户端请求接入 if rList==sk: """ conn就是客户端连接的句柄,当服务端与客户端第一次连接的时候产生变动的句柄 是服务端的sk,连接创建之后如果有数据交互那么每次变动的句柄就是客户端的conn了 """ conn,address=r.accept() print address else: client_data=r.recv(1024) r.sendall(client_data)
select既然叫做IO多路复用模型,它就可以实现实现同时对多路端口访问的监听。因为select是通过句柄的变化来感知客户端接入的。那么我们就可以通过在代码中同时创建多个句柄,然后把这些句柄都丢入inputs列表交给select来进行监控。每个句柄对应不同的端口就可以了
#!usr/bin/env python # coding:utf-8 ip_port=(‘127.0.0.1‘,8888) sk=socket.socket() sk.bind(ip_port) sk.listen(5) sk.setblocking(False) ip_port1=(‘127.0.0.1‘,9999) sk1=socket.socket() sk1.bind(ip_port1) sk1.listen(5) sk1.setblocking(False) #把sk,sk1两个句柄存入列表中,select同时监控2个句柄 inputs=[sk,sk1] while True: rList,w,e = select.select(inputs,[],[],0.05) import time time.sleep(2) print ‘input:‘,inputs print ‘result‘,rList for r in rList: if rList==sk: conn,address=r.accept() print address else: client_data=r.recv(1024) r.sendall(client_data)
结合队列模块我们就可以写出完整的通过select多路复用的socket程序。这里引入队列模块的目的是为了防止消息回复错误。因为如果多个客户端同时接入,那就有可能造成本该回复给客户端1的消息被错误的回复给了客户端2。队列模块可以有效的将数据按照顺序存储和取出。避免消息存取顺序错误。
#!/usr/bin/env python # -*- coding:utf-8 -*- import socket import select import Queue ip_port = (‘127.0.0.1‘,8888) sk = socket.socket() sk.bind(ip_port) sk.listen(5) sk.setblocking(False) inputs = [sk] """ output函数用于select第二个参数,这个参数和第一个rList不同。第一个参数是inputs队里句柄有变化了才感知 第二个参数是只要output队列里有内容就会感知。 """ output = [] """ message字典用于存放文件句柄和队列内容 """ message = {} #message = { #‘c1‘:队列, #‘c2‘:队列,[b,bb,bbb] #} while True: rList,wList,e = select.select(inputs, output, inputs, 1) # 文件描述符可读,rList,一,只有变化,感知 # 文件描述符可写,wList,二,只有存在,感知 for r in rList: #如果过rList的内容有变动就证明一个新的客户端请求连接进来了 if r == sk: conn,address = r.accept() #conn就是获取的socket文件句柄 inputs.append(conn) #将字典的value值设置为队列 message[conn] = Queue.Queue() #如果rList句柄没变动,就说明客户端已经连接好。准备接收客户端发来的数据 else: client_data = r.recv(1024) #如果客户端发来的内容不为空 if client_data: # 将获取的数据追加进output列表,此时select就会感知到第二个参数有值了 output.append(r) #将文件句柄对应的客户端内容写入队列 message[r].put(client_data) else: #如果发过来的数据为空,则删除input队列里对应的客户端文件句柄。表示断开连接 inputs.remove(r) #如果select第二个参数有值,那么output的句柄就会被赋值给wList for w in wList: # 去指定队列取数据 try: #nowait()方法Queue队列获取内容的时候不在阻塞,但是如果队列里没有数据了就报错 data = message[w].get_nowait() #将队列里抓取出来的数据发还给客户端 w.sendall(data) except Queue.Empty: pass #发送完数据之后马上删除output列表里的值,不然会一直触发 output.remove(w) #删除字典里对应的值。 del message[w]
IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。
用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。
从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

阻塞与非阻塞
简单理解为需要做一件事能不能立即得到返回应答,如果不能立即获得返回,需要等待,那就阻塞了,否则就可以理解为非阻塞。详细区别如下图所示:

浙公网安备 33010602011771号