RIOT 上的 NASA bplib DTN 实战:基于 UDP Convergence Layer 的 BPv7 示例深入解析
2026/9/20 21:32:30 网站建设 项目流程

RIOT 上的 NASA bplib DTN 实战:基于 UDP Convergence Layer 的 BPv7 示例深入解析

【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT

导读

本篇文章以 RIOT 操作系统中的bplib_cla_udp示例为核心,系统讲解如何在 RIOT 上借助 NASA 的 bplib 库实现延迟/中断容忍网络(DTN,Delay/Disruption Tolerant Networking)的 Bundle Protocol v7(BPv7)。你将掌握 DTN 的核心理念(存储转发、Convergence Layer 抽象)、该示例的编译配置方法、基于 UDP 的 CLA 底层实现,以及如何在 native 目标上用 tap 设备搭建 1~3 个 BP 节点的通信与中断恢复实验。本文所有实现细节均以当前仓库源码与配置为事实依据,可对照 示例目录 中的文件逐一验证。

一、什么是 DTN 与 Bundle Protocol

延迟/中断容忍网络(DTN)是一类面向"高通信延迟、频繁连接中断"场景的网络架构,其起源是航天通信领域——深空链路具有高延迟、可预测但不可靠的连接特征。DTN 的核心协议称为Bundle Protocol(束协议),bplib 实现的是其BPv7版本。

与 TCP/IP 的"端到端"假设不同,DTN不要求通信双方之间存在持续的端到端连接,而是依赖存储转发(store-and-forward)

  • 消息以Bundle(束)为单位在节点间逐跳传递;
  • 每个节点需要有持久化存储,用于暂存当前无法转发(没有下一跳)的 Bundle;
  • 当下一跳重新可用时,Bundle 会从存储中被取出并继续转发。

在 RIOT 仓库中,bplib 的 RIOT 移植版本由 pkg/bplib 包提供。根据 pkg/bplib/doc.md 的说明,该移植(bplib 7.0.2)目前不支持以下 BPv7 特性:分片(Fragmentation)、状态报告生成、BIBE 扩展(也因此不支持 Custody Transfer)、BPSec 扩展,并且 EID Scheme 仅支持ipn,不支持dtn

二、Convergence Layer 与 CLA 的角色

Bundle 协议本身不规定消息如何在物理链路上传输,而是通过Convergence Layer(汇聚层,CL)抽象来承载:

The messages of the protocol (bundles) are sent over some underlying protocol, called aConvergence Layer(CL). The object that translates between the underlying protocol and the bundle processor is called aConvergence Layer Adapter(CLA).

  • CL:承载 Bundle 传输的底层协议;
  • CLA:在底层协议与 Bundle 处理器之间完成转换的对象。

本示例使用的是UDPCL:Bundle 被直接作为 UDP 载荷发送(legacy UDPCL 风格),参见 pkg/bplib/cla/doc.md 中的说明。RIOT 移植目前实现了两种 CLA:

CLA 模块底层协议说明
bplib_cla_udpUDP(gnrc_sock_udp将 Bundle 直接作为 UDP 载荷发送,即本示例所用
bplib_cla_bleBLE L2CAP CoC通过 L2CAP 连接发送 Bundle,见 BLE 示例

说明:示例 README 中链接的 BLE 示例相对路径已转换为仓库根路径 examples/networking/dtn/bplib_cla_ble/README.md。

三、示例的编译配置:三个核心 Makefile 参数

本示例的配置全部由 Makefile 处理,它解析三个编译期变量并转换为 C 宏:

Makefile 变量对应宏默认值作用
REMOTEBPLIB_EXAMPLE_IP_REMOTE::1设置发送 Bundle 所用的底层 IPv6 远端地址
REMOTE_EID_NODEBPLIB_EXAMPLE_REMOTE_NODE_NO100设置目的端 EID 的节点号(node number)
LOCAL_EID_NODEBPLIB_LOCAL_EID_NODE_NUM100设置当前设备的节点号

如果变量未设置,Makefile 会发出$(warning ...)提示并回退到默认值。例如未设置REMOTE时提示:

REMOTE IPv6 address not set, defaulting to [::1]

此外,示例中 EID 的服务号(service number)是静态选择的,UDP 端口同样固定。在 main.c 中可以确认:

#define BPLIB_EXAMPLE_PORT 4556 #define BPLIB_EXAMPLE_REMOTE_SERVICE_NO 123

其中 UDP 端口 4556 对应 IANA 为 BP/UDP(RFC 9171)分配的端口号,符合标准约定。

3.1 与 CLA 相关的编译期常量

Makefile 中还定义了与 bplib 运行相关的关键常量:

CFLAGS += -DBPLIB_MAX_NUM_CONTACTS=1 # 联系人(contact)表大小 CFLAGS += -DBPLIB_MAX_NUM_CHANNELS=1 # 通道(channel)表大小 CFLAGS += -DCONFIG_BPLIB_STOR_BASE=\"/nvm0/bp\" # Bundle 存储路径前缀 CFLAGS += -DBPLIB_TIME_FILE_NAME=\"/nvm0/bp_time.dat\" # DTN 时间信息文件 CFLAGS += -DFS_NATIVE_DIR=\"$(CURDIR)/native$(LOCAL_EID_NODE)\" # native 上文件系统挂载目录
  • BPLIB_MAX_NUM_CONTACTS=1:本示例只有一个联系人(远端节点)。如果要实现"中间节点向两个不同下一跳转发",需要将其增大(详见"三节点场景"一节)。
  • BPLIB_MAX_NUM_CHANNELS=1:本示例仅向一个 EID 发送、在一个服务号上接收 Bundle。
  • CONFIG_BPLIB_STOR_BASE:存储实现保存 Bundle 的根路径,不同存储实现可能在其下创建子目录(如node_id/service_id/)。
  • BPLIB_TIME_FILE_NAME:bplib 的 TIME 模块用它记录跨启动周期的 DTN 时间参考。若不需要时间追踪,可用bplib_no_vfs模块关闭(见 pkg/bplib/doc.md 的模块表)。
  • FS_NATIVE_DIR:在 native 目标上,将 VFS 文件系统挂载到示例目录下的native$(LOCAL_EID_NODE)子目录,从而按本地节点号隔离存储——这是三节点模拟的关键前提(见后文)。

3.2 启用的模块

Makefile 中通过USEPKG/USEMODULE声明依赖:

USEPKG += bplib USEMODULE += bplib_stor_vfs_ordered # 有序/分层存储 USEMODULE += bplib_cla_udp # UDPCL USEMODULE += shell USEMODULE += shell_cmd_bplib # bplib shell 命令 USEMODULE += shell_cmd_ps USEMODULE += shell_cmd_gnrc_netif USEMODULE += netdev_default USEMODULE += auto_init_gnrc_netif USEMODULE += gnrc_ipv6_default USEMODULE += gnrc_sock_udp USEMODULE += vfs_default

其中bplib_stor_vfs_ordered是有序存储实现:Bundle 按紧急程度(urgency)排序后出队,这要求 DTN 时间已知(vfs+ 文件系统依赖)。UDP CLA 依赖gnrc_sock_udp

四、源码视角:Channel 与 Contact 的初始化

示例的初始化逻辑全部位于 main.c 的_config_nc()函数中,它演示了 bplib 的两大配置对象:

4.1 Channel(通道)配置

通道定义了"向哪个目的 EID 发送、携带哪些块、用什么 CRC"等策略。示例将通道 0 配置为:

BPLib_EID_t dest = { .Scheme = BPLIB_EID_SCHEME_IPN, .IpnSspFormat = BPLIB_EID_IPN_SSP_FORMAT_TWO_DIGIT, .Allocator = 0, .Node = BPLIB_EXAMPLE_REMOTE_NODE_NO, // 由 REMOTE_EID_NODE 决定 .Service = BPLIB_EXAMPLE_REMOTE_SERVICE_NO // 123 }; bplib_channel_set_crc_type(0, BPLib_CRC_Type_CRC16); bplib_channel_set_service_no(0, BPLIB_EXAMPLE_REMOTE_SERVICE_NO); bplib_channel_set_lifetime(0, 3600000); // Bundle 生存期 1 小时(ms) bplib_channel_set_dest_eid(0, dest);

同时为 Bundle 添加了多个扩展块并配置其序号与 CRC:

  • BUNDLE_AGE 块:块序号 2,CRC16;
  • HOP_COUNT 块(跳数限制块):块序号 3,无 CRC,hop limit 为 10——这正是三节点实验中 Bundle 来回转发后在第 10 跳被丢弃的原因;
  • PREVIOUS_NODE 块:块序号 4,无 CRC。

上述块可以通过bplib_channel_set_block_include(0, <block>, false)移除(见 main.c 注释)。

4.2 Contact(联系人)配置

联系人定义了"把数据发给谁、从哪收"。示例将 contact 0 配置为覆盖节点号 1~10000、服务号 1~10000 的 EID 模式,并把出/入地址绑定到 UDP 端口 4556:

BPLib_EID_Pattern_t reachable_eids = { .Scheme = BPLIB_EID_SCHEME_IPN, .IpnSspFormat = ..., .MaxNode = 10000, .MinNode = 1, .MaxService = 10000, .MinService = 1 }; bplib_contact_set_destinations(0, 0, reachable_eids); bplib_contact_set_out_addr(0, BPLIB_EXAMPLE_IP_REMOTE, BPLIB_EXAMPLE_PORT); bplib_contact_set_in_addr(0, BPLIB_EXAMPLE_IP_LOCAL, BPLIB_EXAMPLE_PORT);

注意:由于该 contact 覆盖了节点 1~10000 的所有EID,两台设备之间会转发一切不归属本地的 Bundle——这就是三节点实验中"消息在 A、B 之间来回传输"的根源。

4.3 启动顺序

main()的启动顺序也很有讲究(main.c):

  1. bplib_init()初始化 bplib 实例;
  2. _config_nc()完成 NC(Node Config)配置;
  3. bplib_cla_udp_start(&cla_udp1, 0)启动 UDP CLA(必须先于 bplib 侧的 contact 启动);
  4. BPLib_PI_AddApplication(0)/BPLib_PI_StartApplication(0)注册并启动应用层 I/O;
  5. BPLib_CLA_ContactSetup(0)/BPLib_CLA_ContactStart(0)通知 bplib 该 contact 已就绪;
  6. 创建bplib APP IN线程,通过BPLib_PI_Egress()轮询消费到达的 Bundle 并打印内容;
  7. 进入 shell 循环。

此外源码注释提醒:正式产品中应在退出前调用BPLib_CLA_ContactTeardownBPLib_PI_RemoveApplication,以便把尚未发送的排队 Bundle 推回存储(main.c)。

五、UDP CLA 的底层实现

UDP CLA 的实现位于 pkg/bplib/cla/udp/bplib_cla_udp.c,其头文件在 pkg/bplib/cla/udp/include/bplib_cla_udp.h。从源码结构看,一个 CLA 实例内部包含两个线程:

  • egress 线程(cla_udp_out,命名bplib-cla-udp-rx:循环调用BPLib_CLA_Egress()从 bplib 取待发送 Bundle,再通过sock_udp_send()发给远端。对-EHOSTUNREACH错误做了容忍处理(bplib_cla_udp.c)。
  • ingress 线程(cla_udp_in,命名bplib-cla-udp-tx:循环调用sock_udp_recv()收 UDP 报文,再调用BPLib_CLA_Ingress()送入 bplib(bplib_cla_udp.c)。

启动时,bplib_cla_udp_start()会从 NC 表中读取ClaOutAddr/ClaOutPortClaInAddr/ClaInPort,解析为 IPv6 地址后用sock_udp_create()建立 socket,然后创建上述两个线程。线程优先级为THREAD_PRIORITY_MAIN - 2,栈大小等于THREAD_STACKSIZE_MEDIUM + CONFIG_BPLIB_CLA_UDP_BUFLEN(各含一个收发缓冲)。

重要行为提示:UDP CLA只接收来自已配置远端地址的报文,来自其他 IP 的 UDP 数据会被静默忽略(见 bplib_cla_udp.h 的@note以及 bplib_cla_udp.c 中对-EPROTO的容错分支)。

5.1 UDP CLA 相关配置项

定义在头文件中的两个可配置宏(bplib_cla_udp.h):

默认值说明
CONFIG_BPLIB_CLA_UDP_BUFLEN1024发送与接收缓冲(各自独立)的大小,即 UDP 上的 MTU,一般不应超过约 1400
CONFIG_BPLIB_CLA_UDP_TIMEOUT10000socket 轮询与 bplib egress 的阻塞超时(ms);调用bplib_cla_udp_stop()后,线程最多经过该超时才会退出

六、实验场景一:Loopback 回环测试

最简单的测试无需任何配置修改——所有标志默认即回环:

make all term

前提是系统存在可用的 tap 设备(示例启动时会尝试绑定一个)。此时:

  • 本地节点号与目的节点号均为默认值 100,且REMOTE默认为::1
  • 使用 shell 命令bplib send 0 "[DATA]"发送数据时,当前节点就是目的节点,数据走 bplib 内部投递路径,完全不会经过 gnrc/UDP(README.md 中明确说明)。

这里的0channel 编号:示例只使用一个通道,编号从 0 开始。

七、实验场景二:两台设备(A ↔ B)

7.1 创建两个 tap 设备并启动

按 gnrc 示例的方法创建两个 tap 设备(如tap0tap1),分别启动两个实例:

make all term PORT=tap0 make all term PORT=tap1

随后在两侧 shell 中运行ifconfig查看各自虚拟 tap 设备的 IPv6 地址。假设:

  • 设备 A 本地址为fe80::4832:95ff:feb7:5161
  • 设备 B 本地址为fe80::d8af:c5ff:febc:a409

7.2 重新编译指定远端地址

拿到地址后重新编译,将各自的REMOTE指向对方,并给两台设备不同的节点号:

# 设备 A:本地 fe80::4832:95ff:feb7:5161 make all term PORT=tap0 REMOTE=fe80::d8af:c5ff:febc:a409 LOCAL_EID_NODE=100 REMOTE_EID_NODE=200 # 设备 B:本地 fe80::d8af:c5ff:febc:a409 make all term PORT=tap1 REMOTE=fe80::4832:95ff:feb7:5161 LOCAL_EID_NODE=200 REMOTE_EID_NODE=100

此时运行bplib send 0 "[DATA]",Bundle 应投递到另一实例。数据路径为:

bplib → gnrc(UDP 载荷)→ 对端 → bplib

对端能投递成功,是因为其本地节点号正是目的节点号且通道处于激活状态。

7.3 中断模拟(Disruption Simulation)

DTN 的价值在于应对中断,本示例可以完整体验这一过程:

  1. 在任一节点执行bplib contact 0 stop,将 contact 0 置为stopped状态;
  2. bplib contact 0查看当前状态(示例正常启动时会置为 started);
  3. 处于 stopped 状态时,bplib 认为该 contact 当前无法服务其目的节点;
  4. 此时再bplib send 0 "[DATA]",Bundle不会到达对端,而是被放入storage——在 native 目标上即本示例目录下的native/子目录(对应FS_NATIVE_DIR的默认节点号 100);
  5. 中断结束后执行bplib contact 0 start重新启用 contact;
  6. 经过一定超时后,Bundle 会从存储中被取出并最终送达设备 B。

为什么中断检测很难自动化?README 明确指出:DTN 起源于航天通信,那里的 contact 可由静态、可预测的轨道计算得出;而 IoT 设备一般不具备这种可预测性,因此可靠的中断检测"不幸地并不容易"。

八、实验场景三:三个 BP 节点(A.1 ↔ B ↔ A.2)

真正的三设备转发需要更大的代码改动,但用两台设备模拟三个 BP 节点只需重新配置。核心技巧是利用FS_NATIVE_DIRLOCAL_EID_NODE隔离存储,以及修改LOCAL_EID_NODE让一台设备在 BP 语义下"扮演"不同节点。

8.1 场景设定

设备 A 想发给一个既非 A 也非 B 的目的节点(节点号 300):

# 设备 A:注意远端节点号变了 make all term PORT=tap0 REMOTE=fe80::d8af:c5ff:febc:a409 LOCAL_EID_NODE=100 REMOTE_EID_NODE=300 # 设备 B make all term PORT=tap1 REMOTE=fe80::4832:95ff:feb7:5161 LOCAL_EID_NODE=200 REMOTE_EID_NODE=100

A 发送消息后,B不会投递它(B 的本地节点不是 300),但该消息会在两台设备之间被反复转发。原因是两个节点的 contact 都覆盖节点号 1~10000 的全部 EID(见 main.c),因此双方都认为"所有 1~10000 的 Bundle 应发给对方"。

用 Wireshark 可以观察到这种来回转发。转发最终会停止,是因为 Bundle 携带了 Hop Count 块:跳数达到上限 10 后 Bundle 被删除(对应 main.c 中bplib_channel_set_hop_limit(0, 10))。

8.2 模拟第三个节点

  1. 在设备 B 上执行bplib contact 0 stop
  2. 从 A 发送一个新 Bundle——此时 Wireshark 中只有一个Bundle 被发出,且它被存入 B 的存储(native200/目录);
  3. 修改设备 A,使其看起来像"第三个节点"(本地节点号改为此前目的节点号 300):
# 设备 A:远端节点号再次改变 make all term PORT=tap0 REMOTE=fe80::d8af:c5ff:febc:a409 LOCAL_EID_NODE=300 REMOTE_EID_NODE=100
  1. 在 B 上重新启用 contact(bplib contact 0 start);
  2. 此刻 BP 语义下的"节点 A"(节点号 300)会收到从 B 存储中取出的 Bundle。

这个实验完整演示了 DTN 的核心思想:无需直接端到端路径的存储转发传输

8.3 不做重配置、直接在三节点间转发的前提

README 明确列出,若要在不重新配置的情况下让中间节点转发:

  • 必须存在另一个 IP 设备(此处是另一个 tap 设备);
  • 中间设备必须有两个 contact,即BPLIB_MAX_NUM_CONTACTS >= 2
  • 必须初始化两个分别指向各自下一跳的 UDP CLA。

由于本示例 Makefile 将BPLIB_MAX_NUM_CONTACTS固定为 1,这正是"需要较大改动"的原因所在。

九、bplib shell 命令速查

示例通过USEMODULE += shell_cmd_bplib引入bplibshell 命令(详见 sys/shell/cmds/bplib.doc.md)。其子命令如下:

子命令说明
bplib send <channel> <PAYLOAD>将给定字符串载荷作为 Bundle 从指定 channel 注入
bplib channel <channel> [<new_state>]不带new_state时打印通道状态;否则尝试迁移到setup/start/stop/teardown之一
bplib contact <contact> [<new_state>]不带new_state时打印联系人状态;否则尝试迁移到add/start/stop/remove之一

参数约束:<channel>取值在[0, BPLIB_MAX_NUM_CHANNELS)<contact>取值在[0, BPLIB_MAX_NUM_CONTACTS),即本示例中二者都只能是0

该 shell 命令文档还强调:它只是现有实现的辅助工具,仅有 shell 命令而无 CLA 初始化与 Bundle 消费逻辑的应用无法独立工作。此外,NC 的完整配置(如块包含策略、CRC 类型、contact 远端地址等)当前仍是编译期常量,未来计划迁移为可运行时配置(见 bplib.doc.md 的 "Future efforts" 一节)。

十、bplib 在 RIOT 中的模块与配置全景

本示例只触及 bplib 能力的一部分。综合 pkg/bplib/doc.md 的模块清单,按需可选的模块包括:

  • 存储实现bplib_stor_vfs_ordered(按紧急程度有序出队,本示例所用)、bplib_stor_vfs_unordered(按vfs_readdir顺序取回,速度更快但适合 contact/channel 少的场景)、bplib_stor_void(丢弃无法立即投递的 Bundle,仅适合叶节点与测试)。
  • 优化模块bplib_include_nc_telemetry(默认裁剪遥测表,省 3.3 KB)、bplib_include_as(默认裁剪管理统计表,省 7.5 KB)、bplib_no_vfs(配合bplib_stor_void可完全去掉 vfs 依赖)。
  • 初始化控制bplib_barebones可关闭默认启用的bplib_init/bplib_nc/bplib_fwp三个模块,便于自行替换。

常用配置宏(均定义于 pkg/bplib/doc.md):

说明
CONFIG_BPLIB_CLA_UDP_BUFLENUDP CLA 的 MTU,建议不超过约 1400
CONFIG_BPLIB_CLA_UDP_TIMEOUTUDP CLA stop 后线程终止的超时(ms)
CONFIG_BPLIB_MEMPOOL_LENBundle 发送/投递队列的 mempool 大小;越大可排队 Bundle 越多,mempool 满时新入站 Bundle 会被丢弃(每个 Bundle 至少约 700 字节的BPLib_BBlocks_t加上载荷块)
CONFIG_BPLIB_STOR_BASEBundle 存储路径前缀,默认/nvm0/bp
CONFIG_BPLIB_EGRESS_CACHE_LEN每个 channel/contact 的缓存队列中的 Bundle 引用数,越大越少搜存储,但更耗内存
CONFIG_BPLIB_TIME_FILE_NAME时间信息文件路径,注意不要放在存储实现会遍历的子目录里,否则可能被误认为 Bundle

十一、运行环境与限制

  • 推荐目标:本示例在native目标上配合 gnrc UDP 验证通过;也可尝试用 802.15.4 或nimble_netif承载 UDP,以在真实硬件上测试。BLE 直连场景请改用 BLE 示例。
  • tap 设备:即使只做 loopback,也需要可用的 tap 设备,因为 CLA 会尝试绑定一个。
  • BPv7 能力边界:如前所述,RIOT 移植的 bplib 7.0.2 不支持分片、状态报告、BIBE/BPSec,且 EID Scheme 仅支持ipn
  • 生产注意事项:应定期调用BPLib_STOR_GarbageCollect()防止存储无限增长;为保证跨启动周期的单调时间维护,至少应在重启/终止时调用BPLib_TIME_MaintenanceActivities(),或为应对意外断电而定期调用(pkg/bplib/doc.md)。

结语

通过本示例,可以在一台 Linux 机器上完整体验 DTN 的三个关键能力:基于 UDPCL 的 Bundle 传输、contact 中断下的持久化存储,以及多节点模拟下的存储转发。结合 main.c、Makefile 与 UDP CLA 源码 对照阅读,即可从"会跑示例"深入到"理解 DTN 协议栈在 RIOT 上的落地方式",为在真实硬件或更多收敛层(如 BLE L2CAP)上扩展 bplib 应用打下基础。

【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询