帧头
基础规则
基础帧头固定为 56 字节。Header Length 字段用于未来扩展,但当前版本发送端必须使用 header_length = 56
所有多字节数值字段统一使用 Little Endian 编码。浮点数不出现在基础帧头中,Raw Binary Payload 的浮点规则见Payload 与 Schema
NRCP Frame 总长度由 Header Length 与 Payload Length 决定:
其中:
- 表示 NRCP Frame 总字节长度;
- 表示 Header Length;
- 表示 Payload Length;
接收端必须先读取基础 56 字节 Header,再根据 Header Length 与 Payload Length 判断完整 Frame 边界。若 Header Length 小于 56、Frame 长度不匹配或保留字段非 0,接收端必须拒绝该 Frame
帧头字段布局
| 字节索引 | 命名 | 字段含义 |
|---|---|---|
| 0-3 | Magic | 魔数 |
| 4 | Version | 版本号 |
| 5 | Header length | 帧头字节长度 |
| 6 | Payload codec | 载荷部分序列化方式 |
| 7 | Reserved 0 | 必须为 0 |
| 8-9 | Message Type | 消息类型 |
| 10-11 | Flags | 标志位 |
| 12-13 | Channel ID | 运行时通道 ID |
| 14-15 | Reserved 1 | 必须为 0 |
| 16-23 | Message ID | 消息 ID |
| 24-31 | Related ID | 关联的消息 ID |
| 32-39 | Sequence | 数据流消息序号 |
| 40-47 | Source mono timestamp | 发送方单调时间戳 |
| 48-51 | Flow Epoch | 数据流上下文版本 |
| 52-53 | Payload length | 载荷字节长度 |
| 54-55 | Reserved 2 | 必须为 0 |
字段类型
| 字节索引 | 字段 | 类型 | 编码 |
|---|---|---|---|
| 0-3 | Magic | 固定字节序列 | 0x4e 0x52 0x43 0x50,即 ASCII NRCP |
| 4 | Version | uint8 | 当前为 1 |
| 5 | Header length | uint8 | 当前为 56 |
| 6 | Payload codec | uint8 | 见 Payload Codec 注册表 |
| 7 | Reserved 0 | uint8 | 必须为 0 |
| 8-9 | Message Type | uint16 | Little Endian |
| 10-11 | Flags | uint16 | Little Endian |
| 12-13 | Channel ID | uint16 | Little Endian |
| 14-15 | Reserved 1 | uint16 | 必须为 0 |
| 16-23 | Message ID | uint64 | Little Endian |
| 24-31 | Related ID | uint64 | Little Endian |
| 32-39 | Sequence | uint64 | Little Endian |
| 40-47 | Source mono timestamp | uint64 | Little Endian,单位为微秒 |
| 48-51 | Flow Epoch | uint32 | Little Endian |
| 52-53 | Payload length | uint16 | Little Endian |
| 54-55 | Reserved 2 | uint16 | 必须为 0 |
Payload Codec 注册表
| 值 | 名称 | 说明 |
|---|---|---|
0 | raw_binary | 原始二进制 |
1 | protobuf | Protobuf |
2 | cbor | CBOR |
3 | json | JSON 文本,UTF-8 编码 |
4 | flatbuffers | FlatBuffers |
5 | sbe | SBE |
字段详解
Magic
Magic 固定为 ASCII 字节序列 NRCP,即 0x4e 0x52 0x43 0x50
QUIC 无 TCP 的粘包问题,因此此处的 Magic 作用并非防粘包与重同步,而是:
- Dump/日志/抓包快速识别 NRCP 帧;
- 防止错误的 Buffer 被误解析;
- 单测/回放工具更易定位;
- 与普通业务载荷、内部测试载荷、错误 Stream 内容等做快速区分;
Version
当前基础帧头 Version 为 1
QUIC ALPN 负责协议大版本协商,此处的 Version 负责:
- 帧数据脱离 QUIC 连接后仍然能够被独立解析;
- Dump 文件不一定保存了 ALPN;
- 测试样本、Fuzz 样本具备自描述能力;
- SDK 日志可直接展示帧版本;
- 后期同一 ALPN 下可区分 Header Minor Revision;
- 防止错误版本的解析器误解析帧;
Header length
当前版本发送端必须使用 header_length = 56。接收端收到小于 56 的 Header Length 必须拒绝该 Frame
之所以选择引入该字段是为了后续协议演进的便利,它有助于:
- 后续增加扩展头;
- 老解析器允许跳过未知扩展;
- 新解析器允许解析扩展字段;
- 不破坏基础帧布局;
- 支持 Feature Negotiation 后的扩展字段;
若去除该字段,未来协议中加字段时只能:
- 修改 ALPN;
- 修改整个帧格式;
- 依赖 Flags;
Payload codec
载荷部分的序列化方式,目前定义了以下枚举值:
0:原始二进制(Raw Binary);1:Protobuf;2:CBOR;3:JSON 文本(JSON);4:FlatBuffers;5:SBE
载荷部分之所以存在多种序列化方式,是因为 NRCP 包含控制、状态、告警、日志等不同类别,每种类别适合的序列化方式不同
Reserved 0/1/2
为了内存布局与 CPU 访问优化而添加,同时也作为后期扩展的显式保留字段
Message Type
该字段用于定义载荷的通用协议语义,具体规则见Message Type
Message Type 不表示具体业务能力,具体业务含义由 Operation Name 或 Flow Name 定义
Flags
该字段用于表达跨 Message Type 的通用修饰语义,形式为掩码,具体规则见Flags
未知 Flags 必须按消息语义章节的规则处理
Channel ID
该字段是 NRCP 动态数据流机制的核心字段之一,主要作用是查询所属数据流的上下文(比如载荷类型、QoS 配置等)
非 Flow 消息应使用 channel_id = 0
Message ID 与 Related ID
Message ID 是单方向唯一消息 ID,两者结合可用于:
- Req/Res 关联;
- ACK 关联;
- 日志追踪;
- 幂等处理;
- 应用层消息去重;
- 跨 Stream 关联;
当 Related ID 为 0 时,代表无关联
Sequence
QUIC Stream 在传输层保证了字节流的顺序性,但 Sequence 字段用于:
- QUIC Datagram 乱序;
- QUIC Datagram 丢包统计;
- 应用层 QoS 实现;
- Flow Epoch 切换期间旧包识别;
- 跨 Stream 或 Stream 重建后的 Flow 连续性;
0 代表不启用该字段
Source mono timestamp
该字段主要作用就是时间同步与消息的时效性判断
因为控制终端作为 Client、机械狗本体作为 Server 时,两端时间戳默认为非同步,因此无法直接使用系统时钟判断消息是否过期
更多规则见心跳与时钟同步
Flow Epoch
该字段为动态数据流机制的第二个核心字段,用于标识数据流版本,主要场景有:
- 订阅重建;
- QoS 更新;
- Flow 暂停后恢复;
- Channel ID 复用;
- 旧消息隔离;
- 服务器授权数据流上下文版本校验;
Payload length
Payload Length 表示基础 Header 与扩展 Header 之后的 Payload 字节长度。当前字段类型为 uint16,因此单个 Frame 的 Payload 最大长度为 65535 字节
接收端必须校验实际 Payload 长度与 Payload Length 一致。长度不一致时,Datagram 承载的 Frame 必须被丢弃,Stream 承载的 Frame 必须触发协议错误并进入实现定义的错误处理流程