Python 的 selectors 模块提供了跨平台的异步 I/O 多路复用支持,它封装了操作系统底层的 select、poll 和 epoll 等机制,使得程序可以同时监听多个文件描述符(如套接字),并在它们中的任何一个变为可读或可写时得到通知,而不是轮询检查每个描述符的状态。
下面是一个使用 selectors 库的基本示例,展示如何创建一个简单的服务器,能够处理多个客户端连接:
import selectors
import socket
# 创建一个默认的选择器对象,根据系统自动选择最佳的实现(epoll, kqueue, select等)
sel = selectors.DefaultSelector()
# 监听端口并绑定到服务器套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('localhost', 12345)) # 绑定本地主机和端口号
server_socket.listen() # 开始监听连接请求
server_socket.setblocking(False) # 设置为非阻塞模式
# 注册监听事件到选择器
sel.register(server_socket, selectors.EVENT_READ, data=None)
def accept(sock):
conn, addr = sock.accept() # 接受新的连接
print(f'Accepted connection from {addr}')
conn.setblocking(False) # 新连接也设置为非阻塞模式
# 可以注册新的连接到选择器,监听它的读写事件
sel.register(conn, selectors.EVENT_READ, data=conn)
def service_connection(key, mask):
sock = key.fileobj
if mask & selectors.EVENT_READ:
# 如果是读事件,则尝试接收数据并处理
data = sock.recv(1024)
if data:
print(f'Received: {data!r}')
# 发送响应
sock.sendall(data.upper())
else:
# 没有收到数据,可能客户端已断开连接,关闭套接字
sel.unregister(sock)
sock.close()
print('Closing connection')
while True:
events = sel.select(timeout=None) # 阻塞等待事件发生
for key, mask in events:
callback = {
None: accept,
selectors.EVENT_READ: service_connection,
}.get(key.data, lambda *args: None)
callback(key, mask) # 调用相应的回调函数处理事件
sel.select(timeout=None): 这行代码调用 selectors 对象的 select() 方法。这个方法会阻塞等待直到有至少一个已注册的文件描述符(如套接字)准备好进行 I/O 操作。参数 timeout=None 表示如果没有事件发生,则无限期等待。
events = sel.select(timeout=None): 当有文件描述符准备好进行读、写或其他操作时,select() 方法返回一个包含所有就绪事件的列表。每个事件是一个元组 (key, events_mask),其中 key 是一个与事件关联的 SelectKey 对象,包含了就绪的文件描述符以及你之前注册时设置的 data;events_mask 是一个位掩码,表示该文件描述符上发生了哪些类型的事件(例如,selectors.EVENT_READ 表示可读事件)。
for key, mask in events:: 遍历 select() 返回的所有就绪事件。
callback = {…}.get(key.data, lambda *args: None): 根据 key.data 的值来获取相应的回调函数。在这个例子中,我们通过一个字典映射将不同的数据值关联到不同的处理函数。当 key.data 为 None 时,对应的回调函数是 accept;当 key.data 为 selectors.EVENT_READ 时,回调函数是 service_connection。如果 key.data 不匹配任何键,则使用默认的空 lambda 函数作为回调。
callback(key, mask): 调用前面根据 key.data 获取到的回调函数,并将当前的 key 和 mask 作为参数传递给它。这样就能根据不同事件类型和相关的文件描述符执行正确的处理逻辑。例如,如果是服务器套接字准备接受新连接,那么就会调用 accept 函数;如果是客户端连接准备进行读取操作,则调用 service_connection 函数处理来自客户端的数据。
上述代码中,我们首先创建了一个服务器套接字并将其设置为非阻塞模式,然后将该套接字注册到选择器上监听读事件(即新的连接请求)。当有新的连接建立时,accept 函数会被调用来接受连接,并将新连接也注册到选择器上监听读事件。service_connection 函数则用于处理已有连接的数据读取与发送。
sel.select() 方法会阻塞直到有至少一个已注册的文件描述符准备好进行I/O操作,或者超时(这里没有设置超时,因此会一直等待直到有事件发生)。当有事件发生时,通过遍历 select() 返回的事件列表,并根据对应的回调函数来处理这些事件。