asyncio.select() 不存在,应使用 asyncio.wait_for() 或 loop.add_reader()/add_writer() 等替代方案;推荐优先使用 asyncio.start_server() 处理多客户端。

asyncio.select() 不存在,别白费劲搜了
Python 的 asyncio 不提供 select()、epoll() 或 kqueue() 的直接封装——它自己在底层用这些机制,但不暴露给你。你写 asyncio.select(...) 会报 AttributeError,不是你环境没装好,是压根没这个函数。
真正该用的是 asyncio.wait_for() 配合 asyncio.create_connection() / asyncio.start_server(),或者更底层一点:用 loop.create_future() + loop.add_reader() / loop.add_writer() 手动注册套接字事件。
-
add_reader()对应可读事件(比如客户端发来数据、服务端 accept 到新连接) -
add_writer()对应可写事件(比如 socket.send() 曾经阻塞,现在可以写了) - 这两个方法只接受已设为非阻塞的套接字,调用前必须执行
sock.setblocking(False) - 注册后,回调函数里要主动调用
loop.remove_reader(sock),否则下次就重复触发
用 asyncio.start_server() 处理多个 TCP 客户端最省心
如果你只是想同时收发多个 TCP 连接(比如做简单代理、日志转发、设备心跳),asyncio.start_server() 是默认推荐路径。它自动帮你把每个连接包装成独立协程,不用手动管理 fd 和事件循环注册。
常见误区是以为得自己 socket.listen() 再丢进 add_reader() ——那等于绕开 asyncio 的调度器,反而容易出错。
立即学习“Python免费学习笔记(深入)”;
- 服务器启动后,每个客户端连接都会触发一次
client_connected_cb()回调,传入reader和writer对象 -
reader.read(1024)会挂起直到有数据或断连;writer.write(data)不阻塞,但要记得await writer.drain()等缓冲区清空 - 别在回调里直接
await耗时操作(如数据库查询),会卡住整个事件循环;该扔到线程池的用loop.run_in_executor()
async def handle_client(reader, writer):
addr = writer.get_extra_info('peername')
while True:
data = await reader.read(1024)
if not data:
break
writer.write(data.upper())
await writer.drain()
writer.close()
手动 add_reader() 时,fd 关闭顺序很关键
当你用 add_reader() 监听原始 socket(比如 UDP 或自定义协议),最容易崩的地方不是读逻辑,而是关闭时机不对。常见错误现象:程序退出时报 ResourceWarning: unclosed socket,或者某次回调里 recv() 报 OSError: [Errno 9] Bad file descriptor。
根本原因是:你在回调中调用了 sock.close(),但事件循环还不知道这个 fd 已失效,下一轮仍尝试 read() 它。
- 必须先
loop.remove_reader(sock),再sock.close() - 如果 socket 同时注册了读和写(比如半双工场景),两个都要 remove
- 在异常分支(如
recv()抛ConnectionResetError)里也要做同样清理,不能只在正常流程里关 - Windows 下对 UDP socket 调用
remove_reader()后再close()是安全的;Linux 下某些内核版本要求顺序严格,颠倒大概率 crash
asyncio.open_connection() 超时控制比 socket.connect() 更可靠
同步代码里用 socket.connect((host, port)) 设超时,靠的是 settimeout(),但它在 DNS 解析阶段不生效——如果域名卡住,整个 connect 就卡死。而 asyncio.open_connection() 的 timeout 参数覆盖 DNS 查询 + TCP 握手全过程。
不过要注意:这个 timeout 是给整个协程用的,不是单次重试间隔。如果你需要指数退避重连,得自己包一层。
- 显式传
ssl=True会走 TLS 握手,耗时更长,timeout 值建议比纯 TCP 大 2–3 秒 - 若目标地址是 IPv6 双栈,且系统优先查 AAAA 记录但 DNS 慢,
timeout会等完所有解析尝试才触发 - 连接成功后,
reader和writer的生命周期与 event loop 绑定,不要跨 loop 使用(比如在子线程里 await)
手动管理 fd 的边界情况太多,能用 start_server() 和 open_connection() 就别碰 add_reader();真要碰,记住三件事:非阻塞、先 remove 后 close、异常路径也清理。


















