使用 RIOT 的 unicoap 编写 CoAP 服务器应用:从资源定义到 DTLS 与 Native 板实测 物联网嵌入式操作系统实时系统【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址https://gitcode.com/GitHub_Trending/riot/RIOT点击查看免费下载本教程以 RIOT 仓库中的unicoap_server示例为蓝本完整演示如何用unicoapRIOT 的统一模块化 CoAP 协议栈编写一个可同时运行于 UDP 与 DTLS 之上的 CoAP 服务器。读完本文你将掌握静态 CoAP 资源声明、请求处理器编写、查询参数解析、基于iolist的分块响应构造、PSK 凭据注入以及在 Linux 宿主上用native板卡配合 tap 接口完成端到端联调的完整技能。unicoap 与本文使用的示例工程unicoap是 RIOT 中一套统一、模块化的 CoAP 实现位于 sys/net/application_layer/unicoap其对外主头文件为 sys/include/net/unicoap.h。它的设计思路是将 CoAP 核心消息、选项、PDU、服务器/客户端骨架与底层传输驱动UDP、DTLS、slipmux 等解耦通过USEMODULE按需拼装从而支持在不同网络后端如 GNRC 或 lwIP之间切换。本教程的目标是创建一个向用户打招呼的 CoAP 服务器根资源/返回固定的Hello, World!/greeting资源接受name查询参数并返回个性化问候语同时服务 CoAP over UDP端口 5683与 CoAP over DTLS端口 5684。完整示例代码位于 examples/networking/coap/unicoap_server文章中的代码即取自该目录下的 main.c、Makefile 与 client.py。Getting Started搭建工程骨架Makefile按需引入 unicoap 模块在应用目录下创建Makefile首先把unicoap及其服务器子模块、UDP 与 DTLS 传输驱动加入依赖USEMODULE unicoap USEMODULE unicoap_server USEMODULE unicoap_driver_udp USEMODULE unicoap_driver_dtls其中unicoap提供核心协议实现unicoap_server提供服务器骨架资源匹配、请求分发、响应发送unicoap_driver_udp与unicoap_driver_dtls分别是 CoAP over UDP 与 CoAP over DTLS 的传输驱动。两个驱动可以只引入其中一个示例的注释也明确说明只引入 UDP 或 DTLS 驱动之一也是允许的。从源码结构看传输驱动各自实现了统一的传输接口见 sys/net/application_layer/unicoap/drivers/rfc7252/udp 与 sys/net/application_layer/unicoap/drivers/rfc7252/dtls。指定网络后端以 GNRC 为例RIOT 允许切换网络后端因此需要显式指定一个。本教程选用 GenericGNRC网络栈在Makefile中加入# Include packages that pull up and auto-init the link layer. # NOTE: 6LoWPAN will be included if IEEE802.15.4 devices are present USEMODULE netdev_default # Automatically initialize GNRC upon startup USEMODULE auto_init_gnrc_netif # Specify the mandatory networking modules USEMODULE gnrc_ipv6_default # Additional networking modules that can be dropped if not needed USEMODULE gnrc_icmpv6_echonetdev_default会自动拉起并初始化链路层设备若存在 IEEE802.15.4 设备还会自动引入 6LoWPANauto_init_gnrc_netif在启动时自动初始化 GNRC 网络接口gnrc_ipv6_default是 IPv6 通信的必备模块gnrc_icmpv6_echo提供 ping 回显按需取舍。示例的Makefile还保留了 lwIP 分支通过LWIP_IPV4/LWIP_IPV6变量切换展示了后端可替换这一特性详见 Makefile。引入头文件在main.c中只需包含一个总头文件它会再导出消息、选项、传输、服务器等所有子模块的头文件见 net/unicoap.h 的 include 列表#include net/unicoap.h实现一个简单的 CoAP 资源静态声明资源UNICOAP_RESOURCE 与 XFA要在编译期静态定义资源使用UNICOAP_RESOURCE宏。它依赖跨文件资源声明模块需在Makefile中加入USEMODULE unicoap_server_resource_declarationsUNICOAP_RESOURCE(name)会把资源定义放进一个跨文件数组XFACross-File Array中初始化时统一读取注册。若缺少该模块宏会触发编译期static_assert报错提示见 net/unicoap/server.h。该模块的完整说明见 CoAP Resource Declarations。UNICOAP_RESOURCE(hello) { /* ... */ };name参数是 C 变量名的一部分需要唯一但除此之外没有语义上的要求——示例注释称之为纯粹出于跨文件数组实现的技术需要。你完全可以给它起任何名字。指定资源路径path每个资源都必须被分配路径它是 CoAP URI 中 host/domain 与端口之后的部分。通过unicoap_resource_t.path属性设置使用UNICOAP_PATH_ROOT表示根路径/或用UNICOAP_PATH构造自定义路径UNICOAP_RESOURCE(hello) { .path UNICOAP_PATH_ROOT, };UNICOAP_PATH的每个参数是一个路径分量path component。例如路径/gelato/flavours/menu应写作UNICOAP_PATH(gelato, flavours, menu)注意一个路径分量内绝不能包含斜杠即不要写UNICOAP_PATH(gelato/flavours, menu)。从实现看net/unicoap/server.hUNICOAP_PATH只是把各分量字符串指针放入一个以NULL结尾的指针数组UNICOAP_PATH_ROOT则把该数组置为NULL且unicoap_path_is_root直接据此判断。unicoap还预定义了资源发现路径UNICOAP_PATH_RESOURCE_DISCOVERY即/.well-known/core。声明允许的方法methods用UNICOAP_METHODS宏列出该资源允许的 CoAP 方法类似于 HTTP 方法。methods不能为空——如果请求的方法不在允许集合内unicoap会以Method Not Allowed4.05响应拒绝UNICOAP_RESOURCE(hello) { .path UNICOAP_PATH_ROOT, .methods UNICOAP_METHODS(UNICOAP_METHOD_GET, UNICOAP_METHOD_FETCH), };UNICOAP_METHODS本质上是按位构建一个uint8_t位域UNICOAP_BITFIELD即1 method且与|运算保持同态因此你也可以用位运算组合例如UNICOAP_METHODS(UNICOAP_METHOD_GET) | UNICOAP_METHODS(UNICOAP_METHOD_PUT, UNICOAP_METHOD_POST)。UNICOAP_METHODS_ALL0xff表示放行所有方法。示例工程里根资源实际开放了 GET、PUT、POST 三种方法见 main.c客户端脚本client.py则支持 GET/PUT/POST/DELETE/PATCH/iPATCH/FETCH 全套请求方法。限制传输协议protocols可选可以将资源限定在一组传输协议上这在加密为强制要求的场景下很有用。本示例中问候语也允许明文 UDPUNICOAP_RESOURCE(hello) { /* ... */ .protocols UNICOAP_PROTOCOLS(UNICOAP_PROTO_DTLS, UNICOAP_PROTO_UDP), /* ... */ };unicoap_proto_set_t位域语义与 methods 类似特别地UNICOAP_PROTOCOLS_ALLOW_ALL0表示不做协议检查默认行为UNICOAP_PROTOCOLS_ALLOW_NONE1表示禁止任何协议。匹配逻辑见unicoap_match_protonet/unicoap/server.h。可靠传输UNICOAP_RESOURCE_FLAG_RELIABLE由于我们使用 CoAP over UDPDTLS 也依赖 UDP可以指示unicoap发送 CONconfirmable可确认响应——unicoap会持续重传直到客户端确认收到为止UNICOAP_RESOURCE(hello) { .path UNICOAP_PATH_ROOT, .methods UNICOAP_METHODS(UNICOAP_METHOD_GET, UNICOAP_METHOD_FETCH), .flags UNICOAP_RESOURCE_FLAG_RELIABLE, };根据 net/unicoap/server.h 的说明该 flag 仅在不可靠传输上生效把消息类型设为 CON、要求对端返回 ACK对可靠传输如 TCP会被忽略。同类 flag 还有UNICOAP_RESOURCE_FLAG_MATCH_SUBTREE让资源匹配给定路径的整棵子树例如/laniakea/milky-way可匹配/laniakea/milky-way/solar-system/pluto。绑定请求处理器handler最后挂上处理函数。请求到达/时unicoap会调用它UNICOAP_RESOURCE(hello) { .path UNICOAP_PATH_ROOT, .methods UNICOAP_METHODS(UNICOAP_METHOD_GET, UNICOAP_METHOD_FETCH), .flags UNICOAP_RESOURCE_FLAG_RELIABLE, .handler handle_hello_request };处理器原型接收四个参数请求消息、携带客户端地址等信息的辅助数据aux、用于发送响应的请求上下文ctx以及资源定义中可选的自定义参数handler_argunicoap_request_handler_tstatic int handle_hello_request(unicoap_message_t* message, const unicoap_aux_t* aux, unicoap_request_context_t* ctx, void* arg) { /* ... */ }记录请求并回复 Hello, World!先记录日志用unicoap_string_from_method把 CoAP 方法消息 code转成字符串用unicoap_message_payload_get_size取得负载字节数。这会打印形如GET /, 0 bytes的日志printf( app: %s /, % PRIuSIZE bytes\n, unicoap_string_from_method(message-method), unicoap_message_payload_get_size(message) );根资源非常简单直接返回Hello, World!字符串。对于静态的以\0结尾的字符串unicoap提供了便捷的初始化器unicoap_response_init_string并且可以直接复用message参数的内存unicoap_response_init_string(message, UNICOAP_STATUS_CONTENT, Hello, World!); return unicoap_send_response(message, ctx);unicoap_send_response的返回值不必原样 return但处理器返回值始终约定0 表示成功负整数表示错误。两种组合被视为致命错误调用了unicoap_send_response却不返回状态码、以及既不调用unicoap_send_response也不返回状态码。若你既没发响应又返回负值unicoap会自动补发一个Internal Server Error5.00响应而返回UNICOAP_STATUS_BAD_REQUEST这类状态码时unicoap会自动发送一个无负载的对应响应。若确定不需要响应可返回UNICOAP_IGNORING_REQUEST配合unicoap_response_is_optional判断见 net/unicoap/server.h。创建更动态的 CoAP 资源/greeting简单的问候显然不够个性化。我们让/greeting接受name查询参数当客户端请求coap://...host.../greeting?nameRIOTeer时服务器回复Hello, RIOTeer! Welcome to our itsy bitsy tiny CoAP server!。定义新资源UNICOAP_RESOURCE(greeting) { .path UNICOAP_PATH(greeting), .flags UNICOAP_RESOURCE_FLAG_RELIABLE, .methods UNICOAP_METHODS(UNICOAP_METHOD_GET), .handler handle_greeting_request, };解析 name 查询参数在handle_greeting_request中用unicoap_options_get_first_uri_query_by_name取出name查询参数。若访问者没留下名字直接返回Bad Request4.00。作为简写从处理器直接返回一个 CoAP 状态码即可——unicoap会自动发送不带负载的响应static int handle_greeting_request(unicoap_message_t* message, const unicoap_aux_t* aux, unicoap_request_context_t* ctx, void* arg) { (void)aux; (void)arg; ssize_t res 0; const char* name NULL; if ((res unicoap_options_get_first_uri_query_by_name(message-options, name, name)) 0) { printf(error: could not get name query: % PRIdSIZE (%s)\n, res, strerror(-res)); return UNICOAP_STATUS_BAD_REQUEST; } /* ... */ }示例代码进一步说明URIGET /greeting?nameRIOTeer在 CoAP 中会编码为两个选项——Uri-Path: greeting与Uri-Query: nameRIOTeerunicoap在请求消息的options字段中提供对这些选项的访问见 main.c。顺带一提示例源码调用的是同名的_string后缀变体unicoap_options_get_first_uri_query_by_name_string并判定res 0视为失败语义与教程文档一致。校验输入客户端输入不可轻信。这里施加 30 个 UTF-8 字符的上限并调用is_valid_name做校验示例实现会逐个字节检查是否存在提前出现的\0终止符见 main.c。这次我们借助unicoap_send_response携带专门的错误信息回给客户端/* Validate any input. Here, we apply a 30 UTF-8 character limit. You should perform UTF-8 * validation (is_valid_name). This also includes checking if theres a premature null * terminator. */ if (res 30 || !is_valid_name(name, res)) { unicoap_response_init_string(message, UNICOAP_STATUS_BAD_REQUEST, invalid name query); return unicoap_send_response(message, ctx); }设置 Content-Format 选项规范的响应应正确设置Content-Format选项这里用text/plain。设置选项可能失败例如缓冲区容量不足因此要先做这一步。选项在栈上通过UNICOAP_OPTIONS_ALLOC分配再用unicoap_options_set_content_format设置格式UNICOAP_OPTIONS_ALLOC(options, 2); /* Set Content-Format option to text/plain */ if (unicoap_options_set_content_format(options, UNICOAP_FORMAT_TEXT) 0) { return UNICOAP_STATUS_INTERNAL_SERVER_ERROR; } message-options options;注意分配选项时指定的缓冲区容量应是对内存占用上限的估算。如果你了解 CoAP 选项在内存中的表示方式也可以把容量精确设为所需字节数。本例中只需Content-Format一个选项因此容量精确设为 2 字节此时再添加任何选项都会失败。用 iolist 构造分块负载避免拷贝理论上可以把Hello,、名字、后缀三段拼进一个缓冲区再发送但那样太简单了。当需要较大缓冲区时下面的技术更可取不用缓冲区而是构造一个向量——由负载**块chunk**组成的链表。关键在于网络后端会自行把这些块拷贝进传输缓冲区因此应用侧可以避免额外的拷贝操作。先定义首尾两块#define PREFIX Hello, iolist_t list { .iol_base PREFIX, .iol_len static_strlen(PREFIX), }; #define SUFFIX ! Welcome to our itsy bitsy tiny CoAP server! iolist_t suffix { .iol_base SUFFIX, .iol_len static_strlen(SUFFIX) };提示static_strlen定义于示例代码中即sizeof(string) - 1见 main.c。再接入动态的中间部分——用户的nameiolist_t name_chunk { .iol_next suffix, .iol_base (void*)name, .iol_len res }; list.iol_next name_chunk;name_chunk.iol_next suffix把后缀链到中间块之后list.iol_next name_chunk再把这条两段链挂到首块之后一个三段式向量就完成了。最后设置状态与负载并发送unicoap_message_payload_set_chunks(message, list); unicoap_response_set_status(message, UNICOAP_STATUS_CONTENT); return unicoap_send_response(message, ctx);unicoap提供了多种略有差异的 API 组合来构造响应以减少样板代码更详尽的说明见 CoAP Server。支持 DTLS注入 PSK 凭据前面已在Makefile中引入 CoAP over DTLS 驱动但要让 DTLS 工作还需要把一条凭据注入unicoap。首先包含相关头文件并定义一个 PSK 凭据#if IS_USED(MODULE_UNICOAP_DRIVER_DTLS) # include net/sock/dtls/creds.h # include net/credman.h # include net/dsm.h # include unicoap_example_dtls.h # define EXAMPLE_DTLS_CREDENTIAL_TAG 42 static const uint8_t psk_id_0[] PSK_DEFAULT_IDENTITY; static const uint8_t psk_key_0[] PSK_DEFAULT_KEY; static const credman_credential_t credential { .type CREDMAN_TYPE_PSK, .tag EXAMPLE_DTLS_CREDENTIAL_TAG, .params { .psk { .key { .s psk_key_0, .len sizeof(psk_key_0) - 1, }, .id { .s psk_id_0, .len sizeof(psk_id_0) - 1, }, } }, }; #endifIS_USED(MODULE_UNICOAP_DRIVER_DTLS)是编译期判断仅当 DTLS 驱动通过USEMODULE引入时才编译这段代码见 main.c。示例用的身份与密钥定义在 unicoap_example_dtls.h 中身份PSK identity为Client_identity密钥为secretPSK二者长度上限均为 32 字节该头文件还给出了启用 ECC 时的 ECDSA 公私钥示例。在main函数中先把凭据加入系统凭据管理器credman再把它挂到 DTLS socket 上通过unicoap_transport_dtls_get_socket获取int main(void) { #if IS_USED(MODULE_UNICOAP_DRIVER_DTLS) int res credman_add(credential); if (res 0 res ! CREDMAN_EXIST) { /* ignore duplicate credentials */ printf(app: cannot add credential to system: %d\n, res); return 1; } sock_dtls_t* dtls_socket unicoap_transport_dtls_get_socket(); assert(dtls_socket); if ((res sock_dtls_add_credential(dtls_socket, EXAMPLE_DTLS_CREDENTIAL_TAG)) 0) { printf(app: cannot add credential to DTLS sock: %d\n, res); return 1; } #endif }CREDMAN_EXIST表示凭据已存在此时忽略重复添加。sock_dtls_add_credential通过标签从凭据管理器内部取回对应凭据见 main.c。至此服务器也可以通过加密的 DTLS 连接访问了。在 Native 板上测试 CoAP 服务器示例自带的 client.py 基于aiocoap库实现。测试流程分三步编译并运行服务器、向它发送 CoAP 请求、可选地抓包分析。我们将使用 RIOT 的native板卡客户端和服务器都跑在 Linux 宿主上不涉及任何无线网络——无需天线。编译并运行cd RIOT/examples/networking/coap/unicoap_server BOARDnative make -j flash term启动输出大致如下RIOT/dist/tools/pyterm/pyterm -ps RIOT/examples/networking/coap/unicoap_server/bin/native64/unicoap_server.elf --process-args tap0 Welcome to pyterm! Type /exit to exit. # RIOT native interrupts/signals initialized. # RIOT native64 board initialized. # RIOT native hardware initialization complete. # coap: registered 2 XFA resources # coap.transport.udp: zero_copy_guarantees1 creating UDP sock, port5683 if0 familyinet6 # coap.transport.dtls: creating DTLS sock, port5684 if0 familyinet6 # main(): This is RIOT! (Version: 2025.04-devel-634-RED-unicoap-02-server-minimal) # app: listening at UDP sock_tl_ep port5683 netif0 ipv6:: # app: listening at DTLS sock_tl_ep port5684 netif0 ipv6:: # app: using credential: typePSK idClient_identity keysecretPSK # app: IPv6 address: fe80::c0:ff:ee可观察到几个关键信息registered 2 XFA resources证实两个静态资源通过 XFA 注册成功UDP 与 DTLS 分别监听 5683、5684 端口PSK 凭据Client_identity/secretPSK已就绪板卡获得了链路本地地址fe80::c0:ff:ee。注意如果 tap 接口尚未创建需要先执行sudo ip tuntap add tap0 mode tap user ${USER} sudo ip link set tap0 up这一步骤同样记录在示例的 README.md 中。技巧unicoap会根据CONFIG_UNICOAP_DEBUG_LOGGING与CONFIG_UNICOAP_ASSIST输出调试日志。示例工程在 app.config 中启用了这两项配置CONFIG_UNICOAP_ASSIST是较轻量的诊断仅记录 API 误用、缺失模块与修复提示不含 trace而CONFIG_UNICOAP_DEBUG_LOGGING是包含 trace 日志的完整调试输出启用它时无需再设CONFIG_UNICOAP_ASSIST。发送 CoAP 请求打开第二个终端会话执行python3 client.py -m GET -u coap://[fe80::c0:ff:ee%tap0]/greeting?nameRIOTer脚本会打印一系列调试日志最终以响应收尾response: 2.05 Content bHello, RIOTer! Welcome to our itsy bitsy tiny CoAP server!恭喜你的服务器已经正常工作这里有个小细节值得注意教程原文写的是RIOTeer而示例client.py的推荐用法是RIOTer二者均能命中/greeting?name的解析逻辑返回对应的个性化问候。从 client.py 的用法说明可以看到客户端脚本的完整能力-m/--method指定请求方法GET/PUT/POST/DELETE/PATCH/iPATCH/FETCH-u/--uri指定 URI-mt/--type选择消息类型默认 NON可选 CON--observe注册资源观察通知-p/--payload携带请求负载-to/--timeout设置超时。脚本默认在端口 5600 上绑定 aiocoap 客户端上下文并预先加载了与服务器一致的 PSK 凭据secretPSK/Client_identity这正是它既能访问 UDP 端点也能访问 DTLS 端点的原因。抓包验证 CoAP 消息流想观察真实的 CoAP 报文可再开第三个终端抓包tcpdump -i tap0 -w coap-greeting.pcap随后按上述方式执行客户端脚本再用CTRLC终止tcpdump。用 Wireshark 打开coap-greeting.pcap即可检查 CoAP 消息应能看到一个NON不可确认请求随后是服务器发回的CON响应再之后是客户端回复的ACK消息——这正是UNICOAP_RESOURCE_FLAG_RELIABLE生效的直接证据即便请求是 NON服务器仍以 CON 可靠地投递了问候语并要求客户端确认。延伸阅读从源码理解 unicoap 服务器服务器核心 API资源路径、方法/协议位域、请求处理器、监听器注册、资源发现集中在 sys/include/net/unicoap/server.h消息与选项的构造、读取 API 见 sys/include/net/unicoap/message.h 与 sys/include/net/unicoap/options.h传输抽象与端点管理见 sys/include/net/unicoap/transport.hUDP/DTLS/slipmux 驱动分别位于 drivers/rfc7252 下的udp、dtls、slipmux子目录编译期资源声明的机制细节见 CoAP Resource Declarationsunicoap自身的初始化是自动完成的auto_init_unicoap属于 RIOT 默认模块会在main()之前调用unicoap_init()示例 main.c 对此有说明若用DISABLE_MODULE auto_init_unicoap退出该默认行为则需自行调用。默认配置CONFIG_UNICOAP_CREATE_THREADy会让unicoap创建后台线程运行处理循环见 app.config将其置 0 后可在自选线程上运行unicoap_loop_run()。至此你已经完成了一个支持 UDP 与 DTLS、包含静态与动态资源、可在native板卡上端到端验证的 CoAP 服务器——接下来就可以把它移植到真实硬件板卡或继续探索unicoap的客户端、观察Observe与块传输Block-Wise等进阶能力了。赞分享物联网嵌入式操作系统实时系统【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址https://gitcode.com/GitHub_Trending/riot/RIOT点击查看免费下载相关推荐RIOT unicoap 实战教程用 C 语言编写支持 UDP 与 DTLS 的 CoAP 服务器RIOT unicoap 实战教程用 C 语言编写支持 UDP 与 DTLS 的 CoAP 服务器 导读 本教程基于 RIOT 操作系统The friend物联网嵌入式操作系统实时系统RIOT 实战用 unicoap 构建模块化 CoAP 服务器含 UDP 与 DTLSRIOT 实战用 unicoap 构建模块化 CoAP 服务器含 UDP 与 DTLS 本篇文章基于 RIOT 仓库中的 unicoap_server 示物联网嵌入式操作系统实时系统RIOT unicoap CoAP over DTLS 驱动详解基于 RFC 7252 的安全 CoAP 传输层实现RIOT unicoap CoAP over DTLS 驱动详解基于 RFC 7252 的安全 CoAP 传输层实现 导读 本文围绕 RIOT 操作系统中 u物联网嵌入式操作系统实时系统创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考