
WebSocket 很容易写出一个“演示正常”的版本监听open、message、close、error再connect()。真实问题出现在前后台切换和弱网里。Socket Resume Lab的最初版本能重新连接却出现两个问题同一 topic 收到重复消息离线期间的消息也没有补回来。页面显示“已连接”业务数据却已经断层。所以这篇不把“socket 再次 open”当恢复成功而是把重连、重新订阅和消息游标放到一条状态链里。一、先定义什么叫恢复成功本轮验收数据Sessionws_resume_20261001_10ForegroundtrueStateLIVEAttempt2Backoff2000 msSubscriptions3Duplicate Subscriptions0Cursormsg_10428Recovered Messages7Last RTT54 msWebSocket 流程很直接创建对象监听 open/message/close/error建立连接不再需要时关闭。工程化关键是“谁持有连接”。项目结构entry/src/main/ets/ ├── pages/SocketResumePage.ets ├── network/SocketSession.ets ├── network/SubscriptionRegistry.ets ├── data/CursorStore.ets └── ability/EntryAbility.ets页面只展示状态连接由SocketSession统一管理。二、第一段代码连接对象不能跟着页面反复创建最早我把 WebSocket 写在页面里回前台后页面重建旧 socket 还没退出新 socket 已开始工作重复订阅就从这里产生了。因此连接对象必须有唯一主人。import{webSocket}fromkit.NetworkKitexportclassSocketSession{privatesocket:webSocket.WebSocket|nullnullprivateconnecting:booleanfalseprivateforeground:booleantrueconnect():void{if(!this.foreground||this.connecting||this.socket){return}this.connectingtrueconstwswebSocket.createWebSocket()this.socketws ws.on(open,(){this.connectingfalsethis.recoverAfterOpen()})ws.on(message,(_err,message){this.onMessage(message)})ws.on(close,(){this.socketnullthis.connectingfalsethis.scheduleReconnect()})ws.on(error,()this.scheduleReconnect())ws.connect(this.url)}}页面生命周期不再决定 socket 生命周期。账号切换、应用退出或业务不再需要实时连接时应主动 close 并释放引用。三、第二段代码失败后不要固定 1 秒暴力重试弱网下最危险的写法是setTimeout(connect, 1000)。服务端故障一分钟客户端就会制造大量无意义连接。Demo 使用指数退避1 秒、2 秒、4 秒、8 秒上限 30 秒。本轮第二次尝试因此Backoff 2000 ms。privateattempt:number0privatereconnectTimer:number-1privatebackoffMs:number1000privatescheduleReconnect():void{if(!this.foreground||this.reconnectTimer0){return}this.attempt1this.backoffMsMath.min(1000*Math.pow(2,this.attempt-1),30000)this.stateRECONNECTINGthis.reconnectTimersetTimeout((){this.reconnectTimer-1this.connect()},this.backoffMs)}正式项目还可以加入随机抖动。我不会在 open 回调第一行就把 attempt 清零只有订阅和消息恢复完成后才清。四、第三段代码订阅要分“期望状态”和“连接状态”重复消息的根源不是 WebSocket 本身而是我把历史 subscribe 操作机械重放。更稳定的做法是维护两组集合desired表示业务想要的 topicactive表示本次连接已经确认过的 topic。断线只清 active不清 desired。classSubscriptionRegistry{privatedesired:SetstringnewSet()privateactive:SetstringnewSet()want(topic:string):void{this.desired.add(topic)}resetConnectionState():void{this.active.clear()}asyncresubscribe(send:(topic:string)Promisevoid):Promisevoid{for(consttopicofthis.desired){if(this.active.has(topic)){continue}awaitsend(topic)this.active.add(topic)}}}本轮三个 topic 是topic:news、topic:chat、topic:system最终Duplicate Subscriptions 0。这证明恢复逻辑是从当前业务状态推导而不是重放历史操作。DevEco 里我主要盯onForeground connect attempt2 backoff2000ms subscriptions3 duplicate0 cursormsg_10428 recovered7 State: RECONNECTING - LIVE五、第四段代码游标必须在业务确认后推进离线消息能否补齐取决于游标怎么保存。如果刚收到消息就把 cursor 写进 Preferences但业务处理还没完成此时应用异常退出下次会从“已经写过但还没真正处理”的位置继续消息就丢了。所以 Demo 只有业务提交完成后才更新游标。privateasynccommitMessage(message:PushMessage):Promisevoid{awaitthis.messageStore.apply(message)awaitthis.cursorStore.save(message.id)this.cursormessage.id}privateasyncrecoverAfterOpen():Promisevoid{this.stateRESUBSCRIBINGawaitthis.registry.resubscribe((topic)this.sendSubscribe(topic))this.stateRECOVERINGconstcursorawaitthis.cursorStore.load()constmissedawaitthis.fetchMissedMessages(cursor)for(constmessageofmissed){awaitthis.commitMessage(message)}this.recoveredMessagesmissed.lengththis.attempt0this.stateLIVE}Preferences 只保存轻量 cursor完整离线消息和发送队列应交给 RDB。本轮最终游标为msg_10428共恢复 7 条消息。六、前后台切换只改变策略不改变业务订阅进入后台后Demo 停止心跳和新的重连计时器回前台时如果没有有效连接才重新 connect。订阅集合始终保留。onBackground():void{this.foregroundfalsethis.clearReconnectTimer()this.stopHeartbeat()}onForeground():void{this.foregroundtrueif(!this.socket){this.connect()}else{this.startHeartbeat()}}后台时不要清空业务订阅否则前台重建时很容易制造重复订阅。七、运行结果要看数字不只看 LIVE最终手机图里显示Attempt2Backoff2000 msSubscriptions3Duplicate Subscriptions0Recovered Messages7Cursormsg_10428Last RTT54 ms我还做了两个反例连续前后台切换但不断网socket 不应重复创建网络断开后立即退后台reconnect timer 应被取消回前台再重新计算。八、上线前还要补三件事鉴权失败和网络失败要分开完全离线时应减少无效 connect还要明确服务端游标保留窗口过旧游标应切换全量同步或业务快照。我现在对实时连接的验收标准也变了socket 再次 open 只是第一层订阅不重复、断线数据能补齐、生命周期不制造第二条连接才算真正恢复。九、参考资料HarmonyOS Network KitWebSocket 连接、open/message/close/error 与关闭连接。HarmonyOS Ability KitUIAbility 前后台生命周期。HarmonyOS ArkDataPreferences 轻量持久化。