☰
HarmonyOS 7 Network Kit + Preferences:前后台切换后的 WebSocket 退避重连、重复订阅去重与消息游标恢复【鸿蒙心迹】
2026/10/4 7:40:32 网站建设 项目流程

WebSocket 很容易写出一个“演示正常”的版本:监听open、message、close、error再connect()。真实问题出现在前后台切换和弱网里。

Socket Resume Lab的最初版本能重新连接,却出现两个问题:同一 topic 收到重复消息,离线期间的消息也没有补回来。页面显示“已连接”,业务数据却已经断层。

所以这篇不把“socket 再次 open”当恢复成功,而是把重连、重新订阅和消息游标放到一条状态链里。

一、先定义什么叫恢复成功

本轮验收数据:

  • Session:ws_resume_20261001_10
  • Foreground:true
  • State:LIVE
  • Attempt:2
  • Backoff:2000 ms
  • Subscriptions:3
  • Duplicate Subscriptions:0
  • Cursor:msg_10428
  • Recovered Messages:7
  • Last RTT:54 ms

WebSocket 流程很直接:创建对象,监听 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}from'@kit.NetworkKit'exportclassSocketSession{privatesocket:webSocket.WebSocket|null=nullprivateconnecting:boolean=falseprivateforeground:boolean=trueconnect():void{if(!this.foreground||this.connecting||this.socket){return}this.connecting=trueconstws=webSocket.createWebSocket()this.socket=ws ws.on('open',()=>{this.connecting=falsethis.recoverAfterOpen()})ws.on('message',(_err,message)=>{this.onMessage(message)})ws.on('close',()=>{this.socket=nullthis.connecting=falsethis.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:number=0privatereconnectTimer:number=-1privatebackoffMs:number=1000privatescheduleReconnect():void{if(!this.foreground||this.reconnectTimer>=0){return}this.attempt+=1this.backoffMs=Math.min(1000*Math.pow(2,this.attempt-1),30000)this.state='RECONNECTING'this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=-1this.connect()},this.backoffMs)}

正式项目还可以加入随机抖动。我不会在 open 回调第一行就把 attempt 清零,只有订阅和消息恢复完成后才清。

四、第三段代码:订阅要分“期望状态”和“连接状态”

重复消息的根源不是 WebSocket 本身,而是我把历史 subscribe 操作机械重放。

更稳定的做法是维护两组集合:desired表示业务想要的 topic,active表示本次连接已经确认过的 topic。断线只清 active,不清 desired。

classSubscriptionRegistry{privatedesired:Set<string>=newSet()privateactive:Set<string>=newSet()want(topic:string):void{this.desired.add(topic)}resetConnectionState():void{this.active.clear()}asyncresubscribe(send:(topic:string)=>Promise<void>):Promise<void>{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 attempt=2 backoff=2000ms subscriptions=3 duplicate=0 cursor=msg_10428 recovered=7 State: RECONNECTING -> LIVE

五、第四段代码:游标必须在业务确认后推进

离线消息能否补齐,取决于游标怎么保存。

如果刚收到消息就把 cursor 写进 Preferences,但业务处理还没完成,此时应用异常退出,下次会从“已经写过但还没真正处理”的位置继续,消息就丢了。

所以 Demo 只有业务提交完成后才更新游标。

privateasynccommitMessage(message:PushMessage):Promise<void>{awaitthis.messageStore.apply(message)awaitthis.cursorStore.save(message.id)this.cursor=message.id}privateasyncrecoverAfterOpen():Promise<void>{this.state='RESUBSCRIBING'awaitthis.registry.resubscribe((topic)=>this.sendSubscribe(topic))this.state='RECOVERING'constcursor=awaitthis.cursorStore.load()constmissed=awaitthis.fetchMissedMessages(cursor)for(constmessageofmissed){awaitthis.commitMessage(message)}this.recoveredMessages=missed.lengththis.attempt=0this.state='LIVE'}

Preferences 只保存轻量 cursor;完整离线消息和发送队列应交给 RDB。本轮最终游标为msg_10428,共恢复 7 条消息。

六、前后台切换只改变策略,不改变业务订阅

进入后台后,Demo 停止心跳和新的重连计时器;回前台时,如果没有有效连接才重新 connect。订阅集合始终保留。

onBackground():void{this.foreground=falsethis.clearReconnectTimer()this.stopHeartbeat()}onForeground():void{this.foreground=trueif(!this.socket){this.connect()}else{this.startHeartbeat()}}

后台时不要清空业务订阅,否则前台重建时很容易制造重复订阅。

七、运行结果要看数字,不只看 LIVE

最终手机图里显示:

  • Attempt:2
  • Backoff:2000 ms
  • Subscriptions:3
  • Duplicate Subscriptions:0
  • Recovered Messages:7
  • Cursor:msg_10428
  • Last RTT:54 ms

我还做了两个反例:连续前后台切换但不断网,socket 不应重复创建;网络断开后立即退后台,reconnect timer 应被取消,回前台再重新计算。

八、上线前还要补三件事

鉴权失败和网络失败要分开;完全离线时应减少无效 connect;还要明确服务端游标保留窗口,过旧游标应切换全量同步或业务快照。

我现在对实时连接的验收标准也变了:socket 再次 open 只是第一层;订阅不重复、断线数据能补齐、生命周期不制造第二条连接,才算真正恢复。

九、参考资料

  • HarmonyOS Network Kit:WebSocket 连接、open/message/close/error 与关闭连接。
  • HarmonyOS Ability Kit:UIAbility 前后台生命周期。
  • HarmonyOS ArkData:Preferences 轻量持久化。

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

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

立即咨询