python模块 select--IO复用
学习文档:https://docs.python.org/zh-cn/3.8/library/select.html#module-select
1、select模块
- select模块提供了对select()和poll()函数的访问,这两个函数在大多数操作系统中是可用的。
- 在Linux2.5+上可用epoll()。
- 在Solaris及其衍生版本上可用devpoll()。
- 在大多数BSD上可用kqueue()。
- 在Windows上,本模块仅适用于套接字;在其他操作系统上,本模块也适用于其他文件类型(特别地,在Unix上也适用于管道)。
- select模块不能用于常规文件,不能检测出(自上次读取文件后)文件是否有新数据写入。
- select模块内容
2、select.select()函数
1、select.select()函数说明
select.select(rlist, wlist, xlist[, timeout])
- 这是一个 Unix select() 系统调用接口。
- 前三个参数是由‘可等待对象’组成的序列(可迭代对象):可以是代表文件描述符的整数,或是方法fileno()返回的整数对象:
- rlist:等待,直到可以开始读取
- wlist:等待,直到可以开始写入
- xlist:等待“异常情况”(请参阅当前系统的手册,以获取哪些情况称为异常情况)
- 前三个参数可以是空的可迭代对象,但是否允许三个都是空的则取决于具体平台。(已知在Unix上可行但在Windows上不可行。)
- 可选参数timeout,以一个浮点数表示超时秒数。
- 当timeout被省略时,该函数将被阻塞,直到至少有一个文件描述符就绪。
- timeout为0时,表示执行轮询并且永远不会阻塞。
- 返回值是三个列表,包含已就绪对象,返回的三个列表是前三个参数的子集(分别是可读的、可写的、异常的)。当超时时间已到且没有文件描述符就绪时,返回三个空列表。
- 可等待对象,即可迭代对象中可接受的对象类型:
- Python文件对象(例如sys.stdin以及open()或os.popen()所返回的对象)。
- socket.socket()返回的套接字对象。
- 也可以自定义一个wrapper类,只要它具有适当的fileno()方法(该方法要确实返回一个文件描述符,而不能只是一个随机整数)。
- 注意:Windows上不接受文件对象,但接受套接字。在Windows上,底层的select()函数由WinSock库提供,且不处理不是源自WinSock的文件描述符。
2、select.select()函数与套接字
- 除了普通文件描述符,还可以监控套接字,因为套接字也是文件。
- 例如recv buffer中是否收到了数据,也即监控套接字的可读性,send buffer中是否满了,也即监控套接字的可写性。
示例:服务端
#!/usr/bin/env python3 from socket import * import select ss = socket(AF_INET, SOCK_STREAM) ss.bind(('127.0.0.1', 10000)) ss.listen(5) sss = [ss] #存放套接字的列表 while True: r, w, e = select.select(sss, [], []) #使用select监听套接字,在此被阻塞 for obj in r: #遍历可读的套接字 if obj is ss: #如果套接字是ss,有新的客户端来连接服务器 cs, csaddr = ss.accept() sss.append(cs) #将新的客户端套接字加入到套接字列表中 print(sss) else: #如果套接字是cs,与客户端进行通信 data = obj.recv(1024) print(data) if not data: #如果客户端传一个空数据,将断开该客户端的连接 sss.remove(obj) #将客户端套接字从列表中删除 obj.close() #关闭客户端连接 break obj.send(data) tcpSerSock.close() # 最后一行永远不会执行,它只是用来提醒读者,有这种优雅的退出方式
示例:客户端
- 可以开多个客户端同时连接服务端,每个客户端都可以与服务端进行独立的通信,互不影响。
#!/usr/bin/env python from socket import * cs = socket(AF_INET, SOCK_STREAM) cs.connect(('127.0.0.1',10000)) while True: data = input('> ') if not data: break cs.send(bytes(data, 'utf-8')) data = cs.recv(1024) if not data: break print(str(data, 'utf-8')) cs.close()
2、select.epoll()函数
1、select.epoll()函数说明
select.epoll(sizehint=-1, flags=0)
- 仅支持 Linux 2.5.44 或更高版本
- 返回一个edge poll对象,该对象可作为I/O事件的边缘触发或水平触发接口。
- sizehint指示epoll预计需要注册的事件数。它必须为正数,或为-1(使用默认值)。它仅在epoll_create1()不可用的旧系统上会被用到,其他情况下它没有任何作用(尽管仍会检查其值)。
- flags已经弃用且完全被忽略。但是,如果提供该值,则它必须是0或select.EPOLL_CLOEXEC,否则会抛出OSError异常。
- 3.4 版后已移除: flags 参数。
- epoll对象支持上下文管理器:当在with语句中使用时,新建的文件描述符会在运行至语句块结束时自动关闭。(在3.4版及之后)
1、epoll的eventmask
2、epoll的方法
3、水平触发(LT)和边缘触发(ET)
- LT(level triggered)
- 是缺省的工作方式(即epoll的默认工作方式),支持block和no-block socket。
- 只要满足条件,就触发一个事件。(只要有数据没有被获取完,内核就一直通知)
- 在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.
- 优点:当进行socket通信的时候,保证了数据的完整输出,进行IO操作的时候,如果还有数据,就会一直的通知你。
- 缺点:由于只要还有数据,内核就会不停的从内核空间转到用户空间,所有占用了大量内核资源,试想一下当有大量数据到来的时候,每次读取一个字节,这样就会不停的进行切换。内核资源的浪费严重。效率来讲也是很低的。
- ET(edge-triggered)
- 是高速工作方式,只支持no-block socket。
- 每当状态变化时,触发一个事件(内核只通知一次)
- 在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知。请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once).
- 优点:每次内核只会通知一次,大大减少了内核资源的浪费,提高效率。
- 缺点:不能保证数据的完整。不能及时的取出所有的数据。
- 应用场景: 处理大数据。使用non-block模式的socket。
2、select.epoll()函数与套接字
示例:服务端(必须运行在Linux2.5+)
#!/usr/bin/env python3 from socket import * import select, queue ss = socket(AF_INET, SOCK_STREAM) ss.bind(('127.0.0.1', 10000)) ss.listen(5) ss.setblocking(False) epoll = select.epoll() #创建epoll事件对象,后续要监控的事件添加到其中 epoll.register(ss.fileno(), select.EPOLLIN) #注册服务器监听fd到等待读事件集合 message_queues = {} #保存连接客户端消息的字典,格式为{} fd_to_socket = {ss.fileno(): ss} #文件句柄到所对应对象的字典,格式为{句柄:对象} timeout = 10 while True: events = epoll.poll(timeout) #轮询注册的事件集合,返回值为[(文件句柄,对应的事件),(...),....] if not events: print('epoll超时无活动连接,重新轮询......') continue for fd, event in events: socket = fd_to_socket[fd] if socket == ss: #如果活动socket为当前服务器socket,表示有新连接 cs, address = ss.accept() cs.setblocking(False) #新连接socket设置为非阻塞 epoll.register(cs.fileno(), select.EPOLLIN) #注册新连接fd到待读事件集合 fd_to_socket[cs.fileno()] = cs #把新连接的文件句柄以及对象保存到字典 message_queues[cs] = queue.Queue() elif event & select.EPOLLHUP: #关闭事件 epoll.unregister(fd) #在epoll中注销客户端的文件句柄 fd_to_socket[fd].close() #关闭客户端的文件句柄 del fd_to_socket[fd] #在字典中删除与已关闭客户端相关的信息 elif event & select.EPOLLIN: #可读事件 data = socket.recv(1024) #接收数据 if data: message_queues[socket].put(data) #将数据放入对应客户端的字典 epoll.modify(fd, select.EPOLLOUT) #修改读取到消息的连接到等待写事件集合(即对应客户端收到消息后,再将其fd修改并加入写事件集合) elif event & select.EPOLLOUT: #可写事件 try: msg = message_queues[socket].get_nowait() #从字典中获取对应客户端的信息 except queue.Empty: epoll.modify(fd, select.EPOLLIN) #队列为空,修改文件句柄为读事件 else: socket.send(msg) #发送数据给客户端 epoll.unregister(serversocket.fileno()) #在epoll中注销服务端文件句柄 epoll.close() #关闭epoll ss.close() #关闭服务器socket
示例:客户端
#!/usr/bin/env python3 from socket import * cs = socket(AF_INET, SOCK_STREAM) cs.connect(('192.168.248.128', 10000)) while True: data = input('> ') if not data: break cs.send(bytes(data, 'utf-8')) data = cs.recv(1024) if not data: break print(str(data, 'utf-8')) cs.close()
# #