二、CoAP协议核心概念:请求/响应模型、资源发现、消息类型与ID/Token

好,咱们进入正题。CoAP 协议,说白了就是给物联网设备量身定做的 HTTP 简化版。你想想看,一个传感器节点可能只有几 KB 的 RAM,跑 HTTP 那套庞大的报文头,内存直接就爆了。CoAP 就是来解决这个问题的。

我个人习惯把 CoAP 看作「UDP 上的轻量级 HTTP」。它保留了 HTTP 的 RESTful 风格——GET、POST、PUT、DELETE,但报文头压缩到了 4 字节起步。嗯,这里要注意,它底层跑在 UDP 上,所以可靠性得靠自己来保证。

2.1 请求/响应模型:和 HTTP 很像,但更轻

CoAP 的请求/响应模型,本质上和 HTTP 一样:客户端发请求,服务端回响应。但实现方式有两种,我分别说一下。

2.1.1 常规模式(Confirmable 请求)

客户端发一个 CON(可确认)请求,服务端必须回一个 ACK(确认)响应。如果客户端没收到 ACK,就会重传。这其实就是 CoAP 在 UDP 上模拟 TCP 可靠性的方式。

我在项目中遇到过一个问题:某个温湿度传感器每隔 5 秒上报一次数据,用的是 CON 请求。结果网络稍微一抖动,重传队列就堆满了,导致新数据发不出去。后来我改成了 NON 模式,问题就解决了。这个后面会细说。

2.1.2 分离响应(Separate Response)

有时候服务端处理请求需要时间,比如查询数据库或者控制电机。这时候如果一直占用着 UDP 端口等处理结果,效率很低。CoAP 的解决方案是:服务端先回一个空 ACK,告诉客户端「我收到了,你等着」,等处理完了再单独发一个 CON 响应。

这个机制我特别喜欢。曾经做一个智能灯控项目,网关收到调光指令后,需要先校验权限、再计算 PWM 参数,整个过程要 200ms。用分离响应,网关先 ACK 确认,客户端就可以去干别的事了,等调光完成再通知客户端。用户体验好很多。

2.2 资源发现:设备上有什么,一问便知

你拿到一个新设备,怎么知道它支持哪些功能?HTTP 时代我们看 API 文档,但物联网设备成千上万,不可能每个都配文档。CoAP 提供了资源发现机制,说白了就是设备自己告诉你它有什么。

标准做法是:客户端向设备发送 GET /.well-known/core 请求。设备会返回一个类似这样的列表:

</temp>;rt="temperature";if="sensor",
</light>;rt="light-control";if="actuator",
</config>;rt="device-config";if="core"

你看,每个资源都标注了资源类型(rt)和接口类型(if)。客户端解析这个列表,就知道怎么跟设备交互了。

我的小技巧: 在开发阶段,我习惯用 Firefox 的 Copper 插件来测试资源发现。输入 coap://[设备IP]/.well-known/core,所有资源一目了然。比写代码调试快多了。

资源发现还有一个好处:支持查询过滤。比如你只想找温度传感器,可以发 GET /.well-known/core?rt=temperature,设备只会返回匹配的资源。这在大型网络中能省不少流量。

2.3 消息类型:CON、NON、ACK、RST

CoAP 定义了四种消息类型,我分别说说它们的用途和坑。

类型 全称 含义 典型场景
CON Confirmable 需要确认的消息 控制指令、重要数据上报
NON Non-confirmable 不需要确认的消息 周期性传感器数据、心跳
ACK Acknowledgement 确认收到 CON 响应 CON 请求
RST Reset 复位/拒绝 拒绝无效请求、重置连接

2.3.1 CON(可确认消息)

CON 消息要求接收方必须回复 ACK。如果发送方在规定时间内没收到 ACK,就会重传。重传策略是二进制指数退避——第一次等 2 秒,第二次等 4 秒,第三次等 8 秒……最多重传 4 次。

我曾经踩过一个坑:设备收到 CON 请求后,处理逻辑里有个死循环,导致没来得及发 ACK。客户端那边一直重传,最后网络直接瘫痪了。所以,处理 CON 请求时,一定要先发 ACK,再慢慢处理业务逻辑。

2.3.2 NON(非可确认消息)

NON 消息发出去就不管了,不需要 ACK。适合那些丢了也无所谓的场景,比如温度传感器每 10 秒上报一次数据。丢一个包,下次上报就补回来了。

我建议:对于周期性上报的数据,优先用 NON。能省不少网络开销和内存。但要注意,NON 消息没有重传机制,如果网络质量很差,数据丢失率会很高。

2.3.3 ACK(确认消息)

ACK 就是告诉对方「我收到了」。注意,ACK 必须携带和 CON 相同的 Message ID,这样发送方才能匹配上。

这里有个细节:ACK 可以「捎带」响应数据。比如客户端发 CON GET /temp,服务端可以回一个 ACK,里面直接带上温度值。这样一次交互就完成了,省了一个 RTT。

2.3.4 RST(复位消息)

RST 用于拒绝一个消息。比如设备收到一个它不认识的请求,或者资源已经不存在了,就回 RST。客户端收到 RST 后,应该停止重传。

我记得有一次调试,设备老是莫名其妙地重启。查了半天,发现是客户端发了一个 CON 请求,设备不认识,回了 RST。但客户端代码有 bug,收到 RST 后还在重传,设备每次收到都 RST,最后把自己搞崩溃了。嗯,这个坑大家要注意。

2.4 消息 ID 与 Token:区分消息和请求

这两个概念容易混淆,我分开讲清楚。

2.4.1 消息 ID(Message ID)

消息 ID 是 16 位的,用于检测重复消息和匹配 ACK/CON。每个 CON 消息都有一个唯一的 ID,ACK 必须携带相同的 ID。如果收到重复的 CON,设备应该回一个重复的 ACK,但不再处理业务逻辑。

为什么需要消息 ID?因为 UDP 是不可靠的,同一个包可能被重传多次。消息 ID 让接收方能够识别出重复包,避免重复处理。

2.4.2 Token(令牌)

Token 是 0~8 字节的,用于匹配请求和响应。一个客户端可能同时发出多个请求,每个请求带不同的 Token。服务端响应时,必须携带相同的 Token,这样客户端就知道这个响应对应哪个请求了。

Token 和消息 ID 的区别:消息 ID 是传输层的概念,用于去重和确认;Token 是应用层的概念,用于匹配请求/响应。一个请求可能重传多次(消息 ID 不同),但 Token 始终不变。

重要提醒: Token 不能为 0 长度!虽然 CoAP 标准允许,但很多实现(包括 ESP32 的 libcoap)对空 Token 处理有问题。我建议至少用 1 字节的 Token,简单递增就行。

2.5 避坑指南:我踩过的那些坑

做 CoAP 开发这几年,我总结了几条血泪教训:

  1. CON 请求不要发太频繁。 我曾经一个项目,客户端每 100ms 发一个 CON,结果重传队列爆了,设备直接死机。后来改成 1 秒一次,稳如老狗。
  2. NON 消息也要处理重复。 虽然 NON 不需要 ACK,但 UDP 本身会丢包重传,应用层还是可能收到重复包。我习惯在应用层加一个简单的序列号去重。
  3. 消息 ID 要循环使用。 16 位的 ID 最多 65535 个,用完了要循环。但要注意,同一个 ID 不能同时用于两个未确认的 CON 请求。我一般用一个位图来管理空闲 ID。
  4. Token 要随机化。 如果 Token 是固定值或者简单递增,攻击者可以伪造响应。我建议用随机数生成 Token,至少 4 字节。

好了,CoAP 的核心概念就这些。下一章我们讲如何在 ESP32 上实际搭建 CoAP 服务器和客户端,到时候会用到今天讲的所有知识。你先把这些概念消化一下,尤其是消息类型和 Token 的用法,后面写代码时经常要打交道。