2. CoAP消息模型与UDP传输

CoAP 运行在 UDP 之上,UDP 本身不保证送达、不保证顺序、也不做拥塞控制,因此 CoAP 必须自己定义一套轻量的消息模型来弥补这些缺口。理解 CoAP 的第一步,不是去看资源路径,而是先看清它把“消息”分成了哪几种类型、每种类型承担什么职责,以及消息 ID 与 Token 这两个字段各自解决什么问题。只有把消息层和请求/响应层分开看,后续的重传、去重、观察(Observe)等机制才不会混淆。

四种消息类型:CON、NON、ACK、RST

CoAP 在 UDP 报文头之后定义了 4 位 Type 字段,取值对应四种消息类型:

  • CON(Confirmable,可确认):要求对端必须回应。接收方收到 CON 后,要么回 ACK(确认收到),要么回 RST(拒绝/无法处理)。发送方若在超时前未收到 ACK,会按退避策略重传,因此 CON 是 CoAP 实现“可靠传输”的主要手段。
  • NON(Non-confirmable,不可确认):不要求确认,发送后即结束。适合周期性上报、可容忍丢失的传感器数据,开销最小,但没有重传保障。
  • ACK(Acknowledgement,确认):对某个 CON 的应答,表示“我已收到这条消息”。ACK 可以只做确认(空 ACK),也可以携带 piggybacked 响应,即把响应直接搭在 ACK 里返回。
  • RST(Reset,复位):表示“我收到了,但无法处理或不需要处理”,例如消息 ID 无法匹配、上下文缺失、对端不再关心该请求。RST 也是一种明确的拒绝信号。

需要特别区分两个层次:CON/NON/ACK/RST 属于消息层,描述“这条 UDP 报文要不要确认”;而 GET/POST/PUT/DELETE 以及 2.05、4.04 等响应码属于请求/响应层,描述“这条消息想做什么、结果如何”。一条 CON 消息里可以承载一个 GET 请求,一条 ACK 消息里可以承载一个 2.05 Content 响应,二者是叠加关系而非并列关系。

消息 ID 与 Token:两个字段,两种职责

CoAP 固定头部中有两个关键字段,初学者最容易把它们混为一谈:

  • Message ID(16 位):用于消息层的去重与匹配。它标识“这一条消息”,作用范围是相邻两端之间的一次消息交换。发送方为每个 CON/NON 分配一个消息 ID,接收方靠它判断是否重复收到同一条消息,并靠它把 ACK/RST 对应回原来的 CON。消息 ID 在同一个端点对之间应尽量不重复,通常采用递增计数器实现。
  • Token(0~8 字节):用于请求/响应层的匹配。它标识“这一次请求—响应事务”,作用范围可以跨越中间代理。客户端发请求时带上 Token,服务端在响应中原样回填同一个 Token,客户端据此把响应匹配到对应的请求。因为 Token 可以端到端保持,所以即使经过代理转发,匹配关系依然成立。

一句话概括:Message ID 管“这条消息有没有收到、是不是重复”,Token 管“这个响应属于哪个请求”。在并发请求场景下,客户端可能同时发出多个请求,此时 Token 是区分响应的唯一依据;而消息 ID 只负责每条消息自身的确认与去重。

CoAP 消息层与请求/响应层:Message ID 与 Token 的职责分工 客户端 CoAP Endpoint 服务端 CoAP Endpoint CON · GET /temp · MID=0x1A2B · Token=0x77 消息层:需确认 | 请求层:读取温度资源 ACK · 2.05 Content · MID=0x1A2B · Token=0x77 MID 匹配原 CON | Token 匹配原请求 CON · MID=0x1A2C · Token=0x78(超时重传) 同一 MID 重发,接收方据此去重 RST · MID=0x1A2C 无法处理 / 上下文缺失,明确拒绝 CON 请求 ACK 响应 重传 RST 拒绝 MID 管消息去重与确认,Token 管请求响应匹配

基于 UDP 的可靠性与重传策略

CoAP 的可靠性只对 CON 消息生效,其核心是 ACK 超时重传。发送方发出 CON 后启动重传定时器,若在超时时间内未收到 ACK 或 RST,则重传同一条消息(Message ID 保持不变,这样接收方才能识别为重复)。重传不是无限进行,而是有次数上限,超过上限后发送方放弃并向上层报告超时失败。

超时时间采用指数退避:初始超时 ACK_TIMEOUT 默认 2 秒,每次重传后超时时间翻倍,并叠加一个随机因子 ACK_RANDOM_FACTOR(默认 1.5),用于避免大量设备同时重传造成网络拥塞。最大重传次数 MAX_RETRANSMIT 默认 4 次。按默认参数估算,单次交换的最长等待时间约为 2×(2⁴−1)×1.5 ≈ 45 秒,这也是 CoAP 默认的交换生命周期上限。

默认参数(RFC 7252):
ACK_TIMEOUT        = 2 s
ACK_RANDOM_FACTOR  = 1.5
MAX_RETRANSMIT     = 4

重传时序(示例,实际含随机抖动):
第 1 次发送  → 等待 2s   → 未收到 ACK → 重传
第 2 次重传  → 等待 4s   → 未收到 ACK → 重传
第 3 次重传  → 等待 8s   → 未收到 ACK → 重传
第 4 次重传  → 等待 16s  → 未收到 ACK → 重传
第 5 次重传  → 等待 32s  → 未收到 ACK → 放弃,上报超时

接收方一侧则要做两件事:一是去重,对同一 Message ID 的重复 CON 只处理一次,但每次都要回 ACK,否则发送方会持续重传;二是匹配,用 Token 把响应关联到正确的请求。如果收到无法匹配的消息 ID 或无法处理的请求,可以回 RST 让对端尽快停止重传,而不是让它白白等满超时。

NON 消息不触发重传,但接收方仍可用 Message ID 做去重。若业务需要“至少一次”语义,应使用 CON;若可接受丢失、追求低开销,则用 NON。

一个可对照的交互示例

假设客户端要读取服务端的 /temp 资源,采用 CON 请求:

  1. 客户端构造 CON 消息,Code=0.01(GET),MID=0x1A2B,Token=0x77,Uri-Path=temp,发往服务端。
  2. 服务端收到后,若资源存在,回 ACK 消息,Code=2.05(Content),MID 回填 0x1A2B,Token 回填 0x77,Payload 为 “23.5C”。这是 piggybacked 响应,一次往返完成确认与响应。
  3. 若服务端暂时无法立即给出结果,可先回一个空 ACK(仅确认收到),稍后再单独发一条 CON 响应,此时响应使用新的 MID,但 Token 仍为 0x77,客户端靠 Token 完成匹配。
  4. 若客户端在 2 秒内未收到任何回应,则重传同一 CON(MID 仍为 0x1A2B),服务端若已处理过该 MID,应直接回 ACK 而不重复执行读取。
  5. 若服务端根本不认识该请求或上下文已失效,可回 RST,客户端收到后立即停止重传并判定本次交换失败。

把上述过程落到抓包观察上,判断依据也很直接:看 Type 字段确认是 CON/NON/ACK/RST,看 MID 是否成对出现以判断确认与去重,看 Token 是否一致以判断响应归属。若发现同一 MID 反复出现且间隔递增,基本可以判定为对端未回 ACK 导致的重传;若响应 Token 与请求不一致,则说明匹配逻辑存在问题。掌握这三个观察点,CoAP 消息层的调试就有了清晰的抓手。