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、重连和补偿。只连上不够,断线后能恢复、消息能确认、缺口能补齐,才是可上线的实时连接。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐