4、CoAP协议核心:设计哲学、请求/响应模型、观察者模式与资源发现

4.1 CoAP的设计哲学——轻量才是王道

说实话,我第一次接触CoAP时,第一反应是:「这不就是HTTP的瘦身版吗?」后来踩过几次坑才明白,这种想法太天真了。

CoAP的设计哲学,说白了就是四个字:够用就好。它不像HTTP那样追求功能完备,而是针对资源受限的嵌入式设备做了大量取舍。我习惯把CoAP看作「UDP上的HTTP简化版」,但这样说其实不准确——它更像是一个为物联网场景量身定做的全新协议。

为什么这么说?你看它的核心设计原则:

  • 基于UDP传输:省去了TCP三次握手的开销,一个数据包就能完成请求。我在项目中遇到过,有些MCU的RAM只有几十KB,跑TCP协议栈都费劲,CoAP简直就是救星。
  • 二进制而非文本:HTTP的头部是文本的,一个GET请求可能就上百字节。CoAP用二进制格式,头部只有4字节。你想想看,在LoRa这种低速网络里,这差距有多大。
  • 内置重传机制:UDP不可靠?CoAP自己实现了可靠传输。它用「确认消息」和「重传超时」来保证可靠性,但比TCP轻量得多。
  • 支持异步通信:设备可以「订阅」资源变化,不用轮询。这个后面会细讲。

核心要点:CoAP不是HTTP的替代品,而是针对物联网场景的专门优化。它牺牲了部分通用性,换来了极致的轻量和高效。

4.2 请求/响应模型——像HTTP但不一样

CoAP的请求/响应模型,乍一看和HTTP很像。它也有GET、POST、PUT、DELETE这些方法,也有类似URL的URI。但细节上差别很大。

先看一个典型的CoAP请求:

// CoAP GET请求示例
// 请求:CON, GET, /temperature, MID=0x1234
// 响应:ACK, 2.05 Content, "25.3°C"

这里有几个关键点:

  • CON和NON:请求分为「需要确认」(CON)和「不需要确认」(NON)两种。CON请求必须回复ACK,NON则不需要。我建议对重要数据用CON,比如控制指令;对周期性上报的传感器数据用NON,省带宽。
  • MID(消息ID):每个消息都有一个16位的ID,用来匹配请求和响应。HTTP靠TCP连接来关联,CoAP靠这个ID。
  • Token(令牌):和MID不同,Token是客户端自己生成的,用来区分同一个端点的不同请求。比如你同时发了两个GET请求,靠Token来区分哪个响应对应哪个请求。
  • 响应码:CoAP的响应码和HTTP类似,但更精简。2.05表示成功,4.00表示客户端错误,5.00表示服务端错误。嗯,这里要注意,CoAP的响应码是三位数,但格式是「x.yy」,和HTTP的「xxx」不一样。

我的经验:在实际项目中,我习惯把Token设成和MID不同的值。MID由协议栈自动管理,Token由应用层控制。这样调试时能快速定位问题。

CoAP还有一个HTTP没有的特性——「空消息」。它不包含任何请求或响应,只用来做ACK或RST(复位)。这在UDP场景下很有用,比如你收到一个不认识的消息,可以回复RST告诉对方「别发了」。

4.3 观察者模式——让设备主动「订阅」

这是CoAP最让我眼前一亮的功能。传统的HTTP轮询,说白了就是客户端每隔几秒问一次「数据变了吗?」。这在物联网场景下效率极低——你想想看,一个温度传感器可能一小时才变化一次,但客户端每分钟轮询一次,浪费了多少带宽和电量。

CoAP的观察者模式(Observe)解决了这个问题。它的工作流程是这样的:

  1. 客户端发送一个GET请求,并在选项里加上「Observe: 0」。
  2. 服务端接受订阅,返回当前资源状态,并记录这个客户端。
  3. 当资源变化时,服务端主动推送新数据给所有订阅者。
  4. 客户端可以随时取消订阅(发送Observe: 1的GET请求)。

我曾经在一个智能照明项目里用过这个模式。每个灯控器都是一个CoAP服务端,手机App订阅了所有灯的状态。当用户手动按下开关时,灯控器立即推送状态变化,App实时更新。如果用HTTP轮询,延迟至少3秒,而且网络开销大得多。

注意:观察者模式依赖CoAP的CON/NON机制。如果服务端用CON推送,客户端必须回复ACK,否则服务端会重传。我遇到过一个问题:客户端收到推送后回复了ACK,但网络延迟导致服务端超时重传,客户端又收到了重复数据。解决方案是在客户端做去重,用消息ID过滤。

观察者模式还有一个重要概念——「最大订阅数」。服务端资源有限,不能无限接受订阅。我建议在服务端实现时,设置一个合理的上限(比如32个),超过时返回4.03(Forbidden)。

4.4 资源发现——让设备「自我介绍」

在物联网场景里,设备种类繁多,你不可能事先知道每个设备提供什么资源。CoAP的资源发现机制,就是让设备自己「报家门」。

核心是/.well-known/core这个路径。你向这个路径发一个GET请求,服务端会返回它支持的所有资源列表。格式是这样的:

// 请求:GET coap://[设备IP]/.well-known/core
// 响应:
</sensors/temp>;rt="temperature";if="sensor",
</sensors/humidity>;rt="humidity";if="sensor",
</actuators/led>;rt="light";if="actuator"

这里有几个关键属性:

  • rt(资源类型):描述资源是什么,比如temperature、light。我习惯用统一命名规范,比如「sensor/temperature」这种格式。
  • if(接口描述):描述资源支持哪些操作,比如sensor(只读)、actuator(读写)。
  • sz(资源大小):可选,表示资源返回数据的大小上限。
  • obs:如果资源支持观察者模式,会加上这个标记。

避坑指南:我曾经在项目里发现,有些设备返回的资源列表特别长,导致UDP包分片。CoAP默认的MTU是1280字节,如果资源列表超过这个大小,就得用「块传输」(Block-wise Transfer)来分片。建议资源列表控制在1KB以内,或者实现块传输支持。

资源发现还有一个高级用法——「查询过滤」。你可以在请求里加参数,只获取特定类型的资源:

// 只查询温度传感器
GET coap://[设备IP]/.well-known/core?rt=temperature

// 只查询支持观察者模式的资源
GET coap://[设备IP]/.well-known/core?obs

这个功能在大型物联网系统里特别有用。比如一个智能楼宇里有上千个传感器,你不可能把所有资源都拉下来。通过过滤,只获取你关心的那部分。

4.5 小结与实战建议

CoAP的核心设计,说白了就是「在受限环境下做最有效率的事」。它不像HTTP那样面面俱到,但针对物联网场景做了精准优化。

我个人的建议是:

  • 选型时:如果你的设备RAM小于64KB,或者网络带宽有限(如LoRa、NB-IoT),CoAP是首选。如果设备资源充裕,且需要和Web系统深度集成,HTTP可能更合适。
  • 实现时:注意UDP的MTU限制,超过1280字节的数据一定要用块传输。观察者模式虽然好用,但别忘了做去重和超时处理。
  • 调试时:用Wireshark抓包,CoAP的报文结构一目了然。我习惯先抓一个HTTP请求对比着看,能更快理解CoAP的设计思路。

下一章我们会深入CoAP的安全机制——DTLS和OSCORE。说实话,这部分坑更多,但也是实际项目中必须掌握的。咱们到时候细聊。