文章目录
- [用 UDP 做一条"可靠"的控制通道](#用 UDP 做一条"可靠"的控制通道)
-
- 一、先说结论
- [二、UDP 什么都不保证](#二、UDP 什么都不保证)
- 三、六种机制
-
- [3.1 TTL:让陈旧指令自己失效](#3.1 TTL:让陈旧指令自己失效)
- [3.2 序号 + 防重放:拒绝重复和伪造](#3.2 序号 + 防重放:拒绝重复和伪造)
- [3.3 MAC:拒绝篡改](#3.3 MAC:拒绝篡改)
- [3.4 双向心跳 + 超时:判断"对方还在吗"](#3.4 双向心跳 + 超时:判断"对方还在吗")
- [3.5 租约:判断"谁有资格控制"](#3.5 租约:判断"谁有资格控制")
- [3.6 请求-应答 + 重发:让离散命令有结果](#3.6 请求-应答 + 重发:让离散命令有结果)
- 四、三层状态机
-
- [4.1 连接状态](#4.1 连接状态)
- [4.2 授权状态](#4.2 授权状态)
- [4.3 恢复流程](#4.3 恢复流程)
- 五、四条工程纪律
-
- [纪律 1:所有在途操作必须有出口](#纪律 1:所有在途操作必须有出口)
- [纪律 2:超时用时间戳,不数次数](#纪律 2:超时用时间戳,不数次数)
- [纪律 3:状态变化才通知](#纪律 3:状态变化才通知)
- [纪律 4:区分"状态"和"事实"](#纪律 4:区分"状态"和"事实")
- [六、为什么不用 TCP](#六、为什么不用 TCP)
- 七、这套东西的成本
- 八、小结
用 UDP 做一条"可靠"的控制通道
一、先说结论
用 UDP 做控制通道,目标不是"每一帧都不丢",而是:
任何一次操作,最终都必须有一个确定的结果------成功、被拒、或者超时失败。
这两件事看起来像,其实完全不同。TCP 追求"不丢",UDP 控制通道追求"不留悬案"。
因为对实时控制来说,一个迟到的成功,等于失败。TCP 的重传会把 200ms 前的指令补送到对端,而那时现场情况已经变了。
所以行业里的做法是:用 UDP,然后用应用层把"可靠性"重新定义成安全性和确定性。
二、UDP 什么都不保证
| 保证 | UDP |
|---|---|
| 送达 | ❌ |
| 顺序 | ❌ |
| 不重复 | ❌ |
| 完整(不篡改) | ❌ |
| 对端在线 | ❌ |
| 对端是本人 | ❌ |
每一条都会在控制场景里变成具体的事故:
- 丢了 → 指令没执行,但你不知道
- 乱序 → 旧指令后到,覆盖了新指令
- 重复 → 同一条指令执行两次
- 篡改 → 收到伪造指令
- 对端不在 → 你还在发,以为在控
下面六种机制,一一对上这六个问题。
三、六种机制
3.1 TTL:让陈旧指令自己失效
每帧指令带一个"有效期"(比如 200ms)。对端收到后只在有效期内执行,过期就忽略。
解决乱序:晚到 3 秒的旧指令不再有效,不会覆盖当前值。
同时带来一个副作用(也是好处) :发送方必须持续发。你不发,对端就自动进入安全状态。"不发电=停"比"发停止帧=停"更可靠------因为后者也可能丢。
3.2 序号 + 防重放:拒绝重复和伪造
每帧带一个单调递增的序号。对端记录"见过的最大序号",小于等于它的一律丢弃。
解决重复和重放攻击。
如果两端都维护会话序号,还要处理"发送方重启后序号归零"------通常用会话密钥重新协商来解决。
3.3 MAC:拒绝篡改
每帧附一个完整性标签(MAC),用共享密钥计算。
解决篡改。 没有密钥的人可以伪造包,但对端一验就丢。
这也意味着:协议本身不需要 CRC。CRC 只能防传输误码,防不了恶意构造;MAC 两者都能防。很多新协议因此把 CRC 去掉了。
3.4 双向心跳 + 超时:判断"对方还在吗"
两个方向都要有:
- 设备 → 上位机:上位机知道设备活着,可以显示"已连接"
- 上位机 → 设备:设备知道上位机活着,否则进入失效保护
只有单向心跳是不够的。 典型的坑是:上位机崩溃了,设备还在等指令;或者设备重启了,上位机还以为连着。
3.5 租约:判断"谁有资格控制"
如果系统可能被多个来源控制(多台上位机、机上自动驾驶、遥控器),光有"连上了"不够,还要知道"现在谁说了算"。
租约是一个限期授权:
上位机:我要控制权,租 1000ms
设备: 批准(1000ms 内有效)
上位机:每 500ms 续租一次
设备: 你没续 → 到期收回 → 拒绝你的控制帧
为什么用租约而不是"登录/登出" :因为登出消息会丢。上位机崩溃时不可能发登出,租约到期自动失效是唯一不依赖对端配合的回收方式。
3.6 请求-应答 + 重发:让离散命令有结果
连续指令(速度、位置)可以靠 TTL 自愈。但离散命令(上电、切换模式、返航)不能丢------丢一次就是没执行。
做法:
发送 → 带 request_id → 等应答 → 收到或超时重发(同一个 id)→ 超过次数判失败
三个要点:
- request_id 必须沿用,不能每次重发都换新号,否则迟到的应答对不上
- 过期应答要能识别:id 不匹配就丢,不能当成新命令的应答
- 超时必须报失败,不能静默------操作员点了按钮,必须看到结果
四、三层状态机
把上面这些机制组织起来,需要一个状态机(通常不止一个)。
4.1 连接状态
正在连接 ──收到数据──→ 已连接 ──超时──→ 中断
│ │
└────超时────→ 中断 └──收到数据──→ 已连接
注意最后一条:中断能自动恢复。因为中断只是"界面上的判断",socket 并没有真的断开------数据一回来就该恢复。
实现上的关键就一行:
cpp
if (state != Live) { // 而不是 if (state == Connecting)
state = Live;
}
写 != 就顺带支持了重连,写 == 就只支持首次连接。 一个符号的差别。
4.2 授权状态
不可用 ──心跳就绪──→ 空闲 ──有操作──→ 申请中 ──批准──→ 已授权
↑ │ │
└──冷却+松杆──────── 被拒 ←────────┘
租约到期
"被拒后必须松杆 + 冷却" 这条限制很重要。否则操作员一直推着杆,系统就会无限重发申请,既刷爆网络,又可能去抢别人的控制权。
4.3 恢复流程
设备重启后,上一次的远程控制状态可能还留在设备端。所以新连接第一件事是"复位"------先把设备拉回中立状态,再申请控制权。
这个"先复位再接管"的流程,是很多人第一次做远程控制会漏掉的。
五、四条工程纪律
机制是死的,纪律是活的。这四条决定了实现是"看起来对"还是"真的对"。
纪律 1:所有在途操作必须有出口
一个请求发出去,最终只有四种结局:
接受 / 拒绝 / 本地超时 / 会话复位
每一种都要通知上层。
最容易漏的是后两种。代码里常见的写法:
cpp
if (timeout) {
// 清掉状态,继续
}
状态清了,但 UI 上那个"启动中..."永远不消失。操作员只能重连。
纪律 2:超时用时间戳,不数次数
cpp
// ✅
if (now - lastSent >= 800ms) { resend(); lastSent = now; }
// ❌
if (++counter % 40 == 0) { resend(); }
定时器从来不准。40 次 × 21ms = 840ms,误差会累积。而且某次定时器被拖了 100ms,计数器完全不知道。
用时间戳的好处:抖动不累积,卡顿能自动追上。
纪律 3:状态变化才通知
50Hz 的节拍器每轮都可能"检查"到同一件事。如果每次都通知 UI,界面就废了。
cpp
if (phase == lastPhase) return;
lastPhase = phase;
notify(phase);
纪律 4:区分"状态"和"事实"
本地状态可能滞后于真实世界。
例:本地认为"已授权",但设备那边租约已经过期了。
所以释放资源时,看的是"我可能还持有"这个事实标志,而不是"我现在是不是已授权"状态:
cpp
if (leaseOutstanding) { // 事实:我可能还持有
sendRelease();
leaseOutstanding = false;
}
否则会出现"设备端租约还没过期,本地已经复位成未授权,于是不发释放帧,设备要空等一个租期"。
六、为什么不用 TCP
| 维度 | TCP | UDP + 应用层 |
|---|---|---|
| 丢包 | 无限重传,延迟不可控 | 丢弃,等下一帧 |
| 队头阻塞 | 有一个包丢了,后面的都等 | 无 |
| 连接状态 | 内核维护,断线感知慢 | 自己控制,心跳判活快 |
| 多播/广播 | 不支持 | 支持 |
| 迟到的数据 | 照常送达 | 可以直接丢 |
| 实现复杂度 | 低 | 高(这是唯一劣势) |
核心问题:TCP 的可靠性语义是"最终送达",而控制需要的是"及时有效"。这两个目标在丢包时直接冲突------TCP 选择补送,控制需要放弃。
七、这套东西的成本
诚实地说,用 UDP 做控制通道,成本主要在状态管理上:
- 三到四个状态机
- 二十多个"在途/待确认/待重发"标志
- 每个都要处理超时、复位、被取代、被拒绝
一个成熟的实现通常在 1000~2000 行左右,而且边界条件比主流程长。
判断值不值得:如果丢一帧的后果是"设备继续按旧指令运行",那就值得。 如果只是"界面数字跳一下",用 TCP 更省事。
八、小结
用 UDP 保证"稳定",不是让它不丢包,而是让它:
- 丢包时安全 ------ TTL 让旧指令失效
- 有结果 ------ 请求-应答 + 超时失败
- 能判活 ------ 双向心跳
- 不打架 ------ 租约
- 不被利用 ------ 序号 + MAC
- 人出事也能停 ------ 输入超时失效保护
一句话:不是"保证送达",是"保证不留悬案"。