2. CoAP 报文结构:从比特流到业务数据

CoAP 的报文结构,说白了就是一套紧凑的二进制协议格式。它不像 HTTP 那样用文本头,而是用固定字段加可变选项的组合。我刚开始接触 CoAP 时,第一反应是「这玩意儿怎么这么像 UDP 的头部结构?」——嗯,确实有几分神似,但 CoAP 的字段设计更偏向物联网场景。

一个完整的 CoAP 报文,从前往后依次是:固定头部(4 字节)、Token、Options、Payload。咱们一个一个拆开看。

2.1 固定头部:4 字节的「身份证」

CoAP 的固定头部只有 4 个字节,32 个比特。每个比特都有明确分工。我画了一张图,帮你直观理解这 4 个字节的布局:

CoAP 固定头部结构(4 字节) Ver (2 bit) 版本号 Type (2 bit) 类型码 TKL (4 bit) Token长度 Code (8 bit) Code字段 Message ID (16 bit) 消息ID Token (0~8 字节,由 TKL 决定) Token 值 固定头部共 4 字节 + Token(可变长),后续紧跟 Options 和 Payload

这 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 通常很小,可以用更少的比特表示。

我的经验:解析 Options 时,一定要处理 Delta 为 0 的情况。Delta=0 表示重复上一个 Option 编号,这在多个 Uri-Path 拼接时很常见。比如路径 /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 的紧凑性一目了然。

注意:Payload Marker 0xFF 不能出现在 Options 中。如果 Options 的 Value 里恰好有 0xFF,解析器会误判为 Payload 开始。我建议你在构造报文时,对 Options 的 Value 做转义处理,或者确保 Value 中不包含 0xFF。

2.6 报文解析的「避坑指南」

最后,分享几个我在实际项目中踩过的坑:

  1. TKL 为 0 但后面跟了 Token 字节:有些设备实现不规范,TKL=0 却还在后面塞了 4 个字节。解析时一定要以 TKL 为准,忽略多余字节。
  2. Message ID 回绕:16 位 Message ID 最大 65535,回绕后可能和之前的 ID 冲突。我建议用时间戳的低 16 位加设备 ID 的异或值,降低冲突概率。
  3. Options 顺序错误:CoAP 要求 Options 按编号升序排列。如果乱序,有些服务器会直接丢弃报文。我曾经调试过一个设备,Options 顺序写反了,排查了整整一天。
  4. Payload 不带 Marker:如果 Payload 存在但没加 0xFF,接收方会把 Payload 的第一个字节当成下一个 Option 的 Delta,导致解析崩溃。这个坑在快速原型开发时特别容易踩。

嗯,CoAP 报文结构就讲到这里。记住一句话:固定头部是骨架,Token 是钥匙,Options 是血肉,Payload 是灵魂。把这四个部分吃透,CoAP 协议你就掌握了七成。