I/O多路复用与python实现epoll过程逐步解释

I/O多路复用

I/O多路复用机制是指一个线程兼顾地同时处理多个IO流,详细来说就是在只有一个线程的i情况下,I/O多路复用允许内核中同时存在多个监听套接字(listening socket)和已连接套接字(connected socket),内核会持续监听这些套接字,一旦接收到某套接字的连接请求或数据等,就会将这个事件交给主线程去处理,不同于一个线程阻塞的监听等待一个套接字,I/O多路复用的方式实现了一个线程同时可以处理多个IO流的效果
常见的I/O多路复用方式有select/poll和epoll,它们都是I/O多路复用,但各有不同的实现方式,windows上只能使用select,MacOS上能使用select和poll而Linux上select、poll和epoll都可以使用,默认使用epoll
这里简单列举一下三者的区别,然后在后面介绍一下优势最大的epoll实现过程

  • select在监听时需要将所有要监听的文件描述符拷贝到内核让内核遍历找到相应的套接字查看是否有事件发生,没有事件发生则等待,有事件发生则找到对应的事件并返回对应的事件,即readable、writeable、exptional
  • polll在select之上做了一定的改进,取消了最多同时只能监听1024个文件描述符的限制,其他基本上没有什么改变
  • epoll在给描述符增加、删除和修改事件时的管理使用了非常高效的红黑树数据结构,红黑树的操作元素的时间复杂度为O(logN),然后改进了select的缺点,具体在实现和说明了epoll的细节之后再说明

多路复用之epoll

# server.py
# -*- coding: utf-8 -*-
from collections import defaultdict

import select
import socket
from queue import Queue
from select import (
    EPOLLIN,    # 可读/可接收消息
    EPOLLOUT,   # 可写/可发送消息
    EPOLLERR,   # 出错
    EPOLLHUP,    # 读关闭/连接中断
)

server = socket.socket()
server.bind(("localhost", 12345))
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

server.setblocking(False)

server.listen(5)

# 文件描述符FD为键,套接字SOCKET为值,建立映射关系,通过文件描述符找到对应的套接字
fd2skt = {server.fileno(): server}   # fileno()将对象的FD返回

# 保存套接字接收到的客户端发过来的消息,每个FD会拥有一个独立的队列,里面存放的消息会按先进先出的顺序被处理
message_queues: defaultdict[int, Queue] = defaultdict(Queue)    # key不存在时调用会默认有一个空的队列: message_queues[123].put("a")

epoll = select.epoll()

epoll.register(server, EPOLLIN | EPOLLERR)  # server视为一个FD,如果

while True:
    events = epoll.poll()       # poll方法还可以传入一个超时参数,设置监听消息的超时时间

    # 消息正常被接收时,会返回一个文件描述符和一个具体的事件
    for fd, event in events:
        if fd == server.fileno():   # 最开始进来是服务端的socket,开始做连接准备
            conn, addr = fd2skt[fd].accept()
            print(f"与{addr}建立连接成功")

            # 连接conn和server一样是一个套接字,这里让对epoll注册conn,之后的循环过程中会对其进行监听
            # epoll.register(conn, EPOLLIN | EPOLLERR)
            epoll.register(conn, EPOLLIN | EPOLLHUP | EPOLLERR)
            # 加入连接维护队列
            conn_fd = conn.fileno()
            fd2skt[conn_fd] = conn
        elif event & EPOLLIN:   # 轮询到可读消息
            data = fd2skt[fd].recv(1024)
            addr = fd2skt[fd].getpeername()
            if data:
                print(f"接收到{addr}发来的消息:{data.decode('utf-8')}")
                # 前面已经注册过了,只能注册一次,因此要先取消注册
                epoll.unregister(fd)
                # 客户端发送消息,服务端需要回复,因此通过register告诉epoll可回消息
                epoll.register(fd, EPOLLOUT | EPOLLHUP | EPOLLERR)
                # 将消息加入队列,等待被主线程处理即可
                message_queues[fd].put(data)
            else:   # 连接中断(客户端发送空字节串b'')
                # 如果连接一直没有响应怎么办?可以使用socket.setdefaulttimeout(int)设置recv等待回应的时长,超时时回抛出一个timeout异常,将其捕获并进行其他处理即可
                print(f"客户端{addr}已断开连接")
                # 注销事件和连接,停止监听和处理
                epoll.unregister(fd)
                # 关闭连接
                fd2skt[fd].close()
                # 从字典中移除该连接
                fd2skt.pop(fd)
        elif event & EPOLLOUT:      # 套接字可写(可发送消息)
            msg_queue = message_queues[fd]
            if msg_queue.empty():   # 这个连接的剩余消息为空
                # 取消监听(所有类型的事件),当下一次客户端发来消息时,又会变得可写(会register)
                epoll.unregister(fd)
                # 上面的unregister会将读取在内的所有事件都取消,这会让后续收到消息也无法继续处理,因此要重新注册读相关事件
                epoll.register(fd, EPOLLIN | EPOLLHUP | EPOLLERR)
                # 这里也可以使用modify一个方法代替:
                # epoll.modify(fd, EPOLLIN | EPOLLHUP | EPOLLERR)
            else:
                data = msg_queue.get()
                fd2skt[fd].send(f"你好,服务端已接收到你发送的:{data}".encode("utf-8"))
        elif event & EPOLLERR:  # 发生异常,需要注销相关事件,停止监听和处理
            addr = fd2skt[fd].getpeername()
            print(f"与客户端{addr}的连接发生错误...")
            epoll.unregister(fd)
            fd2skt[fd].close()
            fd2skt.pop(fd)
            message_queues.pop(fd)

响应测试,运行了上面的server.py之后运行下面的client.py得到服务器的返回响应

# client.py
# -*- coding:utf-8 -*-
import socket


def main():
    # ip = socket.gethostname()
    ip = "localhost"
    port = 12345
    ipaddress = (ip, port)
    sk = socket.socket()
    sk.connect(ipaddress)
    sk.send(b"Hello")
    data = sk.recv(1024)
    print(f"receive data from server: {data.decode()}")
    sk.close()


if __name__ == "__main__":
    main()

前面说到了epoll改进了select的缺点,没有具体细说,这里继续解释

  1. select每次都要将描述符从用户空间全部拷贝到内核空间去处理,实际上并不每次都需要全部拷贝,在epoll给套接字注册事件的时候拷贝到内核空间,而不是在监听的时候在拷贝,这样每个描述符只需要拷贝一次
  2. select在有事件发生时,需要遍历所有描述符,一个一个检查套接字看哪些是有事件发生的,epoll则是通过回调事件实现的事件通知,也就是说当事件触发了之后会接收到一个相应的回调告诉我什么事件发生了,显而易见这样做会节省很多开销
  3. epoll使用高效的红黑树结构存储文件描述符,可存储的数量非常多,并且操作高效,这点也不用多说
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python中的频分多路复用是通过使用select、poll或epoll等函数来实现的。这些函数允许程序同时监视多个文件描述符的可读、可写或错误事件,从而实现对多个I/O通道的高效管理。 频分多路复用的主要优点是可以在单个线程中同时处理多个I/O操作,从而提高程序的并发性能。它适用于需要同时监听多个网络连接或文件描述符的场景,常见的应用包括网络服务器、聊天程序等。 在Python中,可以使用select模块提供的select函数来实现频分多路复用。该函数接受三个参数:可读文件描述符列表、可写文件描述符列表和错误文件描述符列表,然后返回就绪的文件描述符列表。 以下是一个简单的示例代码,演示了如何使用select函数实现频分多路复用: ```python import select import socket server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.bind(('localhost', 8000)) server_socket.listen(5) inputs = [server_socket] outputs = [] while True: readable, writable, exceptional = select.select(inputs, outputs, inputs) for sock in readable: if sock is server_socket: client_socket, client_address = server_socket.accept() inputs.append(client_socket) else: data = sock.recv(1024) if data: print(data.decode()) if sock not in outputs: outputs.append(sock) else: inputs.remove(sock) if sock in outputs: outputs.remove(sock) sock.close() for sock in writable: sock.send(b'Hello, client!') outputs.remove(sock) for sock in exceptional: inputs.remove(sock) if sock in outputs: outputs.remove(sock) sock.close() ``` 在上述示例中,通过select.select函数来同时监听可读、可写和错误事件。当有新的客户端连接时,会将其加入到inputs列表中。读事件发生时,从客户端接收数据并将其输出。写事件发生时,向客户端发送数据。异常事件发生时,关闭连接。 总之,Python提供了一些函数和模块来实现频分多路复用,能够方便地处理多个I/O通道的并发操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值