
本文详解如何修复 asyncio telnet 客户端中 `socket.send() raised exception` 未被捕获的问题,通过正确使用 `await writer.drain()`、替换阻塞式 `time.sleep` 为 `asyncio.sleep`,并构建健壮的无限重连循环。
在基于 asyncio.open_connection 构建的异步网络客户端中,一个常见误区是认为 writer.write() 是同步完成的——实际上它只是将数据写入内存缓冲区,并不保证已发送至对端。当底层连接异常中断(如网络闪断、服务端崩溃或防火墙拦截)时,后续调用 writer.write() 可能不会立即抛出异常;真正触发错误的往往是 缓冲区刷新阶段,即 writer.drain() 或下一次 write() 尝试复用已失效连接时。
原始代码中缺失 await writer.drain(),且混用阻塞式 time.sleep(),导致两个关键问题:
✅ 正确做法是:
- 始终 await writer.drain() 后再进入下一轮发送:确保写操作完成或明确失败;
- 用 await asyncio.sleep() 替代 time.sleep():保持事件循环活跃,允许异常传播与重连逻辑执行;
- 将 except 范围覆盖整个通信内层循环:捕获连接建立、写入、drain 等任意环节异常;
- 在异常后加入退避等待:避免密集重连冲击服务端或耗尽系统资源。
以下是优化后的完整可运行示例:
import asyncio valueTime = 3 # 重试/发送间隔(秒) async def telnet_client(host: str, port: int) -> None: while True: try: reader, writer = await asyncio.open_connection(host, port) print(f"✅ Connected to ({host}, {port})") while True: writer.write(b"hellon") # 推荐直接使用 bytes 字面量 await writer.drain() # 关键:等待数据实际发出或失败 print("? Data sent") await asyncio.sleep(valueTime) except (ConnectionRefusedError, OSError, asyncio.IncompleteReadError, BrokenPipeError) as e: print(f"⚠️ Connection error: {type(e).__name__} - {e}") print("? Attempting reconnection...") await asyncio.sleep(valueTime) # 退避等待后重试 except Exception as e: # 捕获其他未预期异常(如 DNS 解析失败等) print(f"❌ Unexpected error: {type(e).__name__} - {e}") await asyncio.sleep(valueTime) if __name__ == "__main__": try: asyncio.run(telnet_client("192.168.1.126", 23)) except KeyboardInterrupt: print("n? Client stopped by user.") except Exception as e: print(f"? Fatal error in main loop: {e}")
? 关键注意事项:
- writer.write() 是无等待的缓冲写入,必须配对 await writer.drain() 才能感知 I/O 级错误;
- 不要忽略 BrokenPipeError 和 OSError —— 它们常在连接已关闭时由 drain() 抛出;
- 若需更精细控制(如指数退避、最大重试次数、连接超时),可封装 asyncio.wait_for(asyncio.open_connection(…), timeout=5.0);
- 生产环境建议添加日志模块(如 Logging)替代 print,便于追踪与告警;
- 对于真实 Telnet 协议交互,还需处理 IAC(Interpret As Command)协商、选项协商等,本例聚焦连接可靠性基础。
通过以上改造,客户端能在任意通信故障(包括 socket.send() 异常)后自动恢复连接,真正实现“失败即重试、恢复即续传”的健壮行为。