2. CoAP报文结构
CoAP 的报文结构,说白了就是一套紧凑的二进制协议格式。它不像 HTTP 那样用文本头,而是用固定字段加可变选项的组合。我刚开始接触 CoAP 时,第一反应是「这玩意儿怎么这么像 UDP 的头部结构?」——嗯,确实有几分神似,但 CoAP 的字段设计更偏向物联网场景。
一个完整的 CoAP 报文,从前往后依次是:固定头部(4 字节)、Token、Options、Payload。咱们一个一个拆开看。
2.1 固定头部:4 字节的「身份证」
CoAP 的固定头部只有 4 个字节,32 个比特。每个比特都有明确分工。我画了一张图,帮你直观理解这 4 个字节的布局:
这 4 个字节里,每个字段都有它的脾气。咱们逐个来看。
2.1.1 版本号(Ver)
占 2 个比特。目前 CoAP 的版本号固定为 01(二进制),也就是十进制的 1。为什么是 2 个比特?因为设计者预留了未来扩展空间。我个人习惯在抓包时先看一眼 Ver 字段——如果出现 00 或 10,那大概率是协议解析出了问题。
2.1.2 类型码(Type)
占 2 个比特。这是 CoAP 的「消息类型」标识,一共 4 种:
| Type 值 | 含义 | 说明 |
|---|---|---|
| 00 | CON(Confirmable) | 需要确认的消息,类似 TCP 的 SYN |
| 01 | NON(Non-confirmable) | 不需要确认,发完即忘 |
| 10 | ACK(Acknowledgement) | 对 CON 的确认响应 |
| 11 | RST(Reset) | 复位消息,表示无法处理 |
我在项目中遇到过一个问题:某个传感器节点发送 CON 请求后,服务器没回 ACK,节点就一直重传。后来发现是服务器端的 Type 解析写死了,只认 NON 类型。所以,Type 字段的解析一定要做全,别偷懒。
2.1.3 Token 长度(TKL)
占 4 个比特。取值范围 0~8,表示后面 Token 字段的实际字节数。注意:TKL 为 0 表示没有 Token。为什么 Token 最大只有 8 字节?因为 CoAP 设计时考虑的是低功耗设备,Token 太长会增加报文开销。我建议你写代码时,Token 长度尽量用 2~4 字节,既保证唯一性,又不浪费带宽。
2.1.4 Code 字段
占 8 个比特。它分为两部分:高 3 位是 Class,低 5 位是 Detail。比如 0.05 表示 Class=0, Detail=5,即 GET 方法。常见的 Code 值:
| Code | 含义 | 场景 |
|---|---|---|
| 0.01 | GET | 读取资源 |
| 0.02 | POST | 创建资源 |
| 0.03 | PUT | 更新资源 |
| 0.04 | DELETE | 删除资源 |
| 2.05 | Content(成功响应) | 返回数据 |
| 4.00 | Bad Request | 请求格式错误 |
| 4.04 | Not Found | 资源不存在 |
| 5.00 | Internal Server Error | 服务器内部错误 |
你想想看,8 个比特能表示 0~255,但 CoAP 只用了其中一小部分。为什么?因为设计者把 Code 分成了请求(0.xx)和响应(2.xx、4.xx、5.xx)两大类,中间留了空间给未来扩展。
2.1.5 Message ID
占 16 个比特。这是 CoAP 实现可靠传输的关键。每个 CON 消息都有一个唯一的 Message ID,接收方回复 ACK 时必须带上相同的 ID。我见过一个坑:两个设备时钟不同步,Message ID 生成算法用了时间戳的低 16 位,结果在高并发时 ID 重复,导致消息乱序。后来我改用随机数加计数器的方式,再没出过问题。
2.2 Token:请求与响应的「配对钥匙」
Token 紧跟在固定头部之后,长度由 TKL 决定(0~8 字节)。它的作用是把请求和响应关联起来。比如客户端发了两个 GET 请求,服务器返回两个响应,客户端怎么知道哪个响应对应哪个请求?——靠 Token。
Token 和 Message ID 的区别:Message ID 用于消息层面的去重和确认,Token 用于请求-响应的匹配。举个例子:
- Message ID 就像快递单号,用来追踪包裹是否送达。
- Token 就像订单号,用来匹配你买的是手机还是电脑。
我建议你在设计系统时,Token 用 4 字节就够了。太长浪费带宽,太短容易冲突。曾经有个项目,Token 只用 1 字节,结果设备多了之后频繁冲突,排查了两天才发现是 Token 碰撞。
2.3 Options:CoAP 的「灵活扩展区」
Options 是 CoAP 报文中最灵活的部分。它采用 TLV(Type-Length-Value) 格式,每个 Option 包含:
- Option Delta:当前 Option 的编号与上一个 Option 编号的差值。
- Option Length:Value 部分的字节数。
- Option Value:实际数据。
常见的 Option 有:
| Option 编号 | 名称 | 用途 |
|---|---|---|
| 1 | If-Match | 条件请求 |
| 3 | Uri-Host | 目标主机 |
| 4 | ETag | 资源标识 |
| 5 | If-None-Match | 条件请求 |
| 7 | Uri-Port | 目标端口 |
| 8 | Location-Path | 资源路径 |
| 11 | Uri-Path | 请求路径 |
| 12 | Content-Format | 内容格式(如 JSON、CBOR) |
| 14 | Max-Age | 缓存有效期 |
| 15 | Uri-Query | 查询参数 |
| 17 | Accept | 期望的响应格式 |
| 23 | Block2 | 块传输(响应分片) |
| 27 | Block1 | 块传输(请求分片) |
| 28 | Size2 | 响应体大小 |
| 60 | Observe | 资源观察(订阅) |
Options 的解析有个技巧:Option 编号是递增的。比如第一个 Option 是 Uri-Path(编号 11),第二个是 Content-Format(编号 12),那么第二个 Option 的 Delta 就是 1(12-11)。这样做的好处是节省字节——Delta 通常很小,可以用更少的比特表示。
/sensor/temp 会拆成两个 Uri-Path Option,第一个编号 11,第二个 Delta=0(表示还是编号 11)。
2.4 Payload:真正的数据载体
Payload 是报文的最后一部分,前面有一个 Payload Marker(0xFF)。如果报文没有 Payload,就不需要这个 Marker。Payload 的格式由 Content-Format Option 指定,常见的有:
application/json(编号 50)application/cbor(编号 60)text/plain(编号 0)application/octet-stream(编号 42)
我建议你在设计 Payload 时,尽量用 CBOR 而不是 JSON。CBOR 是二进制格式,比 JSON 小 30%~50%,对低功耗设备更友好。曾经有个项目,传感器上报数据用 JSON,结果 MTU 经常超限,改成 CBOR 后问题迎刃而解。
2.5 一个完整的报文示例
咱们来看一个实际的 CoAP 请求报文,客户端请求 /temperature 资源:
固定头部(4 字节):
Ver: 01 (版本 1)
Type: 00 (CON)
TKL: 04 (Token 长度 4 字节)
Code: 0.01 (GET)
Message ID: 0x1234
Token(4 字节):
0xAB 0xCD 0xEF 0x01
Options:
Option 1: Uri-Path (编号 11)
Delta: 11, Length: 11, Value: "temperature"
Option 2: Content-Format (编号 12)
Delta: 1, Length: 1, Value: 50 (application/json)
Payload Marker: 0xFF
Payload:
{"unit": "celsius"}
这个报文总共多少字节?固定头部 4 字节 + Token 4 字节 + Options(Uri-Path 的 Delta+Length+Value 共 13 字节,Content-Format 共 3 字节)+ Payload Marker 1 字节 + Payload 18 字节 = 43 字节。相比 HTTP 动辄几百字节的头部,CoAP 的紧凑性一目了然。
2.6 报文解析的「避坑指南」
最后,分享几个我在实际项目中踩过的坑:
- TKL 为 0 但后面跟了 Token 字节:有些设备实现不规范,TKL=0 却还在后面塞了 4 个字节。解析时一定要以 TKL 为准,忽略多余字节。
- Message ID 回绕:16 位 Message ID 最大 65535,回绕后可能和之前的 ID 冲突。我建议用时间戳的低 16 位加设备 ID 的异或值,降低冲突概率。
- Options 顺序错误:CoAP 要求 Options 按编号升序排列。如果乱序,有些服务器会直接丢弃报文。我曾经调试过一个设备,Options 顺序写反了,排查了整整一天。
- Payload 不带 Marker:如果 Payload 存在但没加 0xFF,接收方会把 Payload 的第一个字节当成下一个 Option 的 Delta,导致解析崩溃。这个坑在快速原型开发时特别容易踩。
嗯,CoAP 报文结构就讲到这里。记住一句话:固定头部是骨架,Token 是钥匙,Options 是血肉,Payload 是灵魂。把这四个部分吃透,CoAP 协议你就掌握了七成。