python-IO多路复用

I/O多路复用 I/O多路复用是用于提升效率,单个进程可以同时监听多个网络连接IO
I/O是指Input/Output
I/O多路复用,通过一种机制,可以监视多个文件描述符,一旦描述符就绪(读就绪和写就绪),能通知程序进行相应的读写操作。
I/O多路复用避免阻塞在io上,原本为多进程或多线程来接收多个连接的消息变为单进程或单线程保存多个socket的状态后轮询处理.
select select是通过系统调用来监视一组由多个文件描述符组成的数组,通过调用select()返回结果,数组中就绪的文件描述符会被内核标记出来,然后进程就可以获得这些文件描述符,然后进行相应的读写操作
select的实际执行过程如下:

  1. select需要提供要监控的数组,然后由用户态拷贝到内核态
  2. 内核态线性循环监控数组,每次都需要遍历整个数组
  3. 内核发现文件描述符状态符合操作结果,将其返回
所以对于我们监控的socket都要设置为非阻塞的,只有这样才能保证不会被阻塞
优点 基本各个平台都支持
缺点
  1. 每次调用select,都需要把fd集合由用户态拷贝到内核态,在fd多的时候开销会很大
  2. 单个进程能够监控的fd数量存在最大限制,因为其使用的数据结构是数组。
  3. 每次select都是线性遍历整个数组,当fd很大的时候,遍历的开销也很大
python使用select r, w, e = select.select( rlist, wlist, errlist [,timeout] )
rlist,wlist和errlist均是waitable object; 都是文件描述符,就是一个整数,或者一个拥有返回文件描述符的函数fileno()的对象。
rlist: 等待读就绪的文件描述符数组
wlist: 等待写就绪的文件描述符数组
errlist: 等待异常的数组
在linux下这三个列表可以是空列表,但是在windows上不行
当rlist数组中的文件描述符发生可读时(调用accept或者read函数),则获取文件描述符并添加到r数组中。
当wlist数组中的文件描述符发生可写时,则获取文件描述符添加到w数组中
当errlist数组中的文件描述符发生错误时,将会将文件描述符添加到e队列中
当超时时间没有设置时,如果监听的文件描述符没有任何变化,将会一直阻塞到发生变化为止
当超时时间设置为1时,如果监听的描述符没有变化,则select会阻塞1秒,之后返回三个空列表。 如果由变化,则直接执行并返回。
3个list中可接收的参数,可以是Python的file对象,例如sys.stdin,os.open,open返回的对象等等。socket对象将会返回socket.socket(),也可以自定义类,只要由合适的fileno函数即可,前提是真实的文件名描述符
# -*- coding: utf-8 -*-import select import socket import datetimeresponse = b"Hello, World!"sock = socket.socket() # 需要设置socket选项时,需要先将socketlevel设置为SOL_SOCKETSOL=socket option level # SO_REUSEADDR代表重用地址reuse addr sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(("localhost", 10000)) sock.listen(5) sock.setblocking(0)inputs = [sock, ] while True: print(datetime.datetime.now()) rlist, wlist, errlist = select.select(inputs, [], [], 10)print(" >>> ", rlist, wlist, errlist) for s in rlist: if s == sock: con, addr = s.accept()# 将新的请求连接加入到监控列表中 inputs.append(con) else: # 对于其他的文件描述符要接收信息并返回try: data = s.recv(1024) if data: s.send(response)finally: s.close() inputs.remove(s)

poll poll本质上与select基本相同,只不过监控的最大连接数上相较于select没有了限制,因为poll使用的数据结构是链表,而select使用的是数组,数组是要初始化长度大小的,且不能改变
poll原理
  1. 将fd列表,由用户态拷贝到内核态
  2. 内核态遍历,发现fd状态变为就绪后,返回fd列表
poll状态
POLLIN 有数据读取POLLPRI 有数据紧急读取POLLOUT 准备输出:输出不会阻塞POLLERR 某些错误情况出现POLLHUP 挂起POLLNVAL 无效请求:描述无法打开

优点 跨平台使用
缺点
  1. 每次调用select,都需要把fd集合由用户态拷贝到内核态,在fd多的时候开销会很大
  2. 每次select都是线性遍历整个列表,当fd很大的时候,遍历的开销也很大
python使用poll 【python-IO多路复用】poll方法
  1. register,将要监控的文件描述符注册到poll中,并添加监控的事件类型
  2. unregister,注销文件描述符监控
  3. modify, 修改文件描述符监控事件类型
  4. poll([timeout]),轮训注册监控的文件描述符,返回元祖列表,元祖内容是一个文件描述符及监控类型(
    POLLIN,POLLOUT等等),如果设置了timeout,则会阻塞timeout秒,然后返回控列表,如果没有设置timeout 微秒,则会阻塞到有返回值为止。
# -*- coding: utf-8 -*-import select import socket import datetimesock = socket.socket() sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(("localhost", 10000)) sock.listen(5) # 设置为非阻塞 sock.setblocking(0)poll = select.poll() poll.register(sock, select.POLLIN)connections = {}while True: # 遍历被监控的文件描述符 print(datetime.datetime.now()) for fd, event in poll.poll(10000): if event == select.POLLIN: if fd == sock.fileno(): # 如果是当前的sock,则接收请求 con, addr = sock.accept() poll.register(con.fileno(), select.POLLIN) connections[con.fileno()] = con else: # 如果是监听的请求,读取其内容,并设置其为等待写监听 con = connections[fd] data = con.recv(1024) if data: print("%s accept %s" % (fd, data)) poll.modify(fd, select.POLLOUT) else: con = connections[fd] try: con.send(b"Hello, %d" % fd) print("con >>> ", con) finally: poll.unregister(con) connections.pop(fd) con.close()

epoll epoll相当于是linux内核支持的方法,而epoll主要是解决select,poll的一些缺点
  1. 数组长度限制
    解决方案:fd上限是最大可以打开文件的数目,具体数目可以查看/proc/sys/fs/file-max。一般会和内存有关
  2. 需要每次轮询将数组全部拷贝到内核态
    解决方案:每次注册事件的时候,会把fd拷贝到内核态,而不是每次poll的时候拷贝,这样就保证每个fd只需要拷贝一次。
  3. 每次遍历都需要列表线性遍历
    解决方案:不再采用遍历的方案,给每个fd指定一个回调函数,fd就绪时,调用回调函数,这个回调函数会把fd加入到就绪的fd列表中,所以epoll只需要遍历就绪的list即可。
epoll存在的两种事件模型
水平触发 level-triggered,epoll对于fd的默认事件模型就是水平触发,即监控到fd可读写时,就会触发并且返回fd,例如fd可读时,但是使用recv没有全部读取完毕,那下次还会将fd触发返回,相对而言,这个更安全一些边缘触发 edge-triggered, epoll可以对某个fd进行边缘触发,边缘触发的意思就是每次只要触发一次我就会给你返回一次,即使你处理完成一半,我也不会给你返回了,除非他下次再次发生一个事件。使用例子:epoll.register(serversocket.fileno(), select.EPOLLIN | select.EPOLLET)

python使用epoll
# -*- coding: utf-8 -*-import select import socket import datetimeEOL1 = b'\n\n' EOL2 = b'\n\r\n' response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n' response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n' response += b'Hello, world!'sock = socket.socket() sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(("localhost", 10000)) sock.listen(5) sock.setblocking(0)epoll = select.epoll() epoll.register(sock, select.EPOLLIN)# 为了针对长连接的情况,增加请求和响应操作 connections = {} requests = {} responses = {} try:while True: print(datetime.datetime.now()) events = epoll.poll(1) print(events) for fd, event in events: if fd == sock.fileno(): # 接收请求 con, addr = sock.accept() con.setblocking(0) epoll.register(con, select.EPOLLIN | select.EPOLLET) connections[con.fileno()] = con requests[con.fileno()] = b'' responses[con.fileno()] = response elif event & select.EPOLLIN: print("ssssssssssssss") con = connections[fd] requests[fd] += con.recv(1024) # 判断con是否已经完全发送完成 if EOL1 in requests[fd] or EOL2 in requests[fd]: epoll.modify(fd, select.EPOLLOUT) print('-' * 40 + '\n' + requests[fd].decode()[:-2]) elif event & select.EPOLLOUT: # 发送完成,将fd挂起 con = connections[fd] byteswritten = con.send(responses[fd]) # 将已发送内容截取,并判断是否完全发送完毕,已发送完毕,epoll挂起fd,fdshutdown responses[fd] = responses[fd][byteswritten:] if len(responses[fd]) == 0: epoll.modify(fd, 0) con.shutdown(socket.SHUT_RDWR)elif event & select.EPOLLHUP: # 处理挂起fd, epoll注销fd, 关闭socket, connections移除fd epoll.unregister(fd) connections[fd].close() del connections[fd] finally: epoll.unregister(sock) sock.close()

    推荐阅读