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: 等待读就绪的list
  • wlist: 等待写就绪的list
  • errlist: 等待“异常”的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)
View Code

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)
View Code

结合队列模块我们就可以写出完整的通过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]
View Code

IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。

用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。

从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

 

阻塞与非阻塞

简单理解为需要做一件事能不能立即得到返回应答,如果不能立即获得返回,需要等待,那就阻塞了,否则就可以理解为非阻塞。详细区别如下图所示:
posted @ 2016-07-12 11:37  eggtea  阅读(391)  评论(0)    收藏  举报