HarmonyOS WebSocket 实时连接实战:心跳、重连、ACK 与消息补偿
HarmonyOS WebSocket 实时连接实战:心跳、重连、ACK 与消息补偿
实时连接常用于聊天、订单状态、客服工单、设备状态推送。问题也很典型:连接断了用户不知道,心跳停了还显示在线,重连后消息重复,弱网下漏掉订单状态。WebSocket 治理要把连接状态、心跳、消息确认、重连和补偿拉取做成一条链路。

本文解决:如何建状态机,如何做心跳超时,如何用 ACK 防止消息丢失和重复,断线重连后如何补齐消息缺口。
1. 实时连接先做状态机
WebSocket 不能只靠一个 connected 布尔值。连接中、已连接、断开中、已断开、重连等待都要能表达。

| 状态 | 含义 | 用户表现 |
|---|---|---|
IDLE |
未连接 | 等待进入页面 |
CONNECTING |
正在连接 | 显示连接中 |
CONNECTED |
可收发消息 | 正常展示 |
RECONNECTING |
断线恢复 | 提示正在恢复 |
CLOSED |
不再连接 | 停止心跳 |
2. WebSocket 资料边界和工程目录
HarmonyOS 支持 WebSocket 网络通信。工程上建议把连接管理和业务消息处理分开。
| 资料入口 | 工程落点 |
|---|---|
| WebSocket 连接 | 连接、发送、关闭和回调 |
| Network Kit | 网络能力总体边界 |
| 网络管理 | 网络状态变化处理 |
entry/src/main/ets/common/socket/
SocketState.ets
HeartbeatLoop.ets
AckTracker.ets
ReconnectPlan.ets
MessageCompensator.ets
3. SocketState 管理连接状态
export type SocketStatus = 'IDLE' | 'CONNECTING' | 'CONNECTED' | 'RECONNECTING' | 'CLOSED'
export class SocketState {
private status: SocketStatus = 'IDLE'
move(next: SocketStatus): void {
const allowed: Record<SocketStatus, SocketStatus[]> = {
IDLE: ['CONNECTING', 'CLOSED'],
CONNECTING: ['CONNECTED', 'RECONNECTING', 'CLOSED'],
CONNECTED: ['RECONNECTING', 'CLOSED'],
RECONNECTING: ['CONNECTING', 'CLOSED'],
CLOSED: []
}
if (!allowed[this.status].includes(next)) throw new Error(`非法连接状态:${this.status} -> ${next}`)
this.status = next
}
current(): SocketStatus {
return this.status
}
}
状态机能避免重复连接和已关闭连接继续发消息。
4. HeartbeatLoop 处理心跳
export interface HeartbeatState {
lastPongAt: number
intervalMs: number
timeoutMs: number
}
export class HeartbeatLoop {
isTimeout(state: HeartbeatState, now: number): boolean {
return now - state.lastPongAt > state.timeoutMs
}
nextPingAt(state: HeartbeatState): number {
return state.lastPongAt + state.intervalMs
}
}
心跳超时后不要继续显示在线,应进入重连流程。
5. AckTracker 确认消息到达
export interface SocketMessage {
messageId: string
type: 'order' | 'chat' | 'system'
payload: Record<string, Object>
}
export class AckTracker {
private readonly waiting = new Map<string, number>()
markSent(message: SocketMessage): void {
this.waiting.set(message.messageId, Date.now())
}
ack(messageId: string): void {
this.waiting.delete(messageId)
}
expired(now: number, timeoutMs: number): string[] {
return [...this.waiting.entries()].filter(([, at]) => now - at > timeoutMs).map(([id]) => id)
}
}
ACK 能让发送方知道消息是否被确认。未确认消息可以重发或走补偿接口。
6. ReconnectPlan 控制重连节奏

export class ReconnectPlan {
delayMs(retry: number): number {
return Math.min(1000 * Math.pow(2, retry), 30000)
}
canReconnect(status: SocketStatus, retry: number): boolean {
return status !== 'CLOSED' && retry < 8
}
}
重连要退避,不能断线后疯狂请求服务器。
7. MessageCompensator 补齐缺口
export interface MessageCursor {
lastMessageId: string
receivedAt: number
}
export class MessageCompensator {
buildSyncRequest(cursor: MessageCursor): Record<string, string> {
return {
afterMessageId: cursor.lastMessageId,
from: `${cursor.receivedAt}`
}
}
}
重连成功后不要只等新消息,应按游标拉取断线期间可能遗漏的消息。
8. 实时连接页面状态映射
export interface SocketViewState {
banner: string
canSend: boolean
}
export function buildSocketView(status: SocketStatus): SocketViewState {
if (status === 'CONNECTED') return { banner: '实时连接正常', canSend: true }
if (status === 'RECONNECTING') return { banner: '网络波动,正在恢复消息', canSend: false }
if (status === 'CONNECTING') return { banner: '正在建立实时连接', canSend: false }
return { banner: '实时连接已关闭', canSend: false }
}
弱网下要告诉用户“正在恢复”,而不是让发送按钮看起来可用。
9. WebSocket 验收动作
| 场景 | 操作 | 预期结果 |
|---|---|---|
| 正常连接 | 进入聊天页 | 状态进入 CONNECTED |
| 心跳超时 | 停止 pong | 进入重连 |
| 消息未 ACK | 模拟 ACK 丢失 | 进入待补偿 |
| 重连成功 | 网络恢复 | 拉取断线缺口 |
| 页面退出 | 离开实时页 | 关闭连接和心跳 |
export function assertSocketMessage(msg: SocketMessage): void {
if (!msg.messageId) throw new Error('实时消息必须包含 messageId')
if (!['order', 'chat', 'system'].includes(msg.type)) throw new Error('消息类型不支持')
}
10. 实时连接异常排查表
实时连接问题要从状态机、心跳、ACK 和补偿四层看。
| 现象 | 优先查看 | 处理建议 |
|---|---|---|
| 显示在线但收不到消息 | 心跳时间 | 超时后进入重连 |
| 消息重复 | messageId | 消费端去重 |
| 消息丢失 | 游标和补偿接口 | 重连后拉取缺口 |
| 服务器压力高 | 重连间隔 | 指数退避 |
| 页面退出还收消息 | 连接关闭 | 生命周期释放 |
建议给实时连接增加连接审计记录。审计不需要保存消息正文,只需要保存连接状态、最近心跳、重连次数、未 ACK 数量和补偿结果。这样订单状态漏推时,可以先判断是连接层断了,还是业务消息没有送达。
export interface SocketAuditRecord {
status: SocketStatus
lastPongAt: number
reconnectCount: number
waitingAckCount: number
compensation: 'none' | 'success' | 'failed'
at: number
}
export class SocketAuditLog {
private readonly records: SocketAuditRecord[] = []
append(record: Omit<SocketAuditRecord, 'at'>): void {
this.records.push({ ...record, at: Date.now() })
}
latest(): SocketAuditRecord | undefined {
return this.records[this.records.length - 1]
}
}
如果用户反馈“订单状态没更新”,排查顺序应该是:连接是否处于 CONNECTED,心跳是否超时,消息是否进入 ACK 队列,重连后有没有走补偿拉取。按这个顺序看,能避免把所有问题都归咎于服务端推送。
WebSocket 重连复现场景:给读者一组可执行核验
实时连接要验证心跳、ACK、断线重连和消息补偿。只看到连接成功,不能证明消息不会丢。
| 核验维度 | 读者需要准备的证据 |
|---|---|
| 输入 | 页面入口、用户动作、关键参数 |
| 过程 | 日志、状态变化、异常分支 |
| 输出 | UI 表现、回调结果、持久化结果 |
| 回归 | 同场景重复执行后的结果 |
interface SocketReplayCase {
sessionId: any
heartbeatOk: any
lastAckSeq: any
reconnectCount: any
}
const replay86: SocketReplayCase = {
sessionId: 'sample',
heartbeatOk: 'sample',
lastAckSeq: 'sample',
reconnectCount: 'sample',
}
function assertReplay86(item: SocketReplayCase): void {
if (!item.heartbeatOk && item.reconnectCount === 0) throw new Error('心跳失败后未触发重连')
}
这组核验围绕实时消息可靠性展开,能帮助读者判断断线后是否完成重连和补偿。
WebSocket 断线回放表:把文章方法变成可复现动作
实时连接要验证断线期间的消息。建议在发送消息后断网,再恢复网络,观察 ACK、重连和补偿队列是否能够把遗漏消息补齐。
| 回放动作 | 核验方式 |
|---|---|
| 心跳失败 | 准备输入、执行操作、记录结果、给出结论 |
| 自动重连 | 准备输入、执行操作、记录结果、给出结论 |
| ACK 对齐 | 准备输入、执行操作、记录结果、给出结论 |
| 补偿队列清空 | 准备输入、执行操作、记录结果、给出结论 |
WebSocket 要重点验证断线期间发生了什么。读者可以在发送消息后立刻断网,恢复后看心跳是否重建、ACK 序号是否连续、未确认消息是否补发、重复消息是否去重。实时链路的质量不在连接成功那一刻,而在异常恢复后消息是否仍然可信。
实时连接的落地边界:不要把边界留给读者猜
WebSocket 文章要区分连接状态和消息状态。连接成功不代表消息成功,消息发送不代表服务确认,服务确认不代表页面已经消费。读者落地时需要 ACK、序号和补偿队列三个概念同时存在。
| 落地项 | 处理要求 |
|---|---|
| 连接状态看心跳 | 需要有明确输入、处理边界和失败兜底 |
| 消息状态看 ACK | 需要有明确输入、处理边界和失败兜底 |
| 页面状态看消费 | 需要有明确输入、处理边界和失败兜底 |
| 异常恢复看补偿 | 需要有明确输入、处理边界和失败兜底 |
这类边界写清楚后,读者不需要猜哪些逻辑属于页面、哪些属于服务、哪些属于发布前验收。文章的价值也会从“讲了一个功能”变成“给了一套可迁移的工程判断”。
WebSocket 联调步骤:按真实路径走一遍
WebSocket 联调不要只看连接成功。建议先连接服务端并发送一条带序号消息,然后断网,继续产生本地消息,再恢复网络。恢复后观察是否重连、是否补发未确认消息、是否去重重复 ACK、页面状态是否和服务端一致。这个过程能暴露大多数实时链路问题。
这一步的意义是让读者拿到文章后可以直接复现,而不是只理解概念。技术文章如果能把“输入、动作、日志、结果、失败兜底”写完整,读者照着做时出错概率会低很多。
WebSocket 验收补充:补上容易漏掉的边界
补充一个消息乱序场景:客户端先发送 101、102、103 三条消息,服务端只确认 101,然后网络断开。恢复后客户端应该从 102 开始补偿,而不是全量重发导致重复业务处理。这个场景能验证 ACK 序号是否真的参与恢复逻辑。
实时连接还要处理重复消息。服务端重发、客户端补偿和网络抖动都可能让同一条消息到达两次。读者可以用消息序号或业务 id 做去重,回放时确认重复消息不会让未读数、订单状态或聊天气泡重复增加。这个验收点比单纯测试重连更接近真实线上问题。
这类补充不是为了增加篇幅,而是为了让读者在真实项目里少踩坑:正常路径一般最容易跑通,异常路径、退出路径和恢复路径才是质量差距所在。
11. 小结:实时连接靠状态闭环
WebSocket 的稳定性来自状态机、心跳、ACK、重连和补偿。只连上不够,断线后能恢复、消息能确认、缺口能补齐,才是可上线的实时连接。
更多推荐


所有评论(0)