- 后端
- 物联网
- 消息队列
- 通信
【免费下载链接】emqx
The most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles
EMQX 的 CoAP 网关在connection_required = true的连接模式下,过去一旦底层 DTLS socket 关闭(sock_closed),对应的连接上下文与会话便会随之销毁,导致使用 CoAP over DTLS 的受限设备在网络抖动后必须重新走完整的建连认证流程。本篇文章基于仓库变更记录 changes/ee/fix-16996.en.md 展开,结合 emqx_coap_channel.erl 的通道状态机与 emqx_coap_dtls_connection_SUITE.erl 的测试用例,深入讲解修复后的两个核心行为:连接态下收到sock_closed后保留会话,以及使用相同clientid与有效token在新 DTLS 会话上完成重连接管(takeover)。读完本文,你将掌握该模式的配置方式、token 的签发与校验规则、断线重连的完整流程,以及如何通过仓库中的测试用例验证这些行为。
修复背景:UDP/DTLS 场景下的会话连续性痛点
CoAP 基于 UDP,本身是无连接的;当叠加 DTLS 提供传输安全时,底层 socket 的存续期与业务会话的存续期是解耦的。对于低功耗、弱网设备,DTLS 连接随时可能因网络波动、NAT 超时或设备休眠而中断。在修复之前,CoAP 通道在任意时刻收到sock_closed都会直接触发shutdown,导致:
- 已创建的连接上下文(
clientid、认证信息)丢失; - 后续设备重连时必须重新执行
POST /mqtt/connection完整建连流程; - 期间到达的订阅消息、观察(Observe)状态等会话数据无法衔接。
本次变更(对应仓库变更记录 fix-16996.en.md)的目标非常明确:
修复 CoAP DTLS 连接模式,使
sock_closed之后会话仍然可用,并支持使用相同clientid与有效token的重连接管。
理解连接模式:connection_required 开关
CoAP 网关存在两种工作模式,由配置项gateway.coap.connection_required控制(默认false),其语义在 rel/i18n/emqx_coap_schema.hocon 中有官方说明:
false(默认):无连接上下文(stateless),每个请求独立认证处理,适合纯发布/订阅的简单设备;true:客户端必须创建连接上下文,经过认证并保持心跳存活,适合依赖连接状态的设备流程。
配置项定义位于 emqx_coap_schema.erl,同文件还定义了heartbeat(默认30s)、notify_type、subscribe_qos、publish_qos、blockwise等参数。其中heartbeat仅在connection_required = true时生效,用于判定客户端是否离线(rel/i18n/emqx_coap_schema.hocon)。
在通道初始化时,该开关被读入通道记录并初始化为conn_state = idle(emqx_coap_channel.erl),此后整个生命周期都围绕idle / connected状态机运转。
会话建立:POST /mqtt/connection 与 token 签发
在连接模式下,客户端首先通过 CoAPPOST请求coaps://host/mqtt/connection建立连接。请求的 URI Query 携带认证与身份信息,仓库内部使用?QUERY_PARAMS_MAPPING定义短参数映射(emqx_coap.hrl):
| 短参数 | 完整参数 | 用途 |
|---|---|---|
c | clientid | 客户端标识 |
t | token | 会话令牌 |
u | username | 用户名 |
p | password | 密码 |
q | qos | 服务质量 |
r | retain | 保留消息标志 |
建连请求的识别逻辑在 emqx_coap_channel.erl:URI Path 必须等于[<<"mqtt">>, <<"connection">>]且方法为POST;DELETE同一路径则用于主动断开连接。
建连成功后,服务端签发会话令牌,核心逻辑在process_connect/5(emqx_coap_channel.erl):
- 调用
emqx_gateway_ctx:open_session/6打开会话; - 通过
rand:uniform(?TOKEN_MAXIMUM)生成随机数并转为二进制作为token(?TOKEN_MAXIMUM定义为4294967295,见 emqx_coap_channel.erl); - 将
token写入通道记录,并以 CoAP2.01 Created响应返回给客户端,响应 payload 即该 token。
客户端必须妥善保存该 token——它是后续所有请求的通行凭证,也是断线重连接管的关键。
心跳保活与主动断开
连接建立后,客户端需要周期性发送心跳。从测试用例可以看到,心跳请求同样是PUT /mqtt/connection?clientid=xxx&token=xxx(emqx_coap_dtls_connection_SUITE.erl)。通道侧由emqx_keepalive机制驱动(emqx_coap_channel.erl):心跳超时会触发{shutdown, timeout, ensure_disconnected(keepalive_timeout, Channel)},即判定离线。
主动断开则通过DELETE /mqtt/connection完成:当通道 token 为空且收到删除连接请求时,返回2.02 Deleted并正常关闭通道(emqx_coap_channel.erl)。
核心修复一:sock_closed 后的会话保留
本次修复的关键代码位于handle_info/2(emqx_coap_channel.erl):
handle_info( {sock_closed, _Reason}, #channel{connection_required = true, conn_state = connected} = Channel ) -> {ok, Channel}; handle_info({sock_closed, Reason}, Channel) -> shutdown(Reason, Channel);行为可以拆解为两个分支:
- 连接模式(
connection_required = true)且通道已处于connected状态:收到sock_closed时直接返回{ok, Channel},即不销毁通道、不终止会话、不注销客户端注册。DTLS socket 关闭只代表传输层断开,业务会话继续保留在网关侧,等待客户端重连接管; - 其他情况(连接模式但尚未 connected,或非连接模式):仍按原逻辑执行
shutdown,避免无连接上下文残留。
这一条件分支被单元测试 t_channel_connection_mode_sock_closed 精确覆盖:测试构造connection_required = true的通道,在conn_state = connected且已设置 token 的情况下向handle_info发送{sock_closed, ssl_closed},断言返回{ok, _}(会话保留);而在非 connected 状态或connection_required = false时,断言返回{shutdown, ssl_closed, _}(正常关闭)。
同时,集成测试 t_token_takeover_across_dtls_sessions 验证了更完整的链路:客户端关闭 DTLS socket 后,emqx_gateway_cm_registry:lookup_channels(coap, <<"client1">>)仍然返回非空——说明连接上下文与通道注册在 socket 关闭后依然存活。
核心修复二:相同 clientid + 有效 token 的重连接管
sock_closed后会话保留只是前提,真正的价值在于客户端重连后能够快速恢复。重连接管流程由check_token/2与try_takeover_with_token/4协同完成。
1. 连接模式下强制校验 token
连接模式下,任何非建连请求都会先经过check_auth_state/2(emqx_coap_channel.erl):
- 若是
POST /mqtt/connection建连请求,走正常的会话创建流程; - 否则必须从 URI Query 中解析出
token;缺失 token 的请求直接返回4.00 Bad Request(错误信息Missing token or clientid in connection mode,见 emqx_coap_channel.erl)。
2. token 与 clientid 的匹配校验
check_token/2(emqx_coap_channel.erl)进一步比对请求参数与通道记录:
- 请求
clientid、token均与通道一致:正常放行处理; - 相同
clientid但 token 不匹配:直接拒绝,返回4.01 Unauthorized(Invalid token or clientid in connection mode,见 emqx_coap_channel.erl); clientid与当前通道不同(重连接管场景):进入try_takeover_with_token/4。
3. 跨 DTLS 会话的接管
try_takeover_with_token/4(emqx_coap_channel.erl)通过网关连接管理器查询旧连接:
emqx_gateway_cm:call(coap, ReqClientId, {check_token_and_get_clientinfo, ReqToken})旧通道在handle_call({check_token_and_get_clientinfo, ReqToken}, ...)(emqx_coap_channel.erl)中完成 token 比对,返回脱敏后的 clientinfo(明确移除了password字段,避免通过接管探测接口传播敏感凭据)。token 校验失败或找不到旧通道时返回undefined/false,统一以4.01 Unauthorized拒绝。
校验通过后进入takeover_and_handle_request/5(emqx_coap_channel.erl):
- 合并旧连接的 clientinfo:将
username、is_superuser、auth_expire_at、mountpoint、enable_authn等认证与挂载信息从旧通道恢复(merge_takeover_clientinfo/3,emqx_coap_channel.erl); - 调用
emqx_gateway_ctx:open_session/6打开会话,present = true表示会话已存在(被接管); - 将新请求的 token 写入新通道并置为
connected状态,随后正常处理该请求。
接管完成后,同一clientid在注册表中只保留一个通道——集成测试 t_token_takeover_across_dtls_sessions 通过?assertEqual(1, length(emqx_gateway_cm_registry:lookup_channels(coap, <<"client1">>)))验证了这一点。
4. 安全边界:错误 token 与错误 clientid 均被拒绝
接管机制必须杜绝身份冒用。测试套件对此做了充分验证:
- t_invalid_token_rejected:携带错误 token 重连,请求返回
4.01 Unauthorized; - t_wrong_clientid_with_valid_token_rejected:token 有效但
clientid与签发 token 的客户端不一致,同样被拒绝; - t_partial_token_params_rejected:只带
clientid或缺省clientid的请求返回4.00 Bad Request。
完整配置示例
以下配置同时覆盖 UDP(5683)与 DTLS(5684)监听器,并开启连接模式:
gateway.coap { ## 连接模式开关:true 表示客户端必须建立连接上下文 connection_required = true ## 心跳间隔,仅 connection_required = true 时生效,默认 30s heartbeat = "30s" ## 观察主题消息的 CoAP 消息类型:non | con | qos(qos 按 MQTT QoS 自动映射) notify_type = qos ## 订阅/发布缺省 QoS:qos0 | qos1 | qos2 | coap subscribe_qos = qos1 publish_qos = qos1 ## 主题挂载点 mountpoint = "coap/" listeners.udp.default { bind = "5683" max_connections = 1024000 max_conn_rate = 1000 } listeners.dtls.default { bind = "5684" enable_authn = false dtls_options { verify = verify_none } } }该配置与 DTLS 测试套件的初始化配置基本一致(emqx_coap_dtls_connection_SUITE.erl)。测试中 DTLS 客户端通过er_coap_dtls_socket:connect({127,0,0,1}, 5684, [{verify, verify_none}])建连,随后访问coaps://127.0.0.1/mqtt/connection完成建连(emqx_coap_dtls_connection_SUITE.erl)。注意:通过emqx.conf配置网关为单节点生效,通过 Dashboard 或 HTTP API 配置则集群生效(见 CoAP 网关 README)。
端到端流程回顾
综合源码与测试,连接模式下的一次断线重连完整链路如下:
- 设备 DTLS 连接建立,
POST coaps://host/mqtt/connection?clientid=...&username=...&password=...完成认证,服务端签发 token(2.01 Created); - 设备周期性发送
PUT /mqtt/connection?clientid=...&token=...心跳保活; - 网络波动导致 DTLS socket 关闭,通道收到
{sock_closed, _}——因处于connected状态且连接模式开启,会话与注册保留; - 设备重新建立 DTLS 连接,直接携带相同
clientid与 token 发送业务请求; - 网关校验 token,向旧通道发起接管探测,恢复认证上下文并打开既有会话,请求得到正常处理;
- 若 token 无效、clientid 不匹配或参数缺失,分别以
4.01 Unauthorized/4.00 Bad Request拒绝,会话不被冒用。
CoAP 网关整体的消息处理分层(通道 → 认证检查 → 会话 → 传输管理器 → pubsub/mqtt handler)可参考仓库中的流程图 apps/emqx_gateway_coap/doc/flow.png:认证检查在通道入口完成,业务逻辑按消息类型分发给 pubsub handler 与 mqtt handler,正是本修复涉及的check_auth_state→check_token分支所处的位置。
EMQX CoAP 网关消息处理流程
总结
本次变更从两个层面解决了 CoAP DTLS 连接模式下的会话连续性问题:断开保留(sock_closed不再销毁 connected 态会话)与安全接管(仅相同clientid+ 有效token可跨 DTLS 会话恢复连接上下文)。这既降低了弱网设备重连的成本,又通过 token 强校验避免了身份冒用。若需在生产环境启用,请务必:为设备提供可靠的 token 持久化能力、按设备实际心跳节奏合理设置heartbeat,并保持connection_required = true下的请求参数完整性(clientid与token缺一不可)。
- 后端
- 物联网
- 消息队列
- 通信
【免费下载链接】emqx
The most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles
相关推荐
ShareJS连接管理详解:如何处理断线重连和会话恢复?
ShareJS连接管理详解:如何处理断线重连和会话恢复? ShareJS作为一款强大的实时协作编辑框架,其连接管理机制是确保应用稳定运行的核心。在复杂的网络环境
后端Librespot会话管理:连接状态跟踪与自动重连
Librespot会话管理:连接状态跟踪与自动重连 Librespot作为开源Spotify客户端库,其会话管理系统是确保音乐播放不中断的核心组件。本文将深入解
音频处理bRPC连接优化:长连接保活与心跳机制的实现
bRPC连接优化:长连接保活与心跳机制的实现 长连接保活与心跳机制的重要性 在分布式系统中,网络连接的稳定性直接影响服务的可靠性和性能。传统的短连接方式每次通信
后端RPC框架通信网络
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考