HarmonyOS 7 Network Kit + Preferences:前后台切换后的 WebSocket 退避重连、重复订阅去重与消息游标恢复【鸿蒙心迹】
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'
export class SocketSession {
private socket: webSocket.WebSocket | null = null
private connecting: boolean = false
private foreground: boolean = true
connect(): void {
if (!this.foreground || this.connecting || this.socket) {
return
}
this.connecting = true
const ws = webSocket.createWebSocket()
this.socket = ws
ws.on('open', () => {
this.connecting = false
this.recoverAfterOpen()
})
ws.on('message', (_err, message) => {
this.onMessage(message)
})
ws.on('close', () => {
this.socket = null
this.connecting = false
this.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。
private attempt: number = 0
private reconnectTimer: number = -1
private backoffMs: number = 1000
private scheduleReconnect(): void {
if (!this.foreground || this.reconnectTimer >= 0) {
return
}
this.attempt += 1
this.backoffMs = Math.min(
1000 * Math.pow(2, this.attempt - 1),
30000
)
this.state = 'RECONNECTING'
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = -1
this.connect()
}, this.backoffMs)
}
正式项目还可以加入随机抖动。我不会在 open 回调第一行就把 attempt 清零,只有订阅和消息恢复完成后才清。
四、第三段代码:订阅要分“期望状态”和“连接状态”
重复消息的根源不是 WebSocket 本身,而是我把历史 subscribe 操作机械重放。
更稳定的做法是维护两组集合:desired 表示业务想要的 topic,active 表示本次连接已经确认过的 topic。断线只清 active,不清 desired。
class SubscriptionRegistry {
private desired: Set<string> = new Set()
private active: Set<string> = new Set()
want(topic: string): void {
this.desired.add(topic)
}
resetConnectionState(): void {
this.active.clear()
}
async resubscribe(
send: (topic: string) => Promise<void>
): Promise<void> {
for (const topic of this.desired) {
if (this.active.has(topic)) {
continue
}
await send(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 只有业务提交完成后才更新游标。
private async commitMessage(message: PushMessage): Promise<void> {
await this.messageStore.apply(message)
await this.cursorStore.save(message.id)
this.cursor = message.id
}
private async recoverAfterOpen(): Promise<void> {
this.state = 'RESUBSCRIBING'
await this.registry.resubscribe(
(topic) => this.sendSubscribe(topic)
)
this.state = 'RECOVERING'
const cursor = await this.cursorStore.load()
const missed = await this.fetchMissedMessages(cursor)
for (const message of missed) {
await this.commitMessage(message)
}
this.recoveredMessages = missed.length
this.attempt = 0
this.state = 'LIVE'
}
Preferences 只保存轻量 cursor;完整离线消息和发送队列应交给 RDB。本轮最终游标为 msg_10428,共恢复 7 条消息。
六、前后台切换只改变策略,不改变业务订阅
进入后台后,Demo 停止心跳和新的重连计时器;回前台时,如果没有有效连接才重新 connect。订阅集合始终保留。
onBackground(): void {
this.foreground = false
this.clearReconnectTimer()
this.stopHeartbeat()
}
onForeground(): void {
this.foreground = true
if (!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 轻量持久化。
更多推荐




所有评论(0)