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()

 

#                                                                                                                   #
posted @ 2021-07-02 16:45  麦恒  阅读(167)  评论(0编辑  收藏  举报