[Python3高阶编程] - Unix 网络编程中的 I/O 多路复用入门指南02: 详解 poll
作者: andylin02
关键词: poll、I/O多路复用、Python网络编程、select.poll、非阻塞I/O、并发服务器
一、Python 中的 poll 对象
Python 的 select 模块提供了 poll() 方法,返回一个 poll 对象,用于实现 I/O 多路复用。poll 对象提供了与 C 语言 poll 系统调用类似的功能,但接口更加 Pythonic。
创建 poll 对象
import select
poll_obj = select.poll()
poll 对象的核心方法
| 方法 | 说明 |
|---|---|
register(fd[, eventmask]) | 注册一个文件描述符,eventmask 是事件掩码(默认 POLLIN | POLLPRI) |
unregister(fd) | 注销一个文件描述符 |
modify(fd, eventmask) | 修改已注册文件描述符的事件掩码 |
poll([timeout]) | 等待事件发生,返回 (fd, event) 元组列表;timeout 单位为毫秒,-1 或 None 表示阻塞 |
常用事件常量(select 模块)
| 常量 | 值 | 含义 |
|---|---|---|
select.POLLIN | 0x001 | 有数据可读 |
select.POLLPRI | 0x002 | 有紧急数据可读 |
select.POLLOUT | 0x004 | 可写 |
select.POLLERR | 0x008 | 错误(返回时自动检测) |
select.POLLHUP | 0x010 | 挂起(对端关闭,返回时自动检测) |
select.POLLNVAL | 0x020 | 无效请求(fd 未打开,返回时自动检测) |
通常,对于普通 TCP 服务,我们只需要关心 POLLIN(新连接或数据到达)和 POLLOUT(可写)。错误和挂起事件无需显式注册,poll() 返回时会自动包含。
二、Python poll 的优点
| 优点 | 说明 |
|---|---|
| 无文件描述符数量限制 | 相比 select.select 受限于 FD_SETSIZE,poll 可以处理数千个连接 |
| 效率更高 | 每次调用只需传入需要监视的 fd,内核返回时就绪列表,但 Python 层面仍需遍历,底层实现比 select 好 |
| 数据结构简单 | 使用 register/unregister 动态管理 fd,不需要像 select 那样每次都重新构建列表 |
| 跨平台性较好 | 在 Linux、BSD、macOS 上均支持,Windows 不支持 poll(需使用 select 或 asyncio) |
| 支持修改事件 | 通过 modify 方法可以动态更改 fd 的监听事件,无需先注销再注册 |
三、Python poll 的缺点
| 缺点 | 说明 |
|---|---|
| Windows 不支持 | 这是最大的局限,Windows 上没有原生 poll,程序无法跨平台(除非回退到 select) |
| 仍需遍历就绪列表 | poll 返回的是所有就绪的 fd 列表,仍需用户代码遍历处理,但比 select 的全量遍历要好一些 |
| 大并发下不如 asyncio | Python 的 asyncio 使用更现代的事件循环(如 epoll 的封装),性能更好且代码更简洁 |
四、使用 poll 的完整示例:TCP Echo 服务器
以下代码实现了一个 Echo 服务器,支持多个客户端并发连接,使用 select.poll 进行 I/O 多路复用。
服务器代码(poll_echo_server.py)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import select
import socket
import sys
HOST = '0.0.0.0'
PORT = 8888
MAX_CLIENTS = 100
def main():
# 1. 创建监听 socket
listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_sock.bind((HOST, PORT))
listen_sock.listen(10)
listen_sock.setblocking(False) # 非阻塞模式,配合 poll
print(f"Echo server listening on {HOST}:{PORT}")
# 2. 创建 poll 对象并注册监听 socket
poller = select.poll()
# POLLIN:可读事件(新连接)
poller.register(listen_sock, select.POLLIN)
# 存储每个连接的待发送数据队列
message_queues = {}
# 存储 fd -> socket 对象的映射(因为 poll 返回只给 fd,需要根据 fd 获取 socket)
fd_to_sock = {listen_sock.fileno(): listen_sock}
try:
while True:
# 3. 调用 poll,阻塞等待事件,超时 None 表示永久阻塞
events = poller.poll()
for fd, event in events:
sock = fd_to_sock.get(fd)
if sock is None:
continue
# 处理错误或挂起事件
if event & (select.POLLERR | select.POLLHUP | select.POLLNVAL):
print(f"Error/hangup on fd {fd}, closing")
_close_connection(sock, poller, fd_to_sock, message_queues)
continue
# 处理可读事件
if event & select.POLLIN:
if sock is listen_sock:
# 新连接
try:
client_sock, client_addr = sock.accept()
except socket.error:
continue
client_sock.setblocking(False)
client_fd = client_sock.fileno()
print(f"New connection from {client_addr[0]}:{client_addr[1]}, fd={client_fd}")
# 注册到 poll,只监听读事件
poller.register(client_sock, select.POLLIN)
fd_to_sock[client_fd] = client_sock
message_queues[client_fd] = []
else:
# 客户端数据到达
try:
data = sock.recv(1024)
except (socket.error, ConnectionResetError):
data = None
if data:
print(f"Received from fd {fd}: {data.decode().strip()}")
# 将数据存入发送队列,并修改为监听可写事件
message_queues[fd].append(data)
# 修改事件,添加 POLLOUT
poller.modify(sock, select.POLLIN | select.POLLOUT)
else:
# 对端关闭连接
print(f"Client fd {fd} disconnected")
_close_connection(sock, poller, fd_to_sock, message_queues)
# 处理可写事件
if event & select.POLLOUT:
queue = message_queues.get(fd)
if queue:
# 发送所有待发数据
while queue:
data = queue.pop(0)
try:
sent = sock.send(data)
# 注意:如果一次没发完,实际应该处理部分发送,本例简化
except socket.error:
_close_connection(sock, poller, fd_to_sock, message_queues)
break
if not queue:
# 没有待发数据,改回只监听读事件
poller.modify(sock, select.POLLIN)
except KeyboardInterrupt:
print("\nServer shutting down...")
finally:
for sock in fd_to_sock.values():
sock.close()
print("All sockets closed.")
def _close_connection(sock, poller, fd_to_sock, message_queues):
"""关闭连接并清理相关资源"""
fd = sock.fileno()
poller.unregister(sock)
sock.close()
if fd in fd_to_sock:
del fd_to_sock[fd]
if fd in message_queues:
del message_queues[fd]
if __name__ == '__main__':
main()
五、代码关键点解析
1. 非阻塞模式
所有 socket 都必须设置为非阻塞模式:
listen_sock.setblocking(False)
client_sock.setblocking(False)
这是因为 poll 返回时只表示 I/O 操作不会阻塞,但实际的 accept、recv、send 仍可能阻塞(例如在多线程环境下),非阻塞模式保证了事件循环的响应性。
2. 映射 fd → socket 对象
poll.poll() 返回的是 (fd, event) 元组,而不是 socket 对象本身。因此需要维护一个字典 fd_to_sock 来根据 fd 找到对应的 socket。
3. 事件修改
当有数据需要发送时,使用 poller.modify(sock, select.POLLIN | select.POLLOUT) 添加可写事件监听。数据发送完毕后,再改回只监听 POLLIN。
4. 错误处理
-
POLLERR:发生错误(如连接重置) -
POLLHUP:对端关闭连接(挂起) -
POLLNVAL:文件描述符无效
这些事件无需显式注册,poll() 返回时会自动包含在 event 中。
六、测试方法
方式1:使用 telnet
telnet 127.0.0.1 8888
方式2:使用 nc
nc 127.0.0.1 8888
方式3:多客户端并发测试
可以同时打开多个终端运行 telnet,观察服务器输出。
七、poll 与 select 在 Python 中的性能对比
| 场景 | select | poll |
|---|---|---|
| 连接数 < 100 | 性能相当 | 性能相当 |
| 连接数 100-1000 | select 受限于 FD_SETSIZE(通常 1024) | 可以支持,遍历开销尚可 |
| 连接数 > 1000 | 不适用(上限限制) | 性能开始下降,但比 select 好 |
| 海量连接(万级) | 无法支持 | 仍然可用,但建议使用 epoll |
在 Python 中,如果连接数超过几百,建议使用 select.poll 而不是 select.select。如果追求极致性能且仅在 Linux 上运行,可以使用 select.epoll。
八、跨平台注意事项
-
Linux/BSD/macOS:
select.poll可用,性能良好 -
Windows:
select.poll不可用,需要使用select.select或asyncio(它内部会根据平台选择合适的事件循环)
因此,如果代码需要跨平台,通常需要条件判断或回退到 select。
九、总结
Python 的 select.poll 是 select.select 的升级版,它突破了文件描述符数量限制,使用更灵活的事件管理接口,适合中高并发场景。掌握 poll 是理解更高级事件循环(如 epoll、asyncio)的必经之路。
对于新项目,建议直接使用 asyncio 或 selectors 模块,它们封装了底层多路复用的差异,提供更简洁的 API。
本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!
更多推荐


所有评论(0)