10.5 断线重连与传输错误处理 本节摘要:第 10 章收尾节,讲生产环境绕不开的话题——断线重连。网络不稳,连接会断(stdio 子进程崩溃、HTTP 网络中断、服务重启)。客户端要能优雅恢复,而不是「断了就崩」。本节讲清不同传输在重连语义上的差异、通用的重连模式(指数退避 + 状态恢复),以及哪些会话状态可恢复、哪些不能。读完本节,你能写出扛住网络抖动的生产级客户端。 一、为什么需要重连 生产环境,连接断开是常态: 如果客户端不处理断线,这些情况都会导致「正在跑的任务失败、用户看到错误」。生产级客户端必须能自动重连,把短暂中断变成无感恢复。
本节摘要:第 10 章收尾节,讲生产环境绕不开的话题——断线重连。网络不稳,连接会断(stdio 子进程崩溃、HTTP 网络中断、服务重启)。客户端要能优雅恢复,而不是「断了就崩」。本节讲清不同传输在重连语义上的差异、通用的重连模式(指数退避 + 状态恢复),以及哪些会话状态可恢复、哪些不能。读完本节,你能写出扛住网络抖动的生产级客户端。
生产环境,连接断开是常态:
连接断开的常见原因: - 网络抖动(HTTP 客户端,瞬时不可达) - 服务重启(部署、崩溃恢复) - 子进程崩溃(stdio 客户端) - 长时间空闲被关闭(防火墙、负载均衡) - 客户端重启
如果客户端不处理断线,这些情况都会导致「正在跑的任务失败、用户看到错误」。生产级客户端必须能自动重连,把短暂中断变成无感恢复。
不同传输,「断线」的含义与重连方式不同:
| 传输 | 断线含义 | 能否重连 | 重连方式 |
|---|---|---|---|
| stdio | 子进程崩溃/退出 | 能(重启子进程) | 重新启动子进程 |
| 流式 HTTP | 网络中断/服务重启 | 能(重新 HTTP 连接) | 重新连 URL |
| SSE | SSE 流断开 | 能(重新开流) | 重新开 GET /sse |
| 内存 | 进程内对象失效 | 否(同进程才有) | 不适用 |
stdio 与 HTTP 都能重连,但重连后的会话状态不同——这是关键差异。
重连后,要区分「哪些状态能恢复、哪些不能」:
会话状态分类 ┌─────────────────────────────────────────┐ │ 可恢复状态(重连后还在) │ │ - 服务端注册的工具/资源/提示词(定义) │ │ - 订阅(服务端记的) │ │ - 资源内容(重新读即可) │ └─────────────────────────────────────────┘ ┌─────────────────────────────────────────┐ │ 不可恢复状态(重连后丢失) │ │ - 正在执行的请求(断了就失败) │ │ - 客户端内存缓存(进程内,重启清空) │ │ - 引导填写的多轮往返(断了要重来) │ │ - 临时会话数据(取决于服务端实现) │ └─────────────────────────────────────────┘
关键认知:重连不是「无缝继续」,而是「重新建立连接后,重新获取状态」。正在执行的请求会失败(要重试),内存缓存会丢(要重建)。
⚠️ 注意:别假设重连后「一切如常」。重连后的客户端要重新 list(确认服务端能力没变)、重新 subscribe(确认订阅还在)、重试失败的请求。把重连当成「一次软重启」,而非「无感继续」。
生产级客户端用「指数退避 + 状态恢复」模式重连:
import asyncio import random async def connect_with_retry(url, max_retries=5): """带指数退避的重连。""" retry = 0 while retry < max_retries: try: client = Client(url) await client.__aenter__() # 尝试连接 return client except Exception as e: retry += 1 if retry >= max_retries: raise # 重试次数用尽,抛出 # 指数退避 + 抖动 wait = min(2 ** retry, 60) + random.random() * 2 print(f"连接失败,重试 {retry}/{max_retries},{wait:.1f}s 后重试") await asyncio.sleep(wait)
指数退避的关键:
重连成功后,要恢复状态:
async def reconnect_and_recover(url): client = await connect_with_retry(url) # 1. 重新 list(确认服务端能力没变) tools = await client.list_tools() resources = await client.list_resources() # 2. 重新 subscribe(订阅可能丢了) await client.subscribe("config://app") # 3. 重新读取需要的资源 config = await client.read_resource("config://app") # 4. 重试失败的请求(如果业务允许) # ... return client
这个「重连 → 恢复」流程是生产客户端的标准操作。
不同业务场景,重连策略不同:
| 场景 | 策略 |
|---|---|
| 短暂抖动(几秒) | 自动重连,用户无感 |
| 服务重启(几十秒) | 重连 + 显示「重连中」 |
| 长时间不可用 | 重试有限次后,告知用户「服务不可用」 |
| 关键操作中断 | 重连后提示用户「操作中断,是否重试」 |
💡 技巧:重连要对用户透明,但不掩盖真问题。短暂中断自动重连(用户无感);长时间不可用要明确告知(别让用户傻等)。设计时区分「可自动恢复」与「需用户介入」的边界。
stdio 的重连有个特殊点——子进程崩溃要重启子进程:
# stdio 重连(概念性) async def stdio_reconnect(params): # 子进程可能已崩溃,重新启动 async with stdio_client(params) as (read, write): async with Client(read, write) as client: # 新的子进程,新连接 # 注意:子进程内的状态(如内存数据)丢了 ...
stdio 重连的注意点:
最后强调:预防断线优于恢复。几个预防措施:
| 措施 | 说明 |
|---|---|
| 长连接保活 | HTTP 配 keep-alive,避免空闲断开 |
| 服务端高可用 | 多实例 + 负载均衡,单实例崩了不影响 |
| 健康检查 | 客户端定期 ping,早发现早处理 |
| 优雅关闭 | 服务端关闭前通知客户端 |
| 幂等设计 | 工具设计成幂等,重试安全 |
⚠️ 注意:重连是兜底,不是首选。如果你的服务频繁断线需要重连,根因可能在服务端稳定性或网络质量——优先解决根因,而非优化重连。重连机制是「保险」,不是「正常工作模式」。
第 10 章结束。你已经掌握客户端进阶的全部:传输接入、会话组、缓存、订阅、重连。第 11 章讲认证授权——给这套客户端连接加上 OAuth 保护。