4. CoAP选项系统与内容协商

CoAP把“请求什么资源、以什么格式返回、缓存多久”这些语义全部编码进选项(Option)字段。与HTTP把方法、路径、头字段混在文本行里不同,CoAP选项采用二进制“编号+长度+值”的TLV结构,并规定编号越小越靠前、同类选项可重复出现。理解选项系统,是设备端资源模型能否被正确寻址、正确表示、正确缓存的关键。

选项的编码规则与排序

每个选项由三部分构成:Option Delta(相对上一个选项编号的增量)、Option Length(值长度)、Option Value。Delta和Length各占4位,当值小于13时直接编码;13~268用“13+扩展字节”表示;269~65804用“14+两字节扩展”表示。这种设计让常见选项(如Uri-Path编号11)只占1字节头部,非常适合NB-IoT、LoRa等受限链路。

选项必须按编号升序排列。若同一编号出现多次,则按出现顺序拼接语义。例如Uri-Path编号11重复出现,就表示路径被“/”分段:Uri-Path="sensors"、Uri-Path="temp"等价于HTTP的/sensors/temp。设备端解析时不能假设选项连续,必须按Delta累加还原真实编号。

核心选项的语义与设备端处理

Uri-Path(11):定位资源,可重复。设备端通常维护一棵资源树,解析时逐段匹配。例如收到Uri-Path="dev"、Uri-Path="sensor"、Uri-Path="temp",就应命中/dev/sensor/temp这个资源对象。

Uri-Query(15):传递查询参数,可重复,格式为key=value。它不参与资源定位,而是影响资源表示。例如Uri-Query="unit=c"要求温度以摄氏度返回,Uri-Query="avg=10"要求返回10秒均值。设备端应在资源处理函数中读取这些参数,而不是把它们当作路径的一部分。

Content-Format(12):出现在请求中表示请求体格式,出现在响应中表示响应体格式。常用取值:0表示text/plain;charset=utf-8,50表示application/json,60表示application/cbor。设备端发送响应时必须显式设置该选项,否则客户端无法正确解码。

Accept(17):客户端声明“我希望收到什么格式”。设备端应据此选择最合适的表示。若无法满足,应返回4.06 Not Acceptable,而不是强行返回不支持的格式。

Max-Age(14):响应中表示该表示可缓存的最大秒数,默认60。设备端对变化缓慢的资源(如固件版本)可设较大值,对实时传感器数据应设较小值甚至0,避免客户端使用过期数据。

此外还有ETag(4)用于版本校验、If-None-Match(5)用于条件请求、Block1/Block2(27/23)用于分块传输,它们共同支撑起完整的资源表示协商链路。

CoAP选项解析与内容协商流程 CON请求报文 Code: 0.01 GET Uri-Path(11): "dev" Uri-Path(11): "sensor" Uri-Path(11): "temp" Uri-Query(15): "unit=c" Accept(17): 50 (JSON) 选项解析引擎 1. 按Delta累加还原编号 2. 同编号选项按序拼接 3. 路径段匹配资源树 4. 查询参数注入处理函数 5. Accept与可用格式比对 6. 计算Max-Age与ETag 设备资源树 /dev └ /sensor ├ /temp → 温度值 └ /hum → 湿度值 /fw → 固件版本 /cfg → 配置参数 内容协商决策 Accept=50且资源支持JSON → 返回2.05 Content,Content-Format=50,Max-Age=30 Accept=60但资源仅支持JSON → 返回4.06 Not Acceptable Uri-Query="unit=c" → 处理函数将原始值转换为摄氏度后再序列化 响应报文(2.05 Content) Code: 2.05 | Content-Format(12): 50 | Max-Age(14): 30 | ETag(4): 0x1A2B Payload: {"temp":23.5,"unit":"c","ts":1710000000}

设备端实现要点

在嵌入式设备上实现选项处理,建议把“解析”和“业务”分离。解析层负责把原始字节流还原成option_list结构,业务层只读取已命名的选项值。下面是一段伪代码示例,展示如何从选项列表中提取路径与查询参数:

// 伪代码:选项解析与资源匹配
option_t *opt = parse_options(packet);
char path[64] = "/";
char query[64] = "";

for (int i = 0; i < opt_count; i++) {
    switch (opt[i].number) {
        case 11:  // Uri-Path
            strcat(path, opt[i].value);
            strcat(path, "/");
            break;
        case 15:  // Uri-Query
            strcat(query, opt[i].value);
            break;
        case 17:  // Accept
            accept_format = atoi(opt[i].value);
            break;
    }
}

resource_t *res = find_resource(path);
if (!res) return coap_error(4.04);

if (accept_format != 0 && accept_format != res->format) {
    return coap_error(4.06);  // Not Acceptable
}

// 处理查询参数
if (strstr(query, "unit=c")) {
    convert_to_celsius(res->value);
}

coap_response(2.05, res->format, res->payload, max_age=30);

这段代码体现了三个关键判断:路径不存在返回4.04,格式不匹配返回4.06,查询参数影响表示内容。设备端必须显式设置Content-Format和Max-Age,否则客户端可能按默认值处理,导致解析错误或缓存过期。

常见问题与排查

实际调试中,选项相关故障往往表现为“资源找不到”或“数据乱码”。可按以下顺序排查:

  • 路径匹配失败:检查Uri-Path是否被正确分段。有些客户端把/dev/sensor/temp编码为单个Uri-Path值,此时设备端需要按“/”再拆分一次。
  • 格式协商失败:确认Accept值是否在设备支持列表内。若设备只支持CBOR(60),而客户端请求JSON(50),应返回4.06而非空响应。
  • 缓存不一致:检查Max-Age是否设置合理。对温度这类快变数据,Max-Age建议不超过10秒;对固件版本可设3600秒。
  • 选项顺序错误:CoAP要求选项按编号升序排列。若设备端手工构造报文时把Content-Format放在Uri-Path之前,部分客户端会直接丢弃报文。
实践建议:在设备端资源注册表中,为每个资源显式声明支持的格式列表和默认Max-Age。例如/dev/sensor/temp支持50和60,默认Max-Age=10;/fw支持0,默认Max-Age=3600。这样协商逻辑可以统一由框架处理,业务代码只需关注数据本身。

掌握选项系统后,设备端就不再只是“被动应答”,而是能根据客户端声明的Accept、Uri-Query和缓存策略,主动选择最合适的表示形式。这正是CoAP内容协商的核心价值:在受限网络中,用最小的开销完成格式、语言、缓存策略的协商,让资源表示与客户端期望精确对齐。