协议分层、契约与状态机¶
协议不是一串能够被解析的字节,而是一组关于方向、时间、状态、错误和演进的共同约定。只记录帧头和字段长度,只能实现“正常样本可以解码”,不能保证异常输入下仍然安全。

先分清六个对象¶
| 对象 | 回答的问题 | 示例 |
|---|---|---|
| 连接器 | 导线怎样接入 | JST插座、焊盘、USB连接器 |
| 电气接口 | 电压和信号怎样变化 | 3.3伏UART、CAN差分信号 |
| 成帧规则 | 一帧从哪里开始、在哪里结束 | 固定长度、长度字段、间隔 |
| 消息协议 | 字节代表什么 | MAVLink消息、MSP命令 |
| 功能角色 | 为什么交换数据 | 遥控输入、参数配置、遥测 |
| 安全策略 | 谁在什么状态下可以做什么 | 解锁拒绝、签名校验、限速 |
同一个连接器可以暴露多个接口;同一个接口可以承载多个协议;同一个协议也可以承载不同权限的功能。协议审查必须逐层记录,不能从“插头相同”推导兼容。
最小协议契约¶
每类消息至少定义:
| 类别 | 必须说明 |
|---|---|
| 身份 | 消息ID、发送者、目标、协议和定义版本 |
| 结构 | 帧头、长度、载荷、校验、端序和对齐 |
| 数据 | 类型、单位、比例、坐标系、枚举和保留值 |
| 时间 | 产生时间、发送时间、有效期、超时和时钟基准 |
| 顺序 | 序号、重复、乱序、丢失和重放处理 |
| 状态 | 请求前提、允许状态、转换、完成和回滚 |
| 错误 | 非法长度、未知消息、校验失败和资源耗尽 |
| 安全 | 身份、完整性、机密性、授权和审计 |
| 演进 | 版本发现、扩展规则、弃用和降级 |
“三个32位浮点数”不是完整契约。接收端仍不知道它们是角度还是角速度、采用弧度还是度、属于哪个坐标系、何时采样、是否有效。
发送成功不等于动作完成¶
命令常经历:
“写入串口成功”最多证明数据进入本地输出路径。没有对端确认时,不能证明帧已收到;收到确认也不一定证明动作已经完成。协议必须区分:
- 已接受传输;
- 已收到帧;
- 已接受命令;
- 正在执行;
- 已完成;
- 被拒绝;
- 超时但最终状态未知。
超时不是失败的同义词。若请求已经到达而响应丢失,盲目重试可能重复执行一次性动作。
用状态机描述会话¶
以参数更新为例:
任一步都需要定义:
- 允许进入的前置状态;
- 超时;
- 可重试性;
- 幂等键或事务标识;
- 错误响应;
- 断线后的恢复;
- 是否需要回滚。
如果写入会改变端口本身,应用后连接可能立刻中断。工具应提前知道这是预期切换还是异常丢线,并通过新端口回读确认。
数据有效性与新鲜度¶
接收方不能只缓存“最后一个值”。每项数据还需要:
- 数据有效标志;
- 采样或产生时间;
- 接收时间;
- 最大允许年龄;
- 来源实例;
- 序列或更新计数;
- 失效后的替代策略。
例如,遥控通道数值保持在中位不代表遥控链路正常;位置数值没有变化也可能是静止、冻结或数据源失效。协议必须让应用区分这些状态。
长度与边界¶
解析器应先验证最外层约束,再读取内部字段:
- 输入缓冲区至少包含固定头;
- 版本和帧类型受支持;
- 声明长度不小于最小值;
- 声明长度不大于协议和本地缓冲上限;
- 完整帧已经到达;
- 校验通过;
- 消息自身长度符合定义;
- 字段值和组合约束有效。
长度字段的含义必须精确说明。它可能表示:
- 仅载荷长度;
- 从类型字段到校验字段的长度;
- 整个帧长度;
- 不含固定头但包含尾部;
- 以字节、字或其他单位计数。
使用错误的长度定义会导致越界、粘帧和错误重同步。
未知和扩展¶
可演进协议通常要保留:
- 未知消息的跳过规则;
- 未知标志位的处理;
- 扩展字段的追加规则;
- 保留字段必须为零或必须忽略的要求;
- 不兼容扩展的明确拒绝。
安全默认并不总是“全部拒绝”。如果规范声明未知兼容标志可忽略,接收端应继续处理基础部分;如果未知不兼容标志会改变帧解释,则必须丢弃。实现应按协议定义处理,不能用一个全局规则替代。
校验、认证和加密¶
三者解决不同问题:
| 能力 | 主要目的 | 不能自动提供 |
|---|---|---|
| 校验和或CRC | 检测传输错误 | 发送者身份、防恶意篡改、机密性 |
| 消息认证码或数字签名 | 验证持有密钥者和消息完整性 | 载荷保密 |
| 加密 | 隐藏载荷内容 | 完整性、身份或防重放,除非模式同时提供 |
循环冗余校验(CRC,Cyclic Redundancy Check)不是安全认证。攻击者能够重新计算普通CRC时,仍可构造格式正确的恶意帧。
端序、位序和对齐¶
至少分别说明:
- 多字节整数采用大端还是小端;
- 位字段的最低有效位对应什么;
- 位流是先发送最高位还是最低位;
- 结构体是否允许填充字节;
- 浮点格式;
- 未对齐访问是否允许。
不能直接把网络字节复制到编译器结构体后强制转换。编译器对齐、主机端序和未定义行为都可能破坏可移植性。应使用显式读写函数或由协议生成器产生的序列化代码。
资源和拒绝服务边界¶
即使每帧都合法,高速输入仍可能耗尽:
- 接收环形缓冲;
- 解码队列;
- 命令队列;
- 日志带宽;
- 主循环时间;
- 动态内存;
- 响应带宽。
协议实现应设置:
- 最大帧长;
- 每连接和每消息速率;
- 队列深度;
- 低优先级丢弃策略;
- 高风险命令并发限制;
- 解析预算;
- 错误日志限速。
错误日志本身也可能放大拒绝服务。连续错误帧应累计计数并限速报告。
高风险命令门禁¶
以下操作不能只凭“命令ID正确”执行:
- 解锁或直接执行器控制;
- 修改输出映射和失效保护;
- 写入任务和地理围栏;
- 重启、进入引导加载器或刷写;
- 删除日志、配置或密钥;
- 修改网络、串口或认证策略。
至少检查发送来源、会话权限、当前飞行状态、物理互锁、参数范围、重复请求和审计记录。协议层的认证不能替代飞控状态机的安全门禁。
可观察实现¶
实现应暴露而不是隐藏:
- 接收和发送字节数;
- 有效帧数;
- 长度、校验、版本和权限错误;
- 丢帧、重复和乱序;
- 队列峰值;
- 最近有效消息时间;
- 重连和解析器重同步次数;
- 高风险命令接受与拒绝原因。
计数器需要明确是否饱和、回绕或重启清零。只显示一个“错误总数”会混淆物理噪声、版本不兼容和权限拒绝。
协议审查问题¶
- 接收端怎样从任意字节位置重新找到合法帧?
- 长度字段是否在访问载荷前完成边界检查?
- 校验覆盖哪些字段,哪些字段没有保护?
- 时间戳由谁产生,重启后是否单调?
- 重复命令会执行一次还是多次?
- 未知扩展怎样处理?
- 连接恢复后沿用旧状态还是重新协商?
- 高风险命令的身份、权限和状态门禁在哪里?
- 解析器在最大合法流量和恶意流量下是否有预算?
- 怎样证明当前工具、消息定义和固件属于同一契约版本?
检查理解¶
- 为什么连接器、接口和协议不能互相替代?
- “发送成功”和“动作完成”之间还缺哪些阶段?
- CRC为什么不能证明发送者身份?
- 未知兼容扩展和未知不兼容扩展应如何区别?
- 为什么协议实现需要暴露错误分类和队列峰值?