4. 观察请求与响应流程实战
CoAP Observe 的核心价值在于“一次注册、多次推送”。但要让这条链路真正跑起来,必须把三件事做对:请求里正确携带 Observe 选项、响应里正确解析 Observe 选项、后续通知能持续到达并被识别。本节以 libcoap 命令行工具和 aiocoap 两种方式,完整走一遍观察建立与验证过程。
一、观察请求的报文结构
Observe 选项编号为 6,属于 elective 选项。在请求中,它只有一个合法取值:0,表示“注册观察”。响应中的 Observe 选项则承担两种语义:当资源成功建立观察时,返回一个递增的序列号(用于通知排序与去重);当服务器拒绝或取消观察时,返回 1,表示“取消观察”。因此,解析响应时不能只看“有没有 Observe 选项”,还要看它的值。
一个典型的观察请求如下(以 GET 为例):
CON GET /sensors/temp
Observe: 0
Token: 0x4a2b
Accept: application/json
服务器若接受,会返回 2.05 Content,并携带 Observe 序列号,例如:
ACK 2.05 Content
Observe: 12
Token: 0x4a2b
Content-Format: application/json
Payload: {"t":23.5}
此后每次资源状态变化,服务器都会主动发送通知,Observe 值递增(12、13、14……),Token 保持不变,客户端据此把通知关联到同一次观察。
二、用 libcoap 发起观察
libcoap 自带 coap-client 工具,适合快速验证。使用 -s 选项订阅观察,-O 指定 Observe 选项,-T 指定 Token,-v 打开详细输出以便看到选项解析过程。
coap-client -m get \
-O 6,0 \
-T 4a2b \
-s 120 \
-v \
coap://192.168.1.50:5683/sensors/temp
参数含义:-O 6,0 表示添加编号 6、值 0 的 Observe 选项;-s 120 表示订阅 120 秒;-v 会打印每条报文的选项与负载。执行后典型输出如下(示例):
v:1 t:CON c:GET i:1a2b {4a2b} [ Observe:0 ]
v:1 t:ACK c:2.05 i:1a2b {4a2b} [ Observe:12 Content-Format:50 ]
{"t":23.5}
v:1 t:NON c:2.05 i:0000 {4a2b} [ Observe:13 Content-Format:50 ]
{"t":23.7}
v:1 t:NON c:2.05 i:0000 {4a2b} [ Observe:14 Content-Format:50 ]
{"t":23.6}
判断观察是否建立成功,看三点:第一条响应是 2.05 且带 Observe 选项;后续报文 Token 与注册时一致;Observe 值单调递增。若响应为 4.04 Not Found,说明资源路径错误;若为 4.05 Method Not Allowed,说明该资源不支持 Observe;若响应没有 Observe 选项,则服务器未接受观察,客户端不应继续等待通知。
三、用 aiocoap 编写观察客户端
aiocoap 是 Python 的异步 CoAP 库,适合把观察逻辑嵌入程序。下面示例使用 request.observe() 注册观察,并逐条打印 Observe 值与负载:
import asyncio
from aiocoap import Context, Message, GET
async def main():
ctx = await Context.create_client_context()
req = Message(code=GET, uri="coap://192.168.1.50/sensors/temp")
req.opt.observe = 0
req.opt.accept = 50 # application/json
pr = ctx.request(req).observation
count = 0
async for resp in pr:
obs = resp.opt.observe
print(f"Observe={obs} code={resp.code} payload={resp.payload!r}")
count += 1
if count >= 5:
pr.cancel()
break
await ctx.shutdown()
asyncio.run(main())
这里 req.opt.observe = 0 是注册观察的关键;resp.opt.observe 读取响应中的序列号。当 resp.opt.observe == 1 时表示服务器取消了观察,循环应结束。示例中收到 5 条后主动调用 pr.cancel(),aiocoap 会发送 Observe:1 的取消请求。
四、解析 Observe 选项的注意点
Observe 选项是变长整数,解析时要按 CoAP 选项格式读取长度与值。常见误区有三个:一是把响应中的 Observe 值当成“是否观察”的布尔量,实际上它主要是序列号;二是忽略 Token 匹配,导致把不同观察的通知混在一起;三是没有处理 Observe=1 的取消语义。工程上建议维护一张以 Token 为键的观察表,记录资源路径、最近序列号、最后接收时间,用于去重和超时清理。
五、观察建立检查清单
| 检查项 | 期望结果 | 异常处理 |
|---|---|---|
| 请求含 Observe:0 | 报文中出现选项 6,值为 0 | 确认工具参数或代码赋值正确 |
| 响应码 | 2.05 Content | 4.04 检查路径;4.05 检查方法 |
| 响应含 Observe | 存在且值 ≥ 0 | 无该选项说明未建立观察 |
| Token 一致 | 通知 Token 与注册相同 | 不一致则重新匹配观察表 |
| 序列号递增 | 12→13→14… | 回绕或跳变时按 RFC 规则比较 |
| 取消语义 | Observe=1 表示取消 | 收到后停止等待并清理状态 |