引言
不是所有设备都能负担 MQTT。一个 NB-IoT 水表每 6 小时上报一次,电池要撑十年,模块内存只有几十 KB,链路带宽以百字节计。在这种约束下,TCP 三次握手加 TLS 握手的开销就已经超出预算,长连接的心跳更是持续耗电。CoAP 就是为这类设备设计的:UDP 之上、报文头仅 4 字节、支持请求响应与观察通知。
单有 CoAP 还不够,设备管理需要一套统一的对象模型与生命周期流程,LwM2M 补上了这一层。它定义了 Security、Server、Device、Firmware Update 等标准对象,规定注册、更新、注销与读取写入执行的交互,让不同厂商的设备能被同一个平台管理。
本文先讲 CoAP 的报文结构与交互机制,再覆盖安全与传输扩展,然后进入 LwM2M 的对象模型与注册流程,最后给出实现栈选型与一次完整的注册交互示例。MQTT 与 CoAP 的定位差异在接入篇已说明,报文与 QoS 细节见 MQTT 协议与 Broker 实践 ;LPWAN 承载层的选择见 LoRa、NB-IoT 与 LPWAN 技术选型 。
目录
- 为什么受限设备需要 UDP 上的 REST
- CoAP 报文格式
- 方法码与响应码
- 重传与可靠性参数
- Observe 模式
- Block-wise 传输
- CoAP 安全:DTLS 与 OSCORE
- CoAP over TCP 与 WebSocket
- LwM2M 对象模型
- Bootstrap 与注册流程
- 实现栈与部署
- 一次完整的注册交互示例
1. 为什么受限设备需要 UDP 上的 REST
HTTP 与 MQTT 都建立在 TCP 之上。TCP 的可靠性对受限设备是双刃剑:它保证了有序不丢,但也意味着建连握手、拥塞控制与重传队列都要占用内存与时间。一个 TLS over TCP 的完整握手在网络良好时也要两三个往返,弱网下可能十几秒,期间设备必须持续供电。
UDP 之上做轻量可靠,把可靠性控制权交还给应用层,是更划算的选择:CoAP 用可选的确认机制实现必要时的可靠,不需要时不付出代价。请求响应模型让交互天然适合 REST 语义,设备资源被抽象成 URI,读取温度就是 GET /3303/0/5700。
与 MQTT 的取舍可以归纳为:
- 功耗:CoAP 无长连接心跳,稀疏上报场景更省电。
- 双向性:MQTT 天生支持下行推送,CoAP 需要 Observe 或设备轮询。
- 复杂度:CoAP 需自行处理重传与去重,MQTT 由 TCP 与 Broker 承担。
- 语义:CoAP 是资源导向,MQTT 是消息导向,设备管理场景资源导向更自然。
2. CoAP 报文格式
CoAP 报文由一个 4 字节定长头、可变的 Token、若干 Option 与可选 Payload 组成。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Ver| T | TKL | Code | Message ID |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Token (0..8 bytes) ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options (delta-encoded) ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 1 1 1 1 1 1 1| Payload (可选) ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
字段含义:
| 字段 | 位宽 | 说明 |
|---|---|---|
| Version | 2 bit | 固定为 1 |
| Type | 2 bit | CON=0、NON=1、ACK=2、RST=3 |
| TKL | 4 bit | Token 长度,0 到 8 |
| Code | 8 bit | 方法码或响应码,高 3 位为类别 |
| Message ID | 16 bit | 用于匹配 ACK 与去重 |
| Token | 0 到 8 字节 | 匹配请求与响应,跨代理保持不变 |
GET /3303/0/5700 的报文(CON,MessageID=0x7d34,Token=0x71)
40 01 7d 34 71 b4 74 65 6d 70
40 -- Ver=1, Type=CON, TKL=1
01 -- Code=0.01 GET
7d 34 -- Message ID
71 -- Token
b4 74 65 6d 70 -- Option: Uri-Path "temp"
Type 的四种取值决定交互语义:CON 需要 ACK,NON 不确认,ACK 是对 CON 的应答,RST 表示收到了无法处理的报文。
3. 方法码与响应码
Code 字段用 类别.详情 表示,类别 0 为方法,2 到 5 为响应。
| 码值 | 名称 | 说明 |
|---|---|---|
| 0.01 | GET | 读取资源 |
| 0.02 | POST | 创建或触发 |
| 0.03 | PUT | 更新或替换 |
| 0.04 | DELETE | 删除资源 |
| 2.01 | Created | 创建成功 |
| 2.02 | Deleted | 删除成功 |
| 2.04 | Changed | 更新成功 |
| 2.05 | Content | GET 成功,带载荷 |
| 4.00 | Bad Request | 请求格式错误 |
| 4.01 | Unauthorized | 未授权 |
| 4.04 | Not Found | 资源不存在 |
| 5.03 | Service Unavailable | 服务不可用 |
一个读取温度的交互是 GET 得到 2.05 Content,载荷为 23.5;若资源不存在则回 4.04 Not Found。响应的 Token 必须与请求一致,这是客户端匹配响应的依据,Message ID 只用于 CON 的 ACK 匹配。
4. 重传与可靠性参数
CoAP 的可靠传输只在 CON 上生效。客户端发出 CON 后启动定时器,超时未收到 ACK 就重传,重传次数与间隔由四个参数控制。
| 参数 | 默认值 | 含义 |
|---|---|---|
| ACK_TIMEOUT | 2 s | 初始超时时间 |
| ACK_RANDOM_FACTOR | 1.5 | 超时随机化系数,避免同步重传 |
| MAX_RETRANSMIT | 4 | 最大重传次数 |
| NSTART | 1 | 到同一端点的并发未确认请求数 |
| DEFAULT_LEISURE | 5 s | 多播场景随机延迟上限 |
| EXCHANGE_LIFETIME | 247 s | 报文去重状态保留时长 |
首次超时在 ACK_TIMEOUT 到 ACK_TIMEOUT × ACK_RANDOM_FACTOR 之间随机取,即 2 到 3 秒;之后按指数退避翻倍,直到 MAX_RETRANSMIT 次后放弃。理论最长等待约 45 秒,加上处理时间,EXCHANGE_LIFETIME 取 247 秒作为去重窗口。
重传时间线(CON)
t=0 发送
t=2..3 首次超时,重传 1
t=6..9 重传 2
t=14..21 重传 3
t=30..45 重传 4
t>45 放弃,向应用层报超时
NB-IoT 的典型 RTT 在 1 到 3 秒,默认 ACK_TIMEOUT 偏紧,容易触发无谓重传。工程上常把 ACK_TIMEOUT 调到 3 到 5 秒,并把 MAX_RETRANSMIT 保持 4,以容忍高延迟链路。
5. Observe 模式
Observe(RFC 7641)让客户端订阅资源变化,服务端在变化时主动通知,用一条 GET 加 Observe Option 建立。
请求:GET /3303/0/5700 Observe: 0
响应:2.05 Content Observe: 12 Payload: 23.5
通知:2.05 Content Observe: 13 Payload: 23.7
通知:2.05 Content Observe: 14 Payload: 23.6
Observe 值是递增序号,客户端用它判断通知的新旧与乱序。取消订阅发 Observe: 1 或直接 RST。
通知的传输方式可选 CON 或 NON。NON 通知省流量但不保证送达,CON 通知可靠但增加功耗。工程上按数据重要性选择:告警类用 CON,周期遥测用 NON 并配合较大的通知间隔。
服务端要为每个观察者维护状态,观察者数量大时内存压力显著。RFC 建议用「资源驱动」而非「客户端驱动」的观察者表实现,并对静默观察者做超时清理。周期性资源还应设置最小通知间隔,避免高频变化时把网络打满。
6. Block-wise 传输
受限设备的最大传输单元通常只有几百字节,而固件块、配置文件的传输远超此限。Block-wise(RFC 7959)把载荷切块,每块用 Option 声明序号、大小与是否还有后续。
请求:GET /5/0/0 Block2: NUM=0, M=0, SZX=2(64B)
响应:2.05 Content Block2: NUM=0, M=1, SZX=2 <64 字节>
请求:GET /5/0/0 Block2: NUM=1, M=0, SZX=2
响应:2.05 Content Block2: NUM=1, M=1, SZX=2 <64 字节>
...
- Block1 用于请求方向的分块(上传),Block2 用于响应方向的分块(下载)。
- SZX 编码块大小,2 表示 64 字节,6 表示 1024 字节。
- M 为 1 表示还有后续块,为 0 表示最后一块。
- 块大小可协商,服务端可在首个响应中调小 SZX 以适应链路。
固件下载走 Block2,配合 ETag 做块级校验与断点续传,是 LwM2M FOTA 的基础。与设备侧固件升级的整体流程配合见 OTA 固件升级工程 。
7. CoAP 安全:DTLS 与 OSCORE
CoAP 的安全有两个层次:传输层用 DTLS,应用层用 OSCORE。
DTLS 1.2 是 CoAP 最早的安全方案,用 coaps:// 标识,默认端口 5684。它提供与 TLS 等价的机密性与完整性,但握手开销大,且需要在设备侧维护证书或预共享密钥(PSK)。PSK 模式最省资源,适合内存受限设备,代价是密钥分发与轮换的管理成本。
DTLS 1.3 精简了握手,支持 0-RTT 恢复,弱网重连场景收益明显。RFC 9147 定义了 DTLS 1.3,主流实现已陆续支持。
OSCORE(RFC 8613)在应用层加密,只保护 CoAP 载荷与部分 Option,端到端有效,且能穿越代理。它的优势是可以在 DTLS 之上叠加,或者替代 DTLS 用于无法承担握手的场景。
| 方案 | 层次 | 端到端 | 开销 | 适用 |
|---|---|---|---|---|
| DTLS 1.2 PSK | 传输层 | 否 | 中 | 设备到网关一跳 |
| DTLS 1.3 | 传输层 | 否 | 中低 | 弱网重连频繁 |
| OSCORE | 应用层 | 是 | 低 | 需穿越代理 |
| 无安全 | 无 | 否 | 无 | 仅内网调试 |
安全设计的通用原则(密钥生命周期、最小权限、吊销流程)见 网络安全基础 。
8. CoAP over TCP 与 WebSocket
CoAP 并非只能跑在 UDP 上。RFC 8323 定义了 CoAP over TCP、TLS 与 WebSocket,用于三种场景。
第一,设备侧只能走 TCP 的网络(如某些蜂窝 APN 屏蔽 UDP)。第二,需要穿越只允许 HTTP 的企业代理,此时用 CoAP over WebSocket。第三,设备本身资源充裕,用 TCP 换取更简单的可靠性实现。
over TCP 的报文格式去掉了 Message ID 与 Type,因为 TCP 本身保证有序与不丢,重传逻辑不再需要。这简化了实现,但也失去了 UDP 的低开销优势。
选择口径:受限设备优先 UDP 上的 CoAP;需要穿越代理或复用现有 TCP 基础设施时用 WebSocket 承载;不要为了「统一」而把所有设备都改成 over TCP。
9. LwM2M 对象模型
LwM2M 用「对象 / 实例 / 资源」三级模型描述设备能力,URI 形如 /对象ID/实例ID/资源ID。
| 对象 ID | 名称 | 用途 |
|---|---|---|
| 0 | Security | 服务器地址、安全模式、密钥 |
| 1 | Server | 注册服务器、生命周期、通知策略 |
| 3 | Device | 制造商、型号、固件版本、电量 |
| 4 | Connectivity Monitoring | 网络类型、信号强度、IP |
| 5 | Firmware Update | 固件包、状态、升级结果 |
| 6 | Location | 经纬度、高度、时间戳 |
| 3303 | Temperature | 传感器值、单位、最小值最大值 |
| 3304 | Humidity | 湿度传感器 |
几个具体路径:
/3/0/0设备对象实例 0 的资源 0,制造商名称。/3/0/3固件版本。/3303/0/5700温度传感器实例 0 的传感器值。/1/0/1服务器对象的生命周期(Lifetime),单位秒,决定注册有效期。
资源有读、写、执行三种操作权限。/5/0/2 固件包的下载执行用 Execute 触发,/3/0/13 重启设备也是 Execute。
LwM2M 资源权限示例
/3/0/0 R 制造商名称,只读
/1/0/1 RW Lifetime,可读写
/5/0/2 E 固件下载,执行
/3303/0/5700 R 传感器值,只读,可 Observe
自定义对象用 1024 到 2047 的 ID 区间,避免与标准对象冲突。
10. Bootstrap 与注册流程
LwM2M 的设备生命周期分两个阶段:引导(Bootstrap)与注册(Registration)。
引导阶段让设备获得服务器地址与安全凭据。三种模式:
- Factory Bootstrap:出厂预置,简单但不可远程变更。
- Client Initiated Bootstrap:设备主动向引导服务器请求配置。
- Server Initiated Bootstrap:服务器推送配置,需要设备已在引导服务器注册。
引导完成后设备向管理服务器注册:
POST /rd?ep=urn:imei:8675<=3600&lwm2m=1.2&b=U
Content-Format: application/link-format
</3/0>,</1/0>,</5/0>,</3303/0>
ep是端点名,通常用 IMEI 或序列号,全局唯一。lt是 Lifetime,到期前必须更新注册。b声明支持的绑定模式,U 为 UDP,UQ 为带队列的 UDP。- 载荷是对象列表,声明设备支持哪些对象,服务器据此决定可用的管理操作。
注册成功后服务器返回位置路径(如 /rd/5a3f),后续更新与注销都用这个路径。更新用 POST 带新 Lifetime,注销用 DELETE。
更新:POST /rd/5a3f?lt=3600
注销:DELETE /rd/5a3f
服务器侧对设备的操作有 Read、Write、Execute、Discover、Write-Attributes 与 Observe。Observe 用于订阅资源变化,Write-Attributes 用于设置通知的最小间隔与阈值,例如温度变化超过 0.5 度才通知。
11. 实现栈与部署
| 实现 | 角色 | 语言 | 特点 |
|---|---|---|---|
| Eclipse Leshan | 服务端 | Java | 功能完整,支持 1.0/1.1/1.2 |
| Eclipse Wakaama | 客户端 | C | 内存占用小,适合 MCU |
| Anjay | 客户端 | C | 商用支持,对象丰富 |
| libcoap | 通用 | C | 纯 CoAP,可作为基础库 |
| Californium | 通用 | Java | CoAP 协议栈,Leshan 依赖 |
| aiocoap | 通用 | Python | 适合测试与原型 |
典型部署是设备侧跑 Wakaama 或 Anjay,服务端用 Leshan。服务端需要处理三件事:注册表的存储与过期清理、观察关系的维护、下行操作的请求响应匹配。
LwM2M 部署拓扑
设备(Wakaama) --CoAP/DTLS--> 网关(CoAP 代理) --CoAP/DTLS--> Leshan 服务端
|
业务系统(REST API)
规模化时要注意注册表容量与 Lifetime 分布。若所有设备 Lifetime 都是 3600 秒且同时上线,会出现周期性的注册更新风暴,工程上应加随机抖动。
12. 一次完整的注册交互示例
以下是一次典型的 DTLS PSK 引导加注册的完整时序。
1) DTLS 握手(PSK 模式,coaps://)
Client -> Server: ClientHello (PSK identity hint)
Server -> Client: ServerHello (cipher suite)
Client -> Server: ClientKeyExchange (PSK identity)
Server -> Client: Finished
[握手完成,后续报文加密]
2) 注册
Client -> Server: POST /rd?ep=urn:imei:8675<=3600&lwm2m=1.2&b=U
</3/0>,</1/0>,</4/0>,</5/0>,</3303/0>
Server -> Client: 2.01 Created Location: /rd/5a3f
3) 读取设备型号
Server -> Client: GET /3/0/1
Client -> Server: 2.05 Content "TH-01"
4) 订阅温度
Server -> Client: GET /3303/0/5700 Observe: 0
Client -> Server: 2.05 Content Observe: 7 23.5
Client -> Server: 2.05 Content Observe: 8 23.7 (资源变化时主动通知)
5) 写 Lifetime 并更新注册
Server -> Client: PUT /1/0/1 "7200"
Client -> Server: 2.04 Changed
Client -> Server: POST /rd/5a3f?lt=7200
Server -> Client: 2.04 Changed
6) 触发固件升级
Server -> Client: POST /5/0/2 (Execute)
Client -> Server: 2.04 Changed
[设备按 Block2 分块下载固件,状态通过 /5/0/3 上报]
排查注册失败时按顺序核对:DTLS 握手是否成功、端点名是否唯一、Lifetime 是否合理、对象列表是否与服务器预期一致。多数失败集中在端点名重复与安全凭据不匹配两处。
权衡取舍
- UDP 与 TCP:UDP 省电省内存但需自行处理重传,TCP 简单但开销大,受限设备优先 UDP。
- CON 与 NON:CON 可靠但耗电,NON 省电但不保证送达,按数据重要性分别配置。
- Observe 与轮询:Observe 省流量但服务端要维护观察者状态,观察者数量大时内存吃紧。
- DTLS 与 OSCORE:DTLS 成熟但握手重,OSCORE 轻量且端到端但需额外密钥管理。
- 块大小:块越小越适应窄带但往返次数越多,64 到 256 字节是常见折中。
- 对象模型自定义:自定义对象灵活但失去标准工具支持,优先复用标准对象。
常见坑清单
- 端点名重复:现象是后注册的设备顶掉先注册的,原因是 ep 用了非唯一值,规避方法是用 IMEI 或带唯一后缀。
- Lifetime 过短:现象是注册频繁超时,原因是 lt 小于网络 RTT 加处理时间,规避方法是按链路延迟放大取值。
- 所有设备同刻更新:现象是周期性注册风暴,原因是 Lifetime 相同且上线时间集中,规避方法是加随机抖动。
- ACK_TIMEOUT 沿用默认:现象是 NB-IoT 上大量无谓重传,原因是默认 2 秒小于实际 RTT,规避方法是调到 3 到 5 秒。
- 忽略 EXCHANGE_LIFETIME:现象是重复请求被当新请求处理,原因是去重窗口过短,规避方法是保持 247 秒默认值。
- Observe 观察者泄漏:现象是服务端内存持续上涨,原因是静默观察者未清理,规避方法是设置观察超时并清理。
- NON 通知当可靠用:现象是告警丢失,原因是 NON 无确认,规避方法是重要通知改 CON。
- Block 大小不协商:现象是大载荷传输失败,原因是链路 MTU 小于块大小,规避方法是服务端在首响应中调小 SZX。
- PSK 密钥全网相同:现象是一台设备泄露即全站失守,原因是密钥未一机一密,规避方法是按设备派生密钥。
- 明文 CoAP 上公网:现象是数据可被窃听篡改,原因是用了 coap 而非 coaps,规避方法是强制 DTLS 并禁明文端口。
小结
CoAP 的价值在于把 REST 语义压缩到 4 字节头部,让受限设备以极低开销完成资源读写与观察。理解 Type、Code、Token 与 Message ID 的分工,掌握 ACK_TIMEOUT 与 MAX_RETRANSMIT 的调参口径,是把它用稳的前提。Observe 与 Block-wise 分别解决了主动通知与大载荷传输,两者与 FOTA 强相关。
LwM2M 在 CoAP 之上补齐了设备管理的标准语义:对象实例资源模型让不同厂商设备可被同一平台管理,注册与生命周期流程让设备上下线可被追踪,Bootstrap 让凭据与服务器地址可远程配置。安全上,DTLS 解决传输层保护,OSCORE 解决端到端保护,两者按场景取舍。
下一步建议先理清承载层选择,理解 NB-IoT 与 LoRaWAN 在功耗与覆盖上的差异;再把 CoAP 的注册流程与 OTA 的固件分发串起来,形成从引导、注册、观察到升级的完整设备管理闭环。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。