HarmonyOS 7 MQTT:QoS1离线发件箱与会话续接
发布时间:2026/10/7 8:06:06 作者:尧图编辑部 阅读量:1,286

一、网络恢复了八条指令却发出了九次RelayMeter 是一个现场电表控制 Demo。页面上点“读取参数”“校准时钟”“切换采样档位”命令通过 MQTT over WSS 发给设备。实验室 Wi-Fi 稳定时一切正常到了地下配电间网络会在 5G、无网和弱 Wi-Fi 之间切换原本不显眼的状态问题一次暴露出来。19:42 的那轮测试里离线发件箱有 8 条指令其中 1 条已经过期网络恢复后应当发布 7 条并收到 7 个 PUBACK。实际日志却出现两次重复回执页面一度把已经完成的cmd-2048又标回“等待中”。原因不是 MQTT.js 不会重连而是应用同时启用了库的自动重连、Network Kit 的网络回调重连和页面onPageShow重连三个入口抢着改同一份状态。我把任务定为MQTT-1942固定客户端 IDmeter-7A19主题meters/7A19/commandsQoS 为 1会话过期时间 1800 秒。验收值也先写死重连 3 次sessionPresenttrue发件箱8 → 0过期 1发布 7PUBACK 7重复回执收敛 2最终状态必须是SYNCED。二、QoS 1 只保证至少一次不替业务决定“只能执行一次”MQTT.js 已经处理 PING、QoS 流程和重连但 QoS 1 的语义是至少一次。客户端没看到 PUBACK 时会重发Broker 或设备端就可能再次收到同一条命令。协议层的 packetId 也不能直接当业务幂等键重新建立会话、应用重启或队列迁移后它的生命周期和业务命令并不等价。所以 RelayMeter 在载荷里放稳定的commandId例如cmd-2048设备端按这个 ID 去重并回传业务回执。应用本地则维护AckLedger同一commandId的后续回执只更新观测计数不再逆转已经完成的 UI。PUBACK证明 Broker 接收了发布包业务回执证明设备处理了命令两者不能混成一个“成功”。Network Kit 在这里负责回答“当前是否有可用网络”MQTT.js 负责协议连接与会话ArkData 发件箱负责跨进程重启保存命令。三层各管一段页面只订阅聚合后的状态机OFFLINE → RECONNECTING → SESSION_RESUMED → DRAINING → SYNCED。三、重连入口只留一个退避节奏由协调器掌握第一个改动是关掉 MQTT.js 的固定周期自动重连把重连权交给MqttRecoveryCoordinator。这样 Network Kit 的netAvailable只发信号不直接创建第二个客户端。客户端 ID 必须稳定cleanfalseMQTT 5 会话过期时间设置为 1800 秒如果每次都随机 clientIdBroker 不可能返回旧会话。import{connectAsync,MqttClient}frommqtt;exportclassMqttRecoveryCoordinator{privateclient?:MqttClient;privategeneration0;asyncreconnect(url:string):Promisevoid{constgenthis.generation;awaitthis.client?.endAsync(true);constnextawaitconnectAsync(url,{clientId:meter-7A19,protocolVersion:5,clean:false,reconnectPeriod:0,keepalive:30,properties:{sessionExpiryInterval:1800}});if(gen!this.generation){awaitnext.endAsync(true);return;}this.clientnext;this.acceptConnack(next,gen);}}这里用generation隔离迟到连接。Network Kit 连续报告网络切换时旧 WSS 握手可能比新握手晚完成没有代次判断就会把页面重新绑到旧客户端。退避采用 1、2、4、8 秒并加少量抖动网络真正可用后才启动页面显示RECONNECTING但不会为每次网络属性变化都弹错误。生命周期也要成对处理。aboutToAppear注册 Network Kit 与 MQTT 事件aboutToDisappear解除页面观察者客户端由应用级协调器持有不随页面销毁就强制断开。只有账号退出、设备解绑或应用明确停用连接时才endAsync。把连接所有权放页面里会让一次返回键变成一次无意义的 Broker 重连。四、发件箱先落盘再谈 publish第二个问题是离线点击如何保存。MQTT.js 有 outgoing store但不同运行环境的持久化能力并不等于 HarmonyOS 应用级事务日志。RelayMeter 使用 ArkData 表保存commandId、topic、payload、createdAt、expiresAt、attempt 和状态。用户点击后先插入PENDING再由 drain 流程发布即使进程此时退出命令仍在。asyncdrain(now:number):Promisevoid{if(!this.client?.connected||this.draining)return;this.drainingtrue;try{constrowsawaitthis.outbox.listPending(meter-7A19);for(constrowofrows){if(row.expiresAtnow){awaitthis.outbox.markExpired(row.commandId);continue;}awaitthis.outbox.markInflight(row.commandId,row.attempt1);awaitthis.client.publishAsync(row.topic,row.payload,{qos:1,properties:{messageExpiryInterval:Math.ceil((row.expiresAt-now)/1000)}});awaitthis.outbox.markPubAcked(row.commandId);}}finally{this.drainingfalse;}}publishAsync返回后记录PUBACKED但不马上删除行因为设备业务回执可能稍后到达。真正完成后进入COMPLETED保留一个短期去重窗口再清理。过期命令不补发尤其“开闸”“停机”这类时效动作晚到比丢失更危险。批量 drain 也有上限本次每批最多 4 条避免网络刚恢复就把 7 条指令和订阅流量一起塞进链路。五、sessionPresent 不是一张“什么都不用做”的通行证cleanfalse且 Broker 保留了会话时CONNACK 的sessionPresent会是 true。它说明服务端找到了旧会话不代表本地发件箱已经和服务端完全一致。应用仍要对账本地INFLIGHT是否收到 PUBACK业务回执是否已落账订阅是否由旧会话保留。如果sessionPresentfalseRelayMeter 会重新订阅meters/7A19/acks并把INFLIGHT行退回PENDING让它们按照相同 commandId 再发如果为 true则保留 Broker 会话避免无意义重复订阅但仍扫描发件箱中没有完成证据的行。这个分支写在协调器不散落在页面按钮里。第三段代码处理业务回执。它用 commandId 做一次提交重复回执只增加duplicateAck指标不覆盖最终状态。asynconDeviceAck(payload:Uint8Array):Promisevoid{constackthis.codec.decodeAck(payload);constreservedawaitthis.ackLedger.reserve(ack.commandId);if(!reserved){this.metrics.duplicateAck1;return;}awaitthis.outbox.complete(ack.commandId,ack.deviceRevision);this.stateStore.commit({commandId:ack.commandId,status:COMPLETED,deviceRevision:ack.deviceRevision});}reserve必须和账本写入处于同一事务边界。先更新 UI、再写去重记录会在进程退出窗口留下“看起来成功、下次又执行”的缝隙。设备端也需要同样的幂等键客户端单边去重无法阻止命令被设备执行两次。六、日志要同时看网络、协议和业务三条线这次我没有只打印connectedtrue。HiLog 的第一行记录任务、clientId、QoS 和离线数量第二行记录reconnect3 sessionPresenttrue expiry1800s第三行给出outbox8 expired1 publish7第四行是puback7 duplicateAck2最后一行才宣布outbox0 stateSYNCED。网络线解释为什么开始重连协议线解释会话是否续接、PUBACK 是否齐全业务线解释设备回执是否重复、命令是否真正完成。三条线放到同一个任务 IDMQTT-1942下才不会把“网络恢复”误判成“设备状态已经同步”。七、最终页面把恢复过程留在屏幕上运行页顶部是稳定客户端meter-7A19和SESSION_RESUMED中间展示 Broker、主题、QoS、会话过期时间与当前网络下面是发件箱和回执统计。最终值为 Offline queued 8、Expired 1、Published 7、PUBACK 7、Duplicate ACK 2、Outbox 0。状态轨迹完整显示OFFLINE → RECONNECTING → SESSION_RESUMED → DRAINING → SYNCED。我特意保留“模拟断网”和“重新对账”两个按钮。前者只断开测试网络不清本地队列后者重新读取账本与 Broker 会话不生成新命令。19:42:08 的最终结果证明发件箱已经清空而不是靠刷新页面把数字藏掉。八、哪些事情不能交给这套 Demo 自动决定首先Broker 地址、认证令牌和 WSS 证书策略要按产品环境配置示例地址wss://iot.example.com/mqtt只是 Demo 标识。其次QoS 1 不提供业务事务语义涉及支付、门禁或高危设备时必须由服务端和设备端共同实现 commandId、权限、时效与审计。再次后台执行受系统策略约束不能因为使用 MQTT 就假设应用能永久保活。最后重连次数不是越多越可靠。真正稳定的实现是网络回调只唤醒一个协调器协调器只维护一个有效客户端发件箱只按证据推进状态重复回执只被观测而不反向污染 UI。把这几条边界守住后弱网恢复才从“碰巧连上了”变成可以复盘的工程过程。