返回列表

博客

我们如何优雅地处理网络中断

网络无时无刻不在发生故障。以下是 Tactic Remote 如何确保你在网络中断时不会丢失工作或上下文。

发布于 标签: engineering / reliability / networking
博客

继续查看该栏目下的产品与工程更新。

浏览此分类

如果你曾使用过某个远程工具,在短暂的网络波动后显示一个加载动画就再也恢复不了,你就能理解我们为什么对重连可靠性如此执着。在移动开发工具的世界里,网络中断不是边界情况——而是常态。手机在 Wi-Fi 和蜂窝网络之间切换。电梯屏蔽信号。咖啡店的网络天生就不可靠。

Tactic Remote 的重连引擎围绕一个核心原则设计:网络中断应尽可能对用户透明,在无法做到时也要优雅地告知。

状态机

我们重连逻辑的核心是一个拥有六个状态的有限状态机:

┌──────────┐     connect     ┌───────────┐
│ INITIAL  │───────────────►│ CONNECTED │◄─────────────┐
└──────────┘                 └─────┬─────┘              │
                                   │                    │
                              disconnect           reconnect
                                   │              success │
                              ┌────▼─────┐              │
                              │ UNSTABLE │──── retry ───►│
                              └────┬─────┘    (1-3s)    │
                                   │                    │
                              3 failures           ┌────┴─────┐
                                   │               │RECOVERING│
                              ┌────▼─────┐        └────┬─────┘
                              │RECONNECT │── retry ────►│
                              │  ING     │  (5-30s)     │
                              └────┬─────┘              │
                                   │                    │
                              timeout (5min)            │
                                   │                    │
                              ┌────▼─────┐              │
                              │DISCONNEC │─── manual ───┘
                              │  TED     │   reconnect
                              └──────────┘

CONNECTED:正常运行。数据自由流动。

UNSTABLE:连接尝试失败,但我们怀疑这是暂时性的。重试间隔为 1-3 秒。UI 显示一个不显眼的警告指示器,但不会中断用户操作。

RECONNECTING:多次失败表明存在真实问题。重试间隔指数增长(5 秒、10 秒、20 秒,上限 30 秒)。UI 显示清晰的"正在重连"横幅。

RECOVERING:连接已重新建立,但需要同步状态。客户端向服务器请求状态差异(delta),应用后转入 CONNECTED 状态。

DISCONNECTED:已超过重试超时时间(默认:5 分钟)。用户必须手动触发重连。这可以防止真正离线的设备无限消耗电量。

状态协调协议

重连是容易的。协调——让客户端和服务器重新同步——才是困难的部分。

当 iPhone 在中断后重新连接时,它会向服务器发送其最后已知的状态向量:最后处理的终端更新序列号、会话 ID 和时间戳。服务器据此精确计算客户端遗漏的内容。

场景 1:短暂中断(< 30 秒)。服务器的输出缓冲区仍然包含所有遗漏的更新。服务器按顺序重放,客户端依次应用。从用户视角来看,终端在不到一秒内"赶上"——他们会看到一段快速滚动的遗漏输出。

场景 2:中等时长中断(30 秒 - 5 分钟)。输出缓冲区可能已部分轮转。服务器发送部分重放,随后附加完整的终端快照。客户端应用重放数据,然后与快照进行校验以确保一致性。

场景 3:长时间中断(> 5 分钟)。服务器仅发送完整快照。客户端替换其全部状态。这是效率最低的路径,但无论遗漏了多少内容都能保证正确性。

type ReconciliationResponse = {
  method: 'replay' | 'replay_and_snapshot' | 'snapshot'
  replay?: TerminalUpdate[]
  snapshot?: TerminalSnapshot
  sessions: SessionState[]
  pendingApprovals: ApprovalRequest[]
}

注意协调响应还包含 pendingApprovals。如果 Claude Code 在手机离线期间请求了审批,该请求会作为重连的一部分被送达。这一点至关重要——否则 Claude Code 会一直等待用户从未看到的审批请求。

差点搞垮我们的边界情况

审批过程中断连

用户收到审批通知,点击"批准",然后在审批到达服务器之前失去连接。操作会执行吗?

我们的方案:审批使用确认协议。服务器不会执行被批准的操作,直到收到客户端的审批消息并且向客户端确认收到。如果连接在审批过程中断开,审批会在重连时重新发送。重复的审批是幂等的——发送两次"批准请求 #47"与发送一次效果相同。

离线期间会话结束

你的 Claude Code 任务在手机离线时完成了。当你重新连接时,会话处于已终止状态。客户端应该显示最终输出?完成通知?还是两者都显示?

我们通过在协调响应中包含 sessionEndState 来处理此问题。客户端渲染最终的终端状态,并显示"任务在离线期间完成"的横幅以及事件摘要。用户可以立即看到结果,而不是盯着过时的进行中输出。

Mac 重启的情况

最具挑战性的场景:你的 Mac 在手机离线时重启了(由于系统更新、断电或崩溃)。当手机重新连接时,服务器已经没有之前会话的记录。

我们的方案:Mac 伴侣应用将会话元数据(会话 ID、项目路径、最后已知状态)持久化到磁盘。重启后,它读取这些元数据,并尽可能重新挂接到存活的 tmux 会话。协调协议通过服务器纪元计数器检测到服务器重启,随即启动完整的状态重建,而非尝试增量协调。

性能指标

我们从三个维度衡量重连性能:

视觉恢复时间——从网络恢复到用户看到当前终端状态的时间。本地网络的 P95 为 1.8 秒,Cloudflare 隧道为 3.2 秒。

数据效率——协调过程传输的数据量与完整刷新的对比。对于 60 秒以内的中断,我们的协调协议比完整刷新减少 85% 的数据传输。

正确性——对状态不一致零容忍。我们运行持续集成测试,模拟数千次断连/重连循环(使用随机化的时序),并逐字节验证最终的客户端状态与服务器状态完全一致。

对开发者的启示

如果你正在构建任何类型的实时移动应用,我们的经验总结出三条原则:

  1. 从第一天起就为断连而设计。 在一个为"始终在线"设计的系统上添加重连逻辑,其难度比将间歇性连接作为一等公民来构建要高出几个数量级。

  2. 幂等性是你最好的朋友。 每条客户端到服务器的消息都应该可以安全地重放。仅这一个特性就能消除整整一类的重连 bug。

  3. 用真实网络测试,而非模拟器。 网络状况模拟器无法复现真实世界故障的全部多样性——部分包传递、DNS 解析延迟、TLS 握手超时。我们在实际的蜂窝网络、电梯里和拥挤的会议 Wi-Fi 上进行测试。

可靠的重连不是光鲜的工作。当它正常运作时,用户不会称赞它——他们只是感觉不到中断。但当它失败时,信任瞬间崩塌。我们认为这项投入是值得的,而用户愿意将 Tactic Remote 用于生产工作流,也说明他们认同这一点。

立即体验 Tactic Remote

用手机远程控制编程 Agent

连接 Mac、Windows 或 Linux 电脑上的 Claude Code、Codex 等 Agent,在 iPhone 或 iPad 上查看进展并发送下一条要求。