
本文详解如何修复 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(),导致两个关键问题:
- time.sleep() 会完全阻塞事件循环,使异常无法被及时调度和捕获;
- 未显式等待写入完成,使得 socket 错误延迟暴露,甚至在 while True 循环中静默失败。
✅ 正确做法是:
- 始终 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"hello\n") # 推荐直接使用 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() 异常)后自动恢复连接,真正实现“失败即重试、恢复即续传”的健壮行为。










