一、 多维度对比
| 对比维度 | JSON | CBOR | Protobuf3 |
|---|---|---|---|
| 底层编码本质 | UTF-8 文本格式; 使用标点{}:,"分隔结构 |
二进制格式; 每一个独立元素依靠首字节描述:首字节 = 高 3bit 主类型 MT + 低 5bit 附加信息 AI; key-value 保留字符串 | 二进制格式; 每个字段头部 1 字节 = (tag_number << 3)|wire_type;高 5 位 = tag 编号,低 3 位 = wire_type 线类型; 报文不带字段名称 |
| Schema 依赖 | 无原生 schema,自描述;可以额外配套 JSON Schema 做校验 | 无 schema,自描述;报文中携带字符串 key | 强依赖外部 schema(.proto 文件);tag 编号是契约,报文只有数字 tag |
| 字段标识方式 | 字符串 key,例如 "cellId" |
字符串 key,例如 "cellId" |
数字 tag,例如 1、2、3 |
| 可读性 | ✅ 明文,直接打开查看、抓包调试 | ❌ 二进制,需要工具解码后才能看懂 key | ❌ 二进制;必须匹配对应的 proto 文件才能解析,脱离 schema 完全无法识别含义 |
| 包体积(同一样例) | ~70 字节(最大) | ~42 字节(中等) | ~14 字节(最小) |
| 解析性能 | 最慢,需要文本分词、字符串转类型、处理符号 | 中等,二进制直接读取,但 key 字符串需要匹配比对 | 最快;按 wire_type 固定偏移读取,无字符串 key 比对开销 |
| 默认值处理 | 默认值(0/false)必须显式写出 | 默认值正常编码写入报文 | proto3 默认值(0、false、空串)不序列化,不会出现在报文中,节省流量 |
| 类型约束能力 | 弱类型,无强制约束;数值和字符串容易混淆,拼写、大小写错误不会硬报错 | 弱类型;key 拼写 / 大小写错误会匹配失败,不会强校验结构 | 强类型约束,proto 预先定义类型;tag 固定,新增 tag 旧程序自动跳过,结构稳定 |
| 版本向前向后兼容 | 差;增减字段容易出现解析异常,key 写错直接失效 | 一般;和 JSON 类似,靠 key 匹配,容易因为 key 变更出问题 | 优秀;tag 一旦分配不再修改,新增 tag 老版本程序自动忽略未知字段 |
| 数组 / 批量测点场景开销 | 极高,每个元素伴随文本符号 | 较高,每个 key 字符串持续占用字节 | 极低,repeated 数组只传数值,没有 key 重复开销(96 串电芯上报优势巨大) |
| Qt 原生支持 | ✅ QJsonDocument 原生自带 | ❌ Qt5.12 无原生;Qt5.15+/Qt6 自带 QCbor;低版本依赖 tinycbor | ❌ 无原生;依赖 libprotobuf 或者轻量 nanopb(ESP32 常用) |
| 典型适用场景 | HTTP 鉴权接口、配置文件、日志、调试报文、后台交互 | NB-IoT/LoRa 低带宽、需要 COSE 签名、协议频繁改动、不想维护 proto | MQTT 高频实时采集、电芯电压 / 温度批量上报、CAN 转发、OTA 分包、大数据流 |
| 主要短板 | 冗余符号多、包大、解析慢 | key 字符串依然占用流量,大批量测点不划算 | 需要维护 proto 契约,两端 tag 严格对齐;抓包调试不方便 |
二、核心规则
1.CBOR 核心规则:每个元素由【首字节】控制
首字节 = 3bit 主类型 + 5bit 附加信息
bit: 7 6 5 | 4 3 2 1 0
主类型 | 附加信息(ai)
7 个主类型(MT,Major Type)
| MT | 含义 |
|---|---|
| 0 | 无符号整数(正数) |
| 1 | 负数 |
| 2 | 字节串 byte string |
| 3 | UTF-8 字符串 text string(我们的 key 都是这个) |
| 4 | 数组 array |
| 5 | 映射 map(就是 JSON 的 {},key-value 对) |
| 6 | 标签 tag(扩展,比如时间、二进制标签) |
| 7 | 简单值 / 浮点数:true/false/null、half/float/double 常用 MT7(类型 7)速查表 |
附加信息 ai:
- 0~23:值直接就是 ai 本身,不再额外字节
- 24:后面跟 1 字节 uint8
- 25:后面跟 2 字节 uint16
- 26:后面跟 4 字节 uint32
- 27:后面跟 8 字节 uint64
- 28~30:保留
- 31:无限长度流(indefinite)
1.1例子
{"cellId":5,"voltage":3.242,"temperature":28.6}
对应的 CBOR 十六进制: a3 66 63 65 6c 6c 49 64 05 67 76 6f 6c 74 61 67 65 fa 41 4f 8f 2d 6b 74 65 6d 70 65 72 61 74 75 72 65 fa 41 e4 33 33
a3是第一个字节 0xa3 二进制:101 00011
- MT=5(map,JSON 对象)
- ai=3 → 这个 map 里面有 3 组 key-value
- 👉 对应 JSON:3 个键值对:cellId /voltage/temperature
第 1 组:"cellId"
66 63 65 6c 6c 49 64 05
0x66:二进制011 00110MT=3(text string,字符串 key),ai=6 → 字符串长度 = 6 字节 后面 6 字节:63 65 6c 6c 49 64→ ASCII:c e l l I d→"cellId"0x05:二进制000 00101MT=0(无符号整数),ai=5 → 值 = 5
✅ "cellId" = 5
第 2 组:"voltage"
67 76 6f 6c 74 61 67 65 fa 41 4f 8f 2d
0x67:MT=3,ai=7 → 字符串长度 7 后续 7 字节:76 6f 6c 74 61 67 65→"voltage"0xfa:二进制111 11010MT=7(简单 / 浮点数),ai=26 → 单精度 float(4 字节 IEEE754) 紧跟着 4 字节:41 4f 8f 2d→ float=3.242
✅ "voltage" = 3.242
第 3 组:"temperature"
6b 74 65 6d 70 65 72 61 74 75 72 65 fa 41 e4 33 33
0x6b:MT=3,ai=11 → 字符串长度 11 后面 11 字节:74 65 6d 70 65 72 61 74 75 72 65→"temperature"0xfa:ai=26,单精度 float 4 字节41 e4 33 33→ float=28.6
✅ "temperature" = 28.6
2. Protobuf 的 Wire Format(线编码规则)
Protobuf 每个字段头部是 (tag_number << 3) | wire_type
1 字节,高 5 位是 tag 编号,低 3 位是 wire_type(线类型)
wire_type 对照表(proto3 常用):
| wire_type | 含义 |
|---|---|
| 0 | Varint(int32/uint32/bool 变长整数) |
| 1 | 64-bit 固定 8 字节 |
| 2 | Length-delimited(字符串、bytes、嵌套 message) |
| 5 | 32-bit 固定 4 字节(float!) |
公式: byte = (tag << 3) | wire_type
2.1 例子
{"cellId":5,"voltage":3.242,"temperature":28.6}
对应的 CBOR 十六进制:08 05 15 2D 8F 4F 41 1D 33 33 E4 41
① 0x08
0x08= 十进制 88 >> 3= 1 → tag = 18 & 0b111= 0 → wire_type = 0(varint)- ✅ tag=1,类型 varint,对应你 proto 里:
int32 cellId = 1;后面跟着的0x05就是值 5
② 0x15
0x15= 十进制 2121 >> 3= 2 → tag = 221 & 0b111= 5 → wire_type =5(固定 4 字节 float)- ✅ tag=2,float,对应
float voltage = 2;后面 4 字节:2d 8f 4f 41就是 3.242 的 IEEE754 单精度浮点数
③ 0x1d
0x1d= 十进制 2929 >> 3= 3 → tag = 329 & 0b111=5 → wire_type=5(float)- ✅ tag=3,float,对应
float temperature =3;后面 4 字节:33 33 e4 41是 28.6 的 float
2.2 汇总
| 首字节 | 十进制 | tag | wire_type | 类型 | proto 定义 |
|---|---|---|---|---|---|
| 0x08 | 8 | 1 | 0 | varint(int32) | int32 cellId = 1 |
| 0x15 | 21 | 2 | 5 | 32bit float | float voltage = 2 |
| 0x1d | 29 | 3 | 5 | 32bit float | float temperature =3 |
首字节(16进制) = tag * 8 + wire_type(10进制)
三、对比 Protobuf 编码的核心差异
CBOR:
- map (0xa3) → 里面放【字符串 key + value】
- 报文里面保留
"cellId"、"voltage"文本 - 解析器读到字符串 key,再做业务匹配
- 不需要提前 schema
Protobuf:
- 没有 key 字符串!只有
(tag<<3)|wiretype头 - 依赖外部 .proto 约定 tag 1=cellId
一句话: CBOR 是【带名字】;Protobuf 是【带编号】。
四、CBOR 优点 & 坑
✅ 优点
- 和 JSON 思维一致,key-value,自描述,不用维护 proto
- 比 JSON 体积小,去掉大量标点引号
- 支持整数、float、bool、bytes、数组、map
- 支持 COSE 签名(物联网安全,很多 NB-IoT 设备用)
❌ 缺点
- key 字符串依然占流量!大批量 96 串电芯 repeated 上报时,开销远大于 protobuf
- 没有强 schema 校验:key 写错大小写、拼写错,默默读不到,不会报错
- Qt 无原生支持,需要 tinycbor / QCbor