RIOT unicoap 消息 API 实战指南:CoAP PDU 的解析与序列化
【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT
导读
unicoap是 RIOT 操作系统中的新一代 CoAP 实现,其核心价值在于把 CoAP 消息的解析、选项操作与序列化封装成一组安全、易用且适合资源受限节点的 C API。本文以官方文档《Using Message APIs》为主体,围绕示例应用examples/networking/coap/unicoap_message,完整演示如何把一段字节流(PDU)解析为unicoap_message_t消息、如何读写各类 CoAP 选项,以及如何把构造好的消息重新序列化为可发送的 PDU。读完本文,你将掌握unicoap消息层 API 的完整调用链,能够独立在 RIOT 应用中收发、检查与构造 CoAP 报文。
一、认识 unicoap 消息 API
在unicoap中,消息与**传输格式(PDU)**是两个相互独立的概念:
unicoap_message_t是与传输无关的中央容器类型,承载 CoAP 代码(Code)、载荷(Payload)与选项集合(Options),见 message 文档;- CoAP 报文在不同传输(UDP、DTLS、Slipmux)下头部格式不同,因此解析/序列化按传输提供专用函数,例如 RFC 7252 格式对应
unicoap_pdu_parse_rfc7252_result与unicoap_pdu_build_rfc7252。
从源码结构看,解析与序列化各分两步完成(见 pdu 文档):先由传输驱动解析/写入"头部到选项之前"的部分,再由unicoap公共实现解析/写入选项区。这样既复用了选项处理逻辑,又保留了传输差异。
本文展示的完整可编译示例位于 examples/networking/coap/unicoap_message,在主机上即可运行:
BOARD=native make flash term程序会依次打印"解析示例消息"与"创建示例消息"两个阶段的输出,与下文代码一一对应。
二、Bytes to Message:从 PDU 反序列化
2.1 解析一段 PDU
假设pdu是缓冲区中收到的 CoAP PDU(以 RFC 7252 报文为例):
const uint8_t pdu[] = { /* ... */ };手工解析需要依次完成"分配消息结构、分配选项结构、分配属性结构、把选项挂接到消息"等一整套样板工作。unicoap为此提供了聚合辅助结构unicoap_parser_result_t,一次分配即可:
unicoap_parser_result_t parsed = { 0 };然后直接调用针对 RFC 7252 传输的解析器。由于 CoAP 支持多种传输,PDU 头部各不相同;若报文来自 UDP 或 DTLS,使用 RFC 7252 格式即可:
if ((res = unicoap_pdu_parse_rfc7252_result(pdu, sizeof(pdu), &parsed)) < 0) { puts("Error: parsing failed"); return; } unicoap_message_t* message = &parsed.message;从实现上看,unicoap_pdu_parse_rfc7252_result只是一个内联包装:它把parsed->options挂到parsed->message.options上,再转调底层unicoap_pdu_parse_rfc7252。这样调用方就无需关心选项缓冲区的分配与装配。该函数失败时返回负 errno,例如选项非法返回-EBADOPT,选项缓冲区过小返回-ENOBUFS。
由于头部随传输变化,像 RFC 7252 消息类型(Confirmable/Non-confirmable 等)与 Message ID 这类传输相关字段,统一通过unicoap_message_properties_t的rfc7252成员访问:
printf("CoAP message has token=<%i bytes>\n", parsed.properties.token_length); printf("CoAP over UDP/DTLS has id=%i type=%s\n", parsed.properties.rfc7252.id, unicoap_string_from_rfc7252_type(parsed.properties.rfc7252.type));unicoap_string_from_rfc7252_type会把数值类型转换为可打印的常量 C 字符串,便于日志输出。
2.2 检查消息类别
unicoap提供三个谓词函数区分消息类别:
unicoap_message_code_is_request—— 请求(Request);unicoap_message_code_is_response—— 响应(Response);unicoap_message_code_is_signal—— 信令消息(Signal,如 RFC 7252 的 CSM/Ping/Pong/Release/Abort)。
消息代码(Code)本身是一个字节,但unicoap_message_t提供三种类型化视图,分别对应三类消息:
message->method——unicoap_method_t(GET/POST/PUT/DELETE 等请求方法);message->status——unicoap_status_t(2.05 Content、4.04 Not Found 等响应状态码);message->signal—— 信令码。
获取可读字符串也有对应版本:unicoap_string_from_method、unicoap_string_from_status、unicoap_string_from_signal。如果不想先判断消息类别,可以统一调用unicoap_string_from_code,它会根据 Code 自动生成描述字符串:
const char* method_name = unicoap_string_from_method(message->method);载荷本身通过message->payload与message->payload_size(字节数)访问。
2.3 读取选项
调试阶段可以直接把所有选项打印到标准输出:
unicoap_options_dump_all(message->options);对不可重复选项(如Content-Format,每个报文至多出现一次),unicoap提供以unicoap_options_get为前缀的只读访问器:
unicoap_content_format_t format = 0; if (unicoap_options_get_content_format(message->options, &format) < 0) { puts("Error: could not read Content-Format!"); } assert(format == UNICOAP_FORMAT_JSON);对可重复选项(如Uri-Query可出现多次),unicoap提供多组便捷访问器。先看"取第一个":
const char* query = NULL; ssize_t res = unicoap_options_get_first_uri_query(message->options, &query); if (res < 0) { if (res == -ENOENT) { puts("Message has no Uri-Query option"); } printf("Error: could read first Uri-Query option"); }注意:first类 getter 返回的是PDU 缓冲区内部视图,返回的字符串不带\0终止符,因此打印时必须用%.*s配合返回长度:
printf("First URI query: '%.*s'\n", (int)res, query);若查询串遵循name=value格式,还可以按名字取第一个查询项:
res = unicoap_options_get_first_uri_query_by_name_string(message->options, "color", &query); if (res < 0) { /* The getter also fails in cases where no option was found */ if (res == -ENOENT) { puts("Message has no 'color' query"); } printf("Error: could read first 'color' query"); }对于Uri-Path、Location-Path、Uri-Query、Location-Query这类可重复选项,unicoap还提供"还原连续表示"的复制型访问器:多个Uri-Path选项会被拼接回/original/path形式。这类访问器会执行拷贝,调用方需提供目标缓冲区。下面把多个查询拼成一个?a=1&b=2&c=3形式的查询字符串:
char query_string[50] = { 0 }; res = unicoap_options_copy_uri_queries(message->options, query_string, sizeof(query_string)); if (res < 0) { puts("Error: could not generate URI query string"); }如果不想拷贝、不想额外分配,可以改用选项迭代器遍历所有查询选项。迭代器是遍历选项的主要工具:先分配unicoap_options_iterator_t,再用unicoap_options_iterator_init初始化:
unicoap_options_iterator_t iterator; unicoap_options_iterator_init(&iterator, message->options); while ((res = unicoap_options_get_next_uri_query(&iterator, &query)) >= 0) { printf("- URI query: '%.*s'\n", (int)res, query); }迭代器同样可以遍历所有选项而不管其类型:
unicoap_options_iterator_init(&iterator, message->options); unicoap_option_number_t number; const uint8_t* value = NULL; while ((res = unicoap_options_get_next(&iterator, &number, &value)) >= 0) { const char* name = unicoap_string_from_option_number(number); printf("- option %s nr=%i contains %" PRIuSIZE " bytes\n", name, number, res); }2.4 一个真实的解析输入
示例应用 main.c 中内置了一段真实的 RFC 7252 PDU,对应如下 CoAP 报文(Confirmable、POST、id=65201、无 token):
{ "type": "Confirmable", "code": "POST", "id": 65201, "token": 0, "options": [ "Uri-Path: actuators", "Uri-Path: leds", "Content-Format: application/json", "Uri-Query: color=g", "Accept: application/json" ] }其字节表示为:
const uint8_t pdu[] = { 0x40, 0x02, 0xfe, 0xb1, 0xb9, 0x61, 0x63, 0x74, 0x75, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x04, 0x6c, 0x65, 0x64, 0x73, 0x11, 0x32, 0x37, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3d, 0x67, 0x21, 0x32, 0xff, 0x6d, 0x6f, 0x64, 0x65, 0x3d, 0x6f, 0x6e };可以看到 0xff 标记之前是选项区,之后是载荷mode=on。解析该 PDU 后,示例依次验证了消息类别(unicoap_message_code_is_request)、方法字符串、Content-Format == UNICOAP_FORMAT_JSON、首个 Uri-Query 与按名字取color查询等,完整覆盖了 2.1~2.3 节的全部 API。
三、Message to Bytes:把消息序列化为 PDU
3.1 创建消息容器
因为要往 CoAP 消息里添加选项,需要先分配选项缓冲区。为了免去"分配辅助结构 + 分配缓冲区 + 初始化"的样板代码,直接使用UNICOAP_OPTIONS_ALLOC宏并给出期望容量(以字节为单位):
UNICOAP_OPTIONS_ALLOC(options, 100);该宏在 options/base.h 中定义,会为变量自动生成内部存储缓冲并完成初始化(对象位于栈上)。若不想关心容量,还有使用默认容量CONFIG_UNICOAP_OPTIONS_BUFFER_DEFAULT_CAPACITY的UNICOAP_OPTIONS_ALLOC_DEFAULT(name)变体,以及对应静态分配版本UNICOAP_OPTIONS_ALLOC_STATIC。
接着初始化消息。既可以使用指定初始化器,也可以使用初始化函数。下面用字符串载荷初始化一个 POST 请求,并挂接上刚才的选项对象:
unicoap_request_init_string_with_options(&message, UNICOAP_METHOD_POST, "Hello, World!", &options);该函数位于 message.h,内部转调unicoap_message_init_string_with_options。此外还有面向字节载荷、返回新结构体(unicoap_request_alloc_*系列)等多个变体,可按需选用。示例应用中的载荷是一个更贴近实际场景的指令:
const char payload[] = "Dear thermostat, please adjust your target temperature to 22.5 °C"; unicoap_request_init_string_with_options(&message, UNICOAP_METHOD_POST, payload, &options);3.2 定制选项
不可重复选项用unicoap_options_set系列访问器设置,例如Content-Format:
int res = unicoap_options_set_content_format(&options, UNICOAP_FORMAT_TEXT); if (res < 0) { puts("Error: could not set Content-Format"); }可重复选项提供两种添加方式。第一种:直接提供原始连续表示(如完整路径),unicoap会拆分成多个选项实例:
int res = unicoap_options_add_uri_path_string(&options, "/thermostat/temperature"); if (res < 0) { if (res == -ENOBUFS) { puts("Error: options buffer too small"); } puts("Error: could not add URI path"); }第二种:逐个添加路径组件:
res = unicoap_options_add_uri_path_component_string(&options, "thermostat"); if (res < 0) { puts("Error: could not add path component"); } res = unicoap_options_add_uri_path_component_string(&options, "temperature"); if (res < 0) { puts("Error: could not add path component"); }Uri-Query同理。可以一次添加多个查询(&分隔):
res = unicoap_options_add_uri_queries_string(&options, "unit=C&cool=yes"); if (res < 0) { puts("Error: could not add URI query"); }unicoap对每个此类 API 都提供两种字符串变体:面向\0结尾的 C 字符串(*_string后缀),以及面向无终止符、需显式给出长度的字符串(无后缀)。例如unicoap_options_add_uri_queries(带长度)与unicoap_options_add_uri_queries_string,或unicoap_options_add_uri_query与unicoap_options_add_uri_query_string。
选项顺序规则由CONFIG_UNICOAP_OPTIONS_FULL_SUPPORT控制(默认开启):
- 开启(默认):可以修改先前已设置的选项,也可以任意顺序添加选项;
- 关闭:只能按 CoAP 选项号升序添加/设置选项,违反顺序时 setter 函数返回负错误码。
因此在默认配置下,可以在设置Content-Format为 TEXT 之后,再把它改为 JSON:
res = unicoap_options_set_content_format(&options, UNICOAP_FORMAT_JSON); if (res < 0) { puts("Error: could not change Content-Format"); }同理也可以追加单个查询,最终得到unit=C&cool=yes&time=now:
res = unicoap_options_add_uri_query_string(&options, "time=now"); if (res < 0) { puts("Error: could not add URI query"); }示例应用在调用上述 API 后,用unicoap_options_dump_all(&options)把全部选项打印出来,便于核对添加结果(见 main.c)。
3.3 序列化消息
首先分配一个任意容量的输出缓冲区:
uint8_t pdu[200];头部格式随传输而异。这里使用 CoAP over UDP / CoAP over DTLS,即 RFC 7252 格式。消息属性中不携带 token:
设计备注:此场景非常简单,因此不使用 token。非常受限的节点(Very constrained nodes)同一时刻只处理一个请求,无需用 token 区分多个未完成请求的响应。
unicoap_message_properties_t properties = { .token = NULL, .token_length = 0, .rfc7252 = { .id = 0xABCD, .type = UNICOAP_TYPE_NON } };最后调用与该传输匹配的序列化器:
ssize_t res = unicoap_pdu_build_rfc7252(pdu, sizeof(pdu), message, &properties); if (res < 0) { if (res == -ENOBUFS) { puts("Error: PDU buffer too small"); } puts("Error: could not serialize message"); return; } printf("The final PDU has a size of %" PRIuSIZE " bytes.\n", res);成功时返回值即序列化后的 PDU 总字节数。从实现看,unicoap_pdu_build_rfc7252先调用unicoap_pdu_build_header_rfc7252写入头部,再调用unicoap_pdu_build_options_and_payload续写选项与载荷;缓冲区不足时返回-ENOBUFS。
如果需要零拷贝发送,unicoap还提供**向量化(vectored)**序列化接口unicoap_pdu_buildv_rfc7252,它把头部、选项与载荷组织成iolist_t链表(见 pdu 文档),避免拼接连续缓冲区,适合配合 RIOT 的 socket/网络驱动直接发送。
四、完整流程串联:从解析到序列化
示例应用的main()(见 main.c)把上述两大部分串成一个闭环:
_example_parse_pdu():解析内置 PDU → 读取头部属性 → 检查请求类别 → 读取 Content-Format、Uri-Query → 迭代全部选项;_example_create_message():分配选项缓冲区 → 初始化 POST 消息 → 添加 Uri-Path/Uri-Query、设置 Content-Format → 打印选项 →_example_serialize_message()序列化输出 PDU 大小。
这与文档 message-example.doc.md 的章节一一对应。建议按以下顺序阅读源码加深理解:
- unicoap 消息主头文件:
unicoap_init/unicoap_deinit及消息处理循环入口; - message.h:消息容器、请求/响应初始化、解析与序列化函数声明;
- options/base.h:
UNICOAP_OPTIONS_ALLOC系列分配宏与选项对象定义; - 示例应用 README:运行方式说明。
五、小结
本文围绕unicoap消息 API 梳理了完整的数据流:解析方向上,unicoap_parser_result_t一揽子解决结构装配问题,unicoap_message_code_is_*系列判定消息类别,unicoap_options_get_*与选项迭代器覆盖不可重复、可重复选项的读取;序列化方向上,UNICOAP_OPTIONS_ALLOC快速分配选项区,unicoap_options_add/set_*构造选项(受CONFIG_UNICOAP_OPTIONS_FULL_SUPPORT影响顺序规则),最后由unicoap_pdu_build_rfc7252输出可发送的字节流。
这套 API 的设计取舍值得借鉴:解析器直接引用 PDU 缓冲区(firstgetter 返回非\0终止的视图),避免了不必要的拷贝,适合内存受限的物联网节点;同时通过"连续表示还原"与"按名查询"等高阶访问器,把常见业务模式(路径拼接、查询串解析)封装成一行调用。结合 message 文档 与 pdu 文档 阅读,即可完整掌握unicoap消息层的设计全貌。
【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考