24、通信协议设计:协议分层原则、头部与载荷的设计、状态机在协议中的应用

通信协议,说白了就是两台设备之间约定好的「对话规则」。

我做了这么多年嵌入式系统,见过太多因为协议设计不合理导致的坑。有的协议太死板,扩展性差;有的协议太随意,解析起来一团乱麻。今天我们就来聊聊,怎么设计一套靠谱的通信协议。

协议分层原则:别把所有逻辑塞一层

分层,是解决复杂问题的第一把钥匙。你想想看,TCP/IP 为什么能统治互联网?就是因为它把问题拆成了四层。

我在设计一个物联网网关项目时,一开始把数据打包、校验、重传、应用逻辑全写在一个函数里。结果呢?改一个重传策略,整个协议栈都得重新测试。后来我老老实实分了层,世界清净了。

常见的分层原则是这样的:

  • 物理层:管电压、管脚、波特率。别让上层操心是 RS232 还是 RS485。
  • 链路层:管帧同步、CRC 校验、地址过滤。保证「这一帧」是完整的。
  • 网络层:管路由、分包、重组。如果不需要路由,这层可以省略。
  • 传输层:管可靠传输、流控、重传。保证「这一条消息」能到。
  • 应用层:管业务逻辑。比如温度值怎么编码、命令怎么解释。

核心原则:每一层只关心自己的事,层与层之间通过标准接口交互。这样你换掉物理层(比如从串口换成蓝牙),上层代码一行都不用改。

头部与载荷的设计:别把鸡蛋放一个篮子里

一个协议帧,通常分为头部(Header)和载荷(Payload)。头部是「信封」,载荷是「信」。设计得好,解析效率高;设计得烂,CPU 全浪费在拆包上。

我习惯这样设计头部:

字段 长度(字节) 说明
起始标识 2 比如 0xAA 0x55,用于帧同步
版本号 1 协议版本,方便向后兼容
消息类型 1 命令、数据、应答、心跳等
载荷长度 2 告诉接收方要读多少字节
序列号 2 用于去重和应答匹配
校验和 2 CRC16,覆盖头部+载荷

这里有个坑:载荷长度字段一定要放在头部靠前的位置。为什么?因为接收方只有拿到长度,才知道要申请多大的缓冲区。我曾经见过一个协议,长度字段放在头部末尾,结果接收方得先解析完整个头部才能知道载荷多大——这不是脱裤子放屁吗?

我的经验:载荷长度字段建议用 2 字节,最大支持 65535 字节。如果不够,说明你的协议该考虑分包了。另外,起始标识用 2 字节的魔数(Magic Number),可以有效降低误同步的概率。

状态机在协议中的应用:让协议「活」起来

协议解析,本质上就是一个状态机。你想想看,接收方从空闲状态开始,收到起始标识后进入「接收头部」状态,头部收完进入「接收载荷」状态,载荷收完进入「校验」状态……

我早期做的一个项目,协议解析用的是 if-else 嵌套,大概有 200 行。后来加了一个重传功能,改得我头皮发麻。换成状态机之后,每个状态独立处理,逻辑清晰得像一张白纸。

一个典型的协议解析状态机长这样:

enum State {
    STATE_IDLE,
    STATE_RECV_HEADER,
    STATE_RECV_PAYLOAD,
    STATE_CHECKSUM,
    STATE_DISPATCH
};

void protocol_parse(uint8_t byte) {
    switch (state) {
        case STATE_IDLE:
            if (byte == 0xAA) {
                state = STATE_RECV_HEADER;
                header_buf[0] = byte;
                header_index = 1;
            }
            break;
        case STATE_RECV_HEADER:
            header_buf[header_index++] = byte;
            if (header_index == HEADER_LEN) {
                payload_len = (header_buf[4] << 8) | header_buf[5];
                state = STATE_RECV_PAYLOAD;
                payload_index = 0;
            }
            break;
        case STATE_RECV_PAYLOAD:
            payload_buf[payload_index++] = byte;
            if (payload_index == payload_len) {
                state = STATE_CHECKSUM;
            }
            break;
        case STATE_CHECKSUM:
            // 校验 CRC
            if (crc_ok) {
                state = STATE_DISPATCH;
            } else {
                state = STATE_IDLE;  // 校验失败,丢弃
            }
            break;
        case STATE_DISPATCH:
            // 交给上层处理
            dispatch_message(header_buf, payload_buf);
            state = STATE_IDLE;
            break;
    }
}

注意:状态机一定要有超时处理。如果接收过程中突然断线,状态机卡在 STATE_RECV_PAYLOAD 里,后续数据全乱套。我一般会在每个状态加一个超时计数器,超时后强制回到 STATE_IDLE。

知识体系结构图

下面这张图,概括了本章的核心逻辑:

通信协议设计核心结构 协议分层原则 物理层 → 链路层 网络层 → 传输层 → 应用层 头部与载荷设计 起始标识 + 版本号 消息类型 + 载荷长度 序列号 + 校验和 状态机应用 IDLE → RECV_HEADER RECV_PAYLOAD → CHECKSUM DISPATCH → IDLE 设计要点总结 • 分层:每层独立,接口清晰,方便替换底层硬件 • 头部:长度字段靠前,魔数防误同步,校验覆盖全帧 • 状态机:每个状态独立处理,必须加超时保护 • 扩展性:版本号字段让协议可以平滑升级

嗯,到这里,通信协议设计的基本框架就讲完了。分层让你不纠结,头部设计让你不踩坑,状态机让你不混乱。这三板斧用好了,大部分协议场景都能应付。

一句话总结:好的协议设计,是让发送方和接收方都「舒服」——发送方打包不费劲,接收方解析不迷糊。

交易系统化学习资料 微信Strategy888888