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、测试、元服务和应用上架分发等。

更多推荐