4. 断点续传基本原理
在车云消息可靠传输体系中,断点续传解决的是“连接中断后如何不重不漏地继续传输”这一核心问题。车端与云端之间的链路会经历隧道、弱网、基站切换、休眠唤醒等复杂场景,TCP 长连接随时可能断开。如果没有断点续传机制,每次重连都从零开始,既浪费流量又造成消息重复或丢失。本节从断点定义、续传触发条件、状态保存与恢复、幂等性设计四个维度,讲清断点续传的底层原理。
一、断点的定义:不是“断线位置”,而是“确认边界”
很多初学者把断点理解为“最后一条发送失败的消息位置”,这是不准确的。在可靠传输中,断点的本质是已确认消息的连续边界。也就是说,断点之前的所有消息,云端都已经持久化并返回了确认;断点之后的消息,要么未发送,要么发送了但未收到确认,属于“不确定状态”。
因此断点通常用一个单调递增的序列号(seq)或偏移量(offset)来表示。车端维护 sendSeq,云端维护 ackSeq,断点即 ackSeq 所指向的位置。只有被确认的连续序列才能推进断点,出现空洞时必须等待补齐,否则断点会“跳跃”,导致中间消息永久丢失。
二、续传触发条件:什么时候该续传
续传不是随时都触发,需要明确的判断条件,否则会造成无谓的重传和状态混乱。常见触发条件包括:
- 连接重建成功:TCP/MQTT 重连并完成鉴权后,车端主动发起续传协商。
- 收到云端断点查询响应:云端返回
ackSeq,车端据此判断是否需要补发。 - 本地存在未确认消息:发送队列中仍有
seq > ackSeq的消息。 - 会话未过期:断点状态仍在有效期内,未因超时被清理。
- 网络质量满足阈值:RSSI、丢包率恢复到可传输水平,避免刚续传又断。
反之,如果云端返回的 ackSeq 已经等于车端 sendSeq,说明没有遗漏,无需续传;如果会话已过期,则应放弃旧断点,开启新会话并做全量或快照同步。
三、状态保存与恢复:断点存在哪里
断点状态必须双端持久化,且要能经受进程重启、掉电、休眠。车端通常把 sendSeq、未确认消息队列、会话 ID 写入本地持久化存储(如 SQLite、文件或 NVRAM);云端把 ackSeq、消息去重表、会话元数据写入数据库或分布式缓存。
恢复流程一般是:重连 → 云端下发 ackSeq → 车端对比本地 sendSeq → 从 ackSeq+1 开始补发 → 云端逐条确认并推进 ackSeq。这里的关键是确认要单调、连续,不能因为乱序到达就跳跃确认。
四、幂等性设计:重复不可避免,重复必须无害
断点续传中,消息重复几乎无法避免:车端发出消息后连接断开,未收到确认,重连后会重发;云端可能已经处理过这条消息。因此必须通过幂等性设计,让“重复到达”不产生副作用。
常见做法是为每条消息分配全局唯一的 msgId,云端维护去重表,处理前先查 msgId 是否已存在,存在则直接返回成功确认,不再执行业务逻辑。对于有状态业务,还可以使用版本号或状态机约束,例如“只有当前状态允许时才执行迁移”。
// 云端幂等处理伪代码
if (dedupTable.contains(msg.msgId)) {
return ACK(msg.seq); // 已处理,直接确认
}
dedupTable.add(msg.msgId);
businessHandler.process(msg);
ackSeq = max(ackSeq, msg.seq); // 连续确认
return ACK(msg.seq);
五、典型场景与常见误区
以车机上报行车数据为例:车辆进入隧道前已发送到 seq=105,隧道内断网,106~110 积压在本地队列。出隧道重连后,云端返回 ackSeq=105,车端补发 106~110,云端逐条确认并推进到 110,断点续传完成。整个过程用户无感知,数据不丢不重。
| 误区 | 后果 | 正确做法 |
|---|---|---|
| 断点只存内存 | 重启后断点丢失,全量重传 | 持久化到本地存储 |
| 乱序到达就跳跃确认 | 中间消息永久丢失 | 只推进连续确认边界 |
| 没有 msgId 去重 | 重复入库、重复计费 | 云端幂等表去重 |
| 会话永不过期 | 断点状态无限膨胀 | 设置 TTL 并定期清理 |
| 重连即全量重传 | 流量浪费、云端压力大 | 先协商 ackSeq 再补发 |
总结来看,断点续传的四个支柱缺一不可:断点定义保证“知道从哪继续”,触发条件保证“该续时才续”,状态保存与恢复保证“重启后还能续”,幂等性设计保证“重复了也不出错”。四者配合,才能在车云弱网、断连、休眠等真实工况下实现消息的不丢不重。