让 TLS 与 DTLS 共用业务逻辑:Secure Channel 分层设计
小型化设备接入本地中枢时,需要完成激活、设备信息同步、状态上报和心跳。产品可能使用 TLS/TCP,也可能使用 DTLS/UDP。业务流程基本相同,底层连接方式却存在实质差异。
直接复制两套业务代码,容易让修复和功能演进产生分歧;在每个业务函数中增加 TLS/DTLS 分支,又会让状态机与网络细节纠缠在一起。对 RAM、Flash、任务栈和调度资源都有限的设备,这些重复实现和模糊的资源归属最终会影响产品稳定性。
本文以 OpenHarmony 统一互联 3.0 的 iot_connect 为例,说明 Application Terminal 与 Secure Channel 如何分工,以及怎样通过构建期后端选择,让同一套业务逻辑适配 TLS 和 DTLS。文中同时给出源码接口、生命周期、构建配置和验证方法。
一、为什么设备需要一条安全通信信道
1. Wi-Fi 入网只解决网络可达
WS63 Combo 终端通过 BLE 获得配置并接入 Wi-Fi 后,还需要找到正确的 Hub、证明自己持有合法凭据,并完成业务激活。此后的数据上报和心跳也依赖同一条安全会话。
因此,上层业务需要一个可持续使用的通信对象:它有明确对端、收发入口、错误通知和关闭方法。底层应管理连接与资源,上层据业务响应决定是否进入 ONLINE、是否重试、是否重新发现 Hub。这样才能从“一次请求成功”进一步做到“设备长时间可运行”。
2. 发现信息不能代替安全认证
LAN Search 提供候选 Hub 的域、地址、端口和能力;发现结果用于定位服务和筛选候选。终端随后以管理员 PSK 和对应 identity 建立安全会话,再发送激活与状态数据。
这里的 PSK 用来参与认证和密钥协商,并不作为普通业务字段明文发送。identity 帮助 Hub 查找相应凭据,它与密钥本身有不同的保密要求。实现中仍应减少密钥副本、明确凭据消费时机,并避免将密钥写入日志。
3. 为什么保留两种传输实现
面对支持不同安全传输的 Hub,终端必须选择匹配的协议组合。TLS/TCP 使用可靠字节流;DTLS/UDP 使用数据报。两者在 CoAP 编码、连接能力协商、对端地址处理及重传机制上都有差别。
当前设计支持两种独立的产品构建。WS63 联调选择 DTLS;另一产品可选择 TLS。某一镜像只包含选定的 Secure Channel 后端,实现中没有运行时自动降级到另一后端的流程。
选择时首先要满足对端协议,再评估平台支持、网络条件和资源。UDP 没有 TCP 连接状态,但 DTLS 握手、密码运算、CoAP 重传和缓存同样占用资源,不能仅凭传输名称判断哪个实现更省内存。
二、资源有限,为什么更要把职责拆开
对小型化设备,设计目标包括代码复用,也包括按产品裁剪、限制运行对象数量、让失败后的资源可回收。分层使这些目标有明确的实施位置。
| 约束 | 容易出现的代价 | 当前设计的应对方式 |
|---|---|---|
| Flash 有限 | 为 TLS/DTLS 各维护一份上线业务代码 | 共用 Application Terminal,构建期选择一个后端 |
| RAM 与堆空间有限 | 重复会话、重复 Payload、副本长期驻留 | 明确会话所有者、凭据消费后清理、限制响应快照 |
| 定时器与 Socket 有限 | 失败重试遗留监听、计时器或 Socket | 公共关闭路径统一回收调度和传输资源 |
| 事件回调中存在对象引用 | 在回调栈内释放 Link,引发无效访问 | 错误延后交给调度处理,上层再决定关闭和重连 |
| 产品与协议组合增多 | 相同业务在不同后端产生不同修复 | 业务流程共用测试,后端差异单独验证 |
分层本身不会自动降低 RAM,也不意味着每一层都需要一个任务、一条队列或一份缓存。本实现主要通过模块接口划分职责,复用既有调度机制。是否真的节省资源,要检查最终产物和运行测量。
四个模块分别负责什么

图 1:业务与安全传输的职责边界。 后端分支表示不同镜像的构建选择。
Application Terminal 决定业务顺序、响应解释和恢复策略。Secure Channel 公共层管理一条信道拥有的运行资源。后端提供 Socket、CoAP codec、连接后动作及 peer 信息。LAN Search 则在会话建立之前完成候选发现。
这带来一个可检查的边界:新增业务请求时,通常无需修改后端;更换安全传输时,不应复制激活和心跳状态机。Secure Channel 不解析 /hub/activate 的业务 errCode,也不决定设备何时进入 ONLINE。
三、公共接口怎样承接业务需求
公共头文件是 core/network/transport/secure_channel/include/net_secure_channel.h。下面摘录关键接口:
int32_t NetSecureChannelOpen(const NetSecureChannelParam *param,
NetSecureChannel **channel);
void NetSecureChannelClose(NetSecureChannel *channel);
NetCoapEndpoint *NetSecureChannelGetEndpoint(NetSecureChannel *channel);
const NetSocketAddr *NetSecureChannelGetPeerAddress(NetSecureChannel *channel);
NetSecureChannelTransport NetSecureChannelGetTransport(void);
Open 接收地址、端口、PSK identity、PSK、Endpoint 用户数据及回调上下文。参数中的两个回调各有明确用途:
| 参数 | 用途 |
|---|---|
onCredentialConsumed | Socket 创建阶段消费凭据后,通知调用者清理临时凭据 |
onError | Link 运行错误延后通知业务层 |
endpointUserData | 将 CoAP 回调关联到所属业务上下文 |
当前 Open 的结果通过返回值报告。 代码依次创建传输资源、启动连接、接入调度,成功后写出 channel;不能将它描述成“发起连接后等待成功回调”。其中存在异步错误通知,并不意味着整个 Open 接口采用异步完成契约。
为什么先绑定上层会话对象
NetAppTerminalSessionOpen() 先检查发现能力与当前后端是否匹配,再加载管理员 PSK、分配业务侧会话适配对象,并将它绑定到上下文,然后调用公共 Open。
这样,在建连末段排队的异步错误到达时,业务层能找到它对应的会话。若 Open 失败,适配层撤销绑定并释放自身对象;公共层负责释放自己已经创建的资源。

图 2:公共 Open 的主要调用顺序。 Socket、Link、Session 和 Endpoint 在连接启动之前准备完成。图中省略了各阶段的失败退出路径,失败处理统一进入公共释放流程。
临时凭据的生命期比会话短
凭据读取会产生临时列表和候选记录。管理员记录选定后,查询缓冲和候选副本清零;Socket 消费建链参数后,再通过 onCredentialConsumed 清理调用者的临时凭据。Open 返回后的清理还覆盖提前失败的路径。
这不会删除持久化 PSK,也不等于密码库不再持有握手和会话所需的内部状态。它减少的是应用层不必要的额外密钥副本。
四、TLS 和 DTLS 的差异具体落在哪里
两个后端实现同一组 NetSecureChannelBackend* 函数。公共层直接调用这些统一符号,构建清单决定最终链接哪一个实现,无需为每次业务请求运行一套后端选择逻辑。
| 后端职责 | TLS/TCP 当前实现 | DTLS/UDP 当前实现 |
|---|---|---|
| 创建 Socket | NetTransSocketTlsNew | NetTransSocketDtlsNew |
| CoAP 编解码 | NetCoapTcpPlainEncode/Decode | NetCoapUdpEncode/Decode |
| 连接后动作 | 发送 CSM,声明端点能力 | 不发送 CSM |
| CoAP 对端参数 | 由已连接的 TCP 通道承载,返回 NULL peer | 从 Hub 地址生成显式 peer |
| CoAP 报文重传 | 不使用 UDP CON 的重传方式 | 为 CON 报文启用重传 |
| 配置的密码套件 | PSK AES-128-GCM-SHA256,0x00A8 | DHE-PSK AES-128-GCM-SHA256,0x00AA |
表中的套件是该实现选用的配置,不能推广为所有 TLS 或 DTLS 系统的固定选择。实际互通要求终端与 Hub 的协议版本和套件兼容。
TcpPlain 是当前 CoAP 编解码器的名称。它处理的是交给 TLS Socket 之前的 CoAP 数据,不表示网络上的业务报文以明文传输。
差异一:字节流和数据报需要不同的编码
TCP 的读取边界不等于业务消息边界,CoAP over TCP 需要从字节流中识别消息长度。UDP 则以数据报承载 CoAP 消息。当前 TLS 后端将 NetCoapTcpPlainGetRemainSize 交给 Socket 配置,用于识别还需要读取的数据长度;DTLS 后端选择 UDP codec。
CSM(Capabilities and Settings Message)属于 CoAP 可靠传输的能力与设置交互。CoAP over TCP 也没有 UDP CoAP 的消息类型和 Message ID 字段。因此,即使上层复用请求结构,也不能推断两种传输在线上具有相同报文格式。RFC 8323,第 3 节与第 5.3 节
差异二:安全性与可靠性要分别处理
DTLS 保护数据报的安全传输,业务请求的可靠交互还要由 CoAP 与业务层配合。UDP CoAP 的 CON 消息通过确认和重传处理报文丢失;空 ACK 可以先到达,业务响应随后单独返回。收到 ACK 并不意味着业务处理成功。RFC 7252,第 4.2 节与第 5.2 节
当前 DTLS 后端明确配置了重传缓存:
#define NET_SECURE_CHANNEL_DTLS_RETRANS_BUFFER_SIZE 4096
int32_t ret = NetCoapEndpointRetransEnable(
endpoint,
NetCoapEndpointRetransDefaultCheckFunc,
NET_SECURE_CHANNEL_DTLS_RETRANS_BUFFER_SIZE);
这段摘录来自 net_secure_channel_dtls.c。4096 字节是该功能的缓存配置值,不是整个 DTLS 会话的 RAM 开销。总占用还包括收发缓冲、密码库上下文、握手临时数据、Socket 和协议对象。
对于资源有限的设备,这个例子说明了为什么要看源码和测量:选 UDP 可以改变传输开销,却不会让可靠性成本消失;部分成本会体现在协议重传与缓存中。
五、通过构建选择后端,把可移植性留在源码里
本工程使用 GN 与 CMake 两条构建路径,二者采用同一策略:公共源始终加入,后端源按参数二选一。
下面是 secure_channel.gni 的核心源码清单:
iotc_core_network_secure_channel_src = [
"$secure_channel_path/net_secure_channel.c",
"$secure_channel_path/net_secure_channel_${iot_connect_secure_transport}.c",
]
iot_connect_secure_transport 只允许 tls 或 dtls。CMake 的对应逻辑位于同目录的 CMakeLists.txt,以下为相关摘录:
set(SECURE_CHANNEL_TRANSPORT "tls" CACHE STRING
"Secure channel transport: tls or dtls")
set(IOTC_SRC_LIST
${IOTC_SRC_LIST}
${CMAKE_CURRENT_SOURCE_DIR}/net_secure_channel.c
${CMAKE_CURRENT_SOURCE_DIR}/net_secure_channel_${SECURE_CHANNEL_TRANSPORT}.c
PARENT_SCOPE
)
完整配置还校验合法值,并限制当前 DTLS 路径使用 mbedTLS。GN 通过断言拒绝不支持的组合;CMake 对 DTLS 与 openHiTLS 的组合报配置错误。这是当前工程后端的支持范围,不是对密码库一般能力的判断。

图 3:后端选择必须从配置追踪到产物。
单后端选择的收益与边界
直接收益是 Secure Channel 不需要同时链接两个后端,也不必为 TLS 和 DTLS 各复制一套 Application Terminal。以统一接口隔离差异后,新产品选择后端通常不需要改动业务流程。
但“Secure Channel 只选 DTLS”不等于整个系统中所有 TCP 代码都被移除。其他模块可能仍依赖 TCP,底层密码库也可能编入共享能力。最终裁剪效果要看链接结果,不能只凭一个 GN 参数下结论。
同样,公共 GN 默认值是 TLS,而某个产品可以覆盖为 DTLS。验证某次构建时,应查看实际生效的参数和后端对象,而不是仅查看公共默认值。
PSK 开关与传输选择是两个问题
iot_connect_mbedtls_psk_support 决定适配层是否提供 PSK 配置支持,iot_connect_secure_transport 决定选哪种安全传输。即使已经选中 DTLS,如果 PSK 能力没有进入产物,凭据设置仍可能在握手前失败。当前公共 GN 配置已开启 PSK;产品集成仍应核对实际生效值。
六、统一管理资源,才能经得起反复断线
小型化设备可能在弱网中多次建连失败。若一次失败遗留一个 Socket、定时器或 Endpoint,单次启动看不出问题,长时间运行却会逐渐耗尽资源。
NetSecureChannel 统一持有收发缓冲、Socket、Link、Session、Endpoint、事件源、接收监听和错误定时器。公共释放路径先撤销调度入口,再处理传输对象与缓冲。

图 4:按依赖关系回收资源。 是否需要释放某项,由该项是否已经创建、注册或移交所有权决定。
其中 Socket 的所有权很关键:Link 创建成功后负责释放它;若 Link 尚未创建,Socket 仍需公共层直接释放。代码在释放 Link 后清空 Socket 指针,避免随后再次释放同一对象。
为什么错误回调需要延后处理
传输回调正在使用 Link 时,如果业务层立即关闭会话并释放 Link,回调返回后的代码可能继续访问已释放对象。当前实现将 Link 错误交给一次性调度任务处理,再通知业务层;定时器创建失败时尝试异步执行队列兜底。
图 5:将错误报告与对象释放放在不同的调用时机。
这类机制仍需测试资源耗尽场景:如果计时器、任务分配或队列投递都失败,不能因为有兜底分支就认定所有故障已经覆盖。更有价值的检查是反复失败、关闭、重连后,资源计数与堆占用是否恢复到可解释的水平。
七、业务层复用的前提是接口语义明确
Application Terminal 从当前会话取得 Endpoint 和 peer,然后构造 /hub/activate、/hub/syncDevInfo、/dataReport 与 /hub/heartbeat 请求。后端决定编码和传输方式;业务层处理响应和状态迁移。
其中有三种不同的成功条件:
| 条件 | 表示什么 |
|---|---|
| 发送函数返回成功 | 请求进入了发送流程 |
| 协议层收到确认或响应 | 有了对应的网络/协议反馈 |
| 业务结果解析成功 | 本次业务操作满足进入下一阶段的条件 |
业务层还需要保留尚未完成的意图。当前运行期请求有 runtimePending 和 runtimeResultPending 两个阶段:前者表示请求在途,后者表示结果已经到达但还未消费。新状态上报遇到这两个阶段时设置 reportDirty,稍后读取最新设备状态合并补报。
响应快照也有明确的内存上限。当前激活响应与运行期响应各配置 128 字节缓冲。这限制了上下文的固定开销,同时要求协议演进时评估响应大小,并检查超长响应的失败处理。有限缓冲、请求串行化和补报合并都是资源策略,它们需要业务语义支持,而不是仅靠传输层解决。
八、怎样验证分层和小型化确实有效
代码结构清晰只是第一步。验证应覆盖业务一致性、后端差异、资源回收及实际资源开销。
| 验证方向 | 具体做法 | 判断依据 |
|---|---|---|
| 构建选择 | 分别构建 TLS 和 DTLS 产品,查看有效配置和对象清单 | 只链接对应 Secure Channel 后端 |
| 业务一致性 | 对两种构建运行相同的激活、同步、上报与超时用例 | 状态迁移与业务结果一致 |
| 后端特性 | TLS 检查 codec/CSM,DTLS 检查 UDP codec/peer/CON | 行为符合各自协议 |
| 错误回收 | 在资源创建各阶段模拟失败,反复关闭和重连 | 无遗留监听、定时器、重复释放 |
| 资源测量 | 记录镜像、静态区、堆峰值、任务栈与协议对象数量 | 数据能关联到具体配置和阶段 |
测量 TLS/DTLS 差异时,应固定产品功能、编译优化、日志级别、测试负载和网络条件,并记录密码套件。当前两种后端所选套件不同,因此直接比较握手耗时,得到的是整个配置组合的差异,不能把全部差值归因于 TCP 或 UDP。
RAM 测量至少观察初始化后、握手峰值、稳定在线、重传期间和关闭后的状态。只看链接 Map 无法反映密码运算临时内存、重传缓存与运行期堆峰值;只看在线稳定时的剩余堆,也可能漏掉握手阶段的最高压力。
本文有 WS63 DTLS 正常接入和保活的三端联调证据,TLS 真机与完整异常矩阵需独立验证。本文不提供未经测量的 Flash/RAM 节省比例。
九、代码阅读顺序与复用边界
以下路径均相对于 foundation/communication/iot_connect/:
| 阅读顺序 | 文件 | 核心问题 |
|---|---|---|
| 1 | core/network/app_terminal/net_app_terminal_session.c | 业务怎样检查能力、选择凭据并打开会话 |
| 2 | core/network/transport/secure_channel/include/net_secure_channel.h | 公共接口向上层承诺什么 |
| 3 | core/network/transport/secure_channel/net_secure_channel.c | 对象如何创建、接入调度和释放 |
| 4 | 同目录 net_secure_channel_tls.c、net_secure_channel_dtls.c | 哪些差异必须留在后端 |
| 5 | 同目录 secure_channel.gni、CMakeLists.txt | 构建怎样只选择一个后端 |
| 6 | core/network/app_terminal/net_app_terminal_coap.c、net_app_terminal_svc.c | 业务怎样收发、消费结果和恢复 |
复用到另一款 Wi-Fi 应用终端时,仍需适配产品网络事件、凭据来源和设备模型。用于 Hub Server 时,还要处理监听、接入认证和多会话管理,不能直接套用应用终端的客户端上线状态机。
对资源有限的产品,一套可持续维护的通信设计应同时具备稳定的业务接口、可裁剪的传输实现和明确的资源生命周期。Application Terminal 与 Secure Channel 的分工把这三件事落实到代码、构建和验证中,才使“双传输支持”能够服务于小型化设备。
更多推荐
所有评论(0)