WebSocket协议与GameNetwork技术文档

目录

  1. 通信架构概述
  2. @kit.NetworkKit引入
  3. GameMessage协议设计
  4. GameNetwork封装类
  5. webSocket.connect回调模式
  6. webSocket.on(‘message’)双参数
  7. 自动重连机制
  8. 心跳设计
  9. 房间管理
  10. 错误处理体系
  11. 未来:分布式软总线迁移

1. 通信架构概述

1.1 Host-Adjudicator模型

NearPlay是一个面向HarmonyOS平台的多人实时互动游戏应用,其核心挑战在于如何在多设备间建立低延迟、高可靠的实时通信通道。为了满足游戏场景对状态一致性和消息实时性的严苛要求,我们采用了Host-Adjudicator(主机-裁判)模型作为通信架构的核心设计范式。

在该模型中,房间内的一名玩家设备被选为Host(主机),同时服务端充当**Adjudicator(裁判)**角色。Host负责收集房间内所有玩家的操作指令并统一转发至服务端,服务端作为Adjudicator执行游戏逻辑判定(如投票结果统计、角色分配、胜负判定等),再将判定结果广播给所有玩家。这一设计的核心优势在于:

  • 状态一致性保障:所有游戏状态的变更均由Adjudicator单点裁决,避免了多端并发写入导致的状态冲突问题。在狼人杀等需要严格信息隔离的游戏中,Adjudicator能够精确控制哪些信息该广播、哪些信息该定向推送,确保游戏公平性。
  • 网络负载优化:Host作为客户端的聚合节点,将多名玩家的操作打包后统一发送,减少了服务端的连接处理压力,同时也降低了客户端的上行带宽消耗。在"你来比划我来猜"等需要高频同步动作的游戏场景中,这种聚合模式能显著减少消息条数。
  • 断线恢复简化:当某位玩家断线重连后,只需从Adjudicator处同步最新的游戏快照,无需与房间内其他玩家逐一协商状态,大幅简化了断线恢复的实现复杂度。

此模型与传统的客户端-服务端两层架构的关键区别在于:Host不仅是普通的客户端,还承担了部分服务端职责——即在客户端层面完成消息聚合和初步过滤。这种设计在移动网络环境下尤为重要:移动设备的网络不稳定,Host聚合可以减少因个别玩家网络波动导致的消息丢失或重复。当某位非Host玩家的连接中断时,Host可以缓存其未发送的消息,待其重连后补发,而非将这一责任完全交给服务端。

从架构层面看,Host的选择策略也影响着系统的整体稳定性。当前实现中,Host由房间创建者自动担任,后续可以扩展为基于网络质量的动态选择策略——通过ping值检测各玩家的网络延迟,选择延迟最低的设备作为Host,以最大化整体消息传递效率。Host迁移机制也是未来的重要扩展点——当前Host断线时,服务端需要从剩余玩家中重新指定Host,这个过程应尽可能快速以减少游戏中断。

+------------------------------------------------------------+
|              NearPlay 通信架构 - Host-Adjudicator           |
|                                                            |
|   +-------+     +-------+     +-------+     +-------+     |
|   |Player |     |Player |     |Player |     |Player |     |
|   |  A    |     |  B    |     |  C    |     |  D    |     |
|   +---+---+     +---+---+     +---+---+     +---+---+     |
|       |             |             |             |          |
|       +------+------++            +------+------++         |
|              |                            |                |
|              v                            v                |
|        +-----------+               +-----------+          |
|        |   Host    |               |   Host    |          |
|        | (Player A)|               | (Player C)|          |
|        +-----+-----+               +-----+-----+          |
|              |                            |                |
|              +------------+---------------+                |
|                           | WebSocket                       |
|                           v                                 |
|              +---------------------+                       |
|              |    Adjudicator      |                       |
|              |   (Server/云端)      |                       |
|              |                     |                       |
|              |  - 游戏逻辑裁决      |                       |
|              |  - 状态快照管理      |                       |
|              |  - 消息路由分发      |                       |
|              |  - 信息隔离控制      |                       |
|              +---------------------+                       |
+------------------------------------------------------------+

1.2 WebSocket选型论证

在实时游戏通信方案的技术选型中,我们对三种主流方案进行了深入对比分析:

HTTP长轮询(Long Polling):客户端定时向服务端发送请求,服务端在有新数据时响应,否则保持连接等待。这种方式虽然实现简单、兼容性好,但存在根本性的缺陷——每次请求都携带完整的HTTP头部(通常数百字节),在游戏场景下频繁轮询造成的头部开销极为可观。更关键的是,HTTP轮询本质上是半双工通信,服务端无法主动推送数据,导致消息延迟不可控。在狼人杀的投票环节中,如果某位玩家投票后需要等待下一轮轮询才能通知其他玩家,延迟可能达到1-5秒,严重影响游戏体验。此外,频繁的连接建立与断开也会增加服务端的TCP连接管理负担。每次HTTP请求的连接建立过程包含TCP三次握手和TLS协商(HTTPS场景),延迟累计可达数百毫秒。

分布式软总线(Distributed Soft Bus):HarmonyOS提供的设备间直连通信机制,支持同账号下多设备间的低延迟数据传输。该方案在局域网场景下具有天然优势——零跳路由、亚毫秒级延迟、无需中转服务器。然而,分布式软总线目前仅支持同账号、同华为账号下的设备互连,无法满足NearPlay跨账号多人游戏的需求。此外,其通信范围受限于同一局域网,无法支持远程玩家加入,这直接违背了NearPlay"随时随地开局"的产品定位。分布式软总线的API体系也更偏向数据同步而非消息收发,缺乏游戏场景所需的消息路由、房间管理等基础设施。其底层使用的CoAP协议或自研的轻量级传输协议,与WebSocket的标准化程度相比存在差距。

WebSocket:基于TCP的全双工通信协议,通过一次HTTP升级握手建立持久连接,之后客户端与服务端可在同一TCP连接上双向自由通信。WebSocket的优势在于:连接建立后通信开销极低(每帧仅2-10字节头部),支持服务端主动推送,消息延迟可控制在数十毫秒级别,天然适合实时游戏场景。WebSocket协议最初于2011年通过RFC 6455完成标准定义,后经RFC 7936、RFC 8307、RFC 8441等标准完善,已成为实时通信领域的事实标准。在HarmonyOS生态中,WebSocket从API 6即获支持,API成熟度高,文档完善,社区资源丰富。其握手过程基于HTTP Upgrade机制,客户端发送一个特殊的HTTP请求(携带Upgrade: websocket头部),服务端回复101状态码后协议即切换为WebSocket,之后的通信完全脱离HTTP语义。

1.3 三种方案对比

维度 HTTP长轮询 WebSocket 分布式软总线
通信模式 半双工 全双工 全双工
延迟 1-5秒(轮询周期) 10-100ms <1ms(局域网)
头部开销 每次数百字节 首次握手后2-10字节 无(内核级传输)
服务端推送 不支持 原生支持 原生支持
跨账号 支持 支持 不支持
跨网络 支持 支持 不支持(仅局域网)
连接管理 频繁建断 持久连接 系统管理
游戏适用性 中(受限场景)
实现复杂度 高(需适配系统限制)
HarmonyOS API @kit.NetworkKit http @kit.NetworkKit webSocket @kit.DistributedServiceKit

基于上述对比,WebSocket在跨网络多人游戏场景中提供了最佳的延迟-兼容性-复杂度平衡。虽然分布式软总线在局域网延迟上具有绝对优势,但其账号和网络限制使其无法作为主通信方案。我们选择WebSocket作为主通信协议,同时保留未来向分布式软总线迁移的可能性,作为局域网场景的加速方案。

1.4 消息流转全景

                    NearPlay 消息流转时序

  Player A          Player B          Server(Adjudicator)          Player C
     |                 |                      |                        |
     |  --- CHAT --->  |                      |                        |
     |  (broadcast)    |                      |                        |
     |                 |  --- JOIN ---------> |                        |
     |                 |                      |  --- STATE --------->  |
     |                 |                      |  (room update)         |
     |                 |                      |                        |
     |  --- VOTE -------------------------->  |                        |
     |                 |  --- VOTE -------->  |                        |
     |                 |                      |  --- REVEAL -------->  |
     |                 |                      |  (vote result)         |
     |                 |                      |                        |
     |  --- NIGHT_ACTION ------------------>  |                        |
     |  (private msg)  |                      |                        |
     |                 |                      |                        |
     |  <-- ROLE_ASSIGN -------------------   |                        |
     |  (directed)     |  <-- ROLE_ASSIGN --  |  <-- ROLE_ASSIGN --   |
     |                 |                      |                        |

在上述时序中可以清晰看到Host-Adjudicator模型的消息流转规律:玩家的操作类消息(CHAT、JOIN、VOTE、NIGHT_ACTION等)上行至服务端,服务端裁决后的状态类消息(STATE、REVEAL、ROLE_ASSIGN等)下行至客户端。PRIVATE类型的消息(如狼人杀中仅狼人可见的夜间行动通知)由服务端根据游戏规则定向推送给特定玩家,而非广播,从而实现信息隔离。这种上下行分离的消息流转模式使得每个环节的职责清晰——客户端只负责采集和展示,服务端只负责裁决和分发——极大地降低了系统的耦合度。


2. @kit.NetworkKit引入

2.1 正确的导入路径

在HarmonyOS SDK中,WebSocket相关API被归入@kit.NetworkKit模块,而非许多开发者直觉上以为的@kit.InternetKit。这是NearPlay项目中最容易踩的第一个坑。正确的导入方式如下:

import { webSocket } from '@kit.NetworkKit'
import { BusinessError } from '@kit.BasicServicesKit'

@kit.NetworkKit是HarmonyOS为网络通信能力提供的统一Kit,其命名空间涵盖了WebSocket客户端、WebSocket服务端(API 23+)、HTTP请求、Socket通信等完整的网络协议栈。该Kit从API 6开始提供WebSocket客户端能力,从API 23开始提供WebSocket服务端能力。webSocket是该Kit导出的一个命名空间对象,其上挂载了createWebSocket()工厂方法和相关类型定义(如WebSocketCloseResultWebSocketRequestOptions等)。

值得注意的是,HarmonyOS的Kit化导入是从旧版@ohos.net.webSocket直接导入迁移而来的。在API 9之前,开发者使用的是import webSocket from '@ohos.net.webSocket',这种导入方式在新版SDK中仍然可用但不推荐。Kit化导入的优势在于:按功能域而非技术模块名组织,导入路径更稳定(内部模块重构时Kit名不变),且与HarmonyOS的系统能力声明体系一致。

2.2 @kit.InternetKit的陷阱

@kit.InternetKit在实际SDK中并不存在作为一个独立的Kit导出名。这是一个常见的混淆点,源于早期HarmonyOS开发者文档中曾出现过"Internet Kit"的表述,以及部分开发者将网络能力与"互联网"一词关联的习惯。在代码中尝试以下导入:

// 错误!编译失败
import { webSocket } from '@kit.InternetKit'
// 错误!编译失败
import { http } from '@kit.InternetKit'

这将导致编译器报出Cannot find module '@kit.InternetKit'的错误。原因很简单:HarmonyOS的Kit命名体系遵循功能域划分原则,网络通信(包括HTTP、WebSocket、TCP/UDP Socket)统一归属NetworkKit,而不会按"是否有互联网"这种非技术维度拆分。

开发者在社区中可能遇到以下混淆来源:

  • 旧版@ohos.net.webSocket直接导入方式与Kit化后的@kit.NetworkKit之间的映射关系不清
  • 部分第三方教程仍使用@ohos.net.webSocket旧式导入,未更新为Kit导入
  • @kit.NetworkKit与网络权限ohos.permission.INTERNET的名称关联容易造成"InternetKit"的误推
  • 某些早期示例代码使用了后来被废弃的Kit命名,未随SDK更新同步修正

这里需要明确一个关键区别:权限名中的"INTERNET"和Kit名中的"Network"属于不同的命名体系。权限描述的是"做什么"(访问互联网),Kit描述的是"用什么能力做"(网络通信能力)。两者不应混淆。

2.3 导入规范

在NearPlay项目中,所有涉及WebSocket的文件必须遵循以下导入规范:

// 第一行:WebSocket模块
import { webSocket } from '@kit.NetworkKit'
// 第二行:错误类型(所有回调中的err参数类型)
import { BusinessError } from '@kit.BasicServicesKit'
// 第三行:项目内部模块
import { GameMessage, MessageType } from './GameMessage'

这种导入顺序并非随意规定——ArkTS编译器要求所有import语句位于文件顶部且先于其他语句(arkts-no-misplaced-imports规则),而将系统Kit导入置于项目内部导入之前,既符合从底层到上层的逻辑依赖顺序,也便于代码审查时快速识别外部依赖。

需要特别注意的是,import { webSocket } from '@kit.NetworkKit'导入的webSocket是一个命名空间对象,而非一个类。创建WebSocket实例需要调用其上的工厂方法:

const ws: webSocket.WebSocket = webSocket.createWebSocket()

这里webSocket.WebSocket是类型名(大写W的WebSocket类),webSocket.createWebSocket()是工厂方法调用。两者都通过同一命名空间webSocket访问,这是HarmonyOS Kit API的常见设计模式。初学者常犯的错误包括:直接使用new WebSocket()(应使用工厂方法),或将命名空间对象当作类型注解使用。

另外,关于权限配置:在module.json5中必须声明ohos.permission.INTERNET权限,否则WebSocket连接将被系统拒绝并触发201错误码。该权限属于normal级别,无需用户授权,仅需在配置文件中声明即可生效。


3. GameMessage协议设计

3.1 消息格式选型:JSON

NearPlay的应用层通信协议采用JSON作为消息序列化格式。选择JSON而非Protobuf或自定义二进制协议,基于以下考量:

  • 调试友好:在开发阶段,JSON消息可直接在浏览器开发者工具或网络抓包中阅读,极大降低了联调成本。游戏逻辑复杂度高的场景下,可读性的价值远超几十字节的带宽节省。
  • ArkTS生态适配:ArkTS内置JSON.parse()/JSON.stringify(),无需引入第三方序列化库。在HarmonyOS的ArkTS运行时中,JSON操作经过了高度优化,对于NearPlay的消息体量(通常小于1KB)性能绰绰有余。
  • 服务端兼容性:后端服务使用Node.js/Go等语言开发,JSON是这些语言的"一等公民",无需额外编解码开销。
  • 动态字段支持:游戏消息的payload字段在不同游戏阶段承载不同的数据结构,JSON的动态性天然适配这种需求,而Protobuf需要为每种payload定义独立的message类型并使用oneof,增加了协议维护成本。

JSON的唯一劣势是序列化后的消息体积略大于二进制协议(约大30%-50%),但在NearPlay的消息体量下,绝对差异仅为几百字节,对WebSocket连接的带宽影响可忽略不计。

3.2 消息类型枚举

GameMessage通过MessageType枚举区分消息类别。当前实现定义了四种基础类型:

export enum MessageType {
  ACTION = 0,   // 玩家操作类消息
  STATE = 1,    // 状态同步类消息
  SYSTEM = 2,   // 系统通知类消息
  PRIVATE = 3   // 私有定向消息
}

然而,随着NearPlay游戏矩阵的扩展,这四种粗粒度分类通过action字段进一步细化为15+种具体消息类型:

action值 所属MessageType 说明 典型payload
CHAT ACTION 房间内聊天消息 {"content":"大家好"}
JOIN ACTION 玩家加入房间 {"userName":"Alice","avatar":1}
LEAVE ACTION 玩家离开房间 {"reason":"主动退出"}
START ACTION 房主开始游戏 {"gameId":"werewolf-001"}
VOTE ACTION 投票操作 {"targetId":"user-003"}
ROLE_ASSIGN PRIVATE 角色分配通知 {"role":"werewolf","teamId":1}
NIGHT_ACTION PRIVATE 夜间行动 {"targetId":"user-005","action":"kill"}
SNAP ACTION 比划者动作同步 {"gesture":"hand-wave","frame":42}
GUESS ACTION 猜测者提交答案 {"answer":"游泳"}
DESCRIBE ACTION 描述者发言 {"content":"一种水上运动"}
CHOOSE ACTION 选择操作 {"optionId":3}
CROWD_SOURCE ACTION 众包答案提交 {"word":"大象"}
ANSWER STATE 答案公布 {"correctAnswer":"游泳","isCorrect":true}
REVEAL STATE 信息揭示 {"votes":{"user-001":"user-003"}}
LIKE ACTION 点赞互动 {"targetUserId":"user-002"}

这种"type+action"的两级分类体系具有良好的可扩展性。type决定消息的处理路径(广播、定向、系统通知等),action决定具体的业务逻辑。新增游戏类型时只需扩展action值和payload约定,无需修改消息传输框架。

3.3 消息字段设计

每条GameMessage包含以下核心字段:

export class GameMessage {
  type: MessageType = MessageType.ACTION    // 消息大类
  gameId: string = ''                       // 游戏标识
  roomId: string = ''                       // 房间标识
  fromUserId: string = ''                   // 发送者ID
  toUserId: string = ''                     // 接收者ID(空表示广播)
  action: string = ''                       // 具体操作类型
  payload: string = ''                      // JSON格式的业务数据
  timestamp: number = 0                     // 毫秒级时间戳
}

字段设计原理

type字段作为消息的第一级分类器,决定了消息的处理路径。ACTION类型消息需要经过游戏逻辑引擎处理,STATE类型消息直接更新本地状态机,SYSTEM类型消息用于UI通知(如"XX加入了房间"),PRIVATE类型消息则触发信息隔离逻辑——只有目标用户才能看到消息内容。

gameId字段支持NearPlay的多游戏架构。同一房间在不同时段可能切换不同游戏,gameId让服务端的消息路由能精确地将消息分发到对应的游戏逻辑处理器。

roomId是消息路由的核心依据。服务端的Adjudicator以房间为单位管理游戏实例,每条上行消息通过roomId定位到对应的游戏实例,每条下行消息也通过roomId确定广播范围。

fromUserIdtoUserId构成了消息的发送-接收模型。当toUserId为空字符串时,消息为广播性质,房间内所有玩家可见;当toUserId为特定用户ID时,消息为定向推送,仅该用户可见。这种设计在狼人杀游戏中尤为重要——ROLE_ASSIGN消息必须定向推送给对应玩家,不能广播暴露角色信息。

action字段是消息的第二级分类器,承载具体的操作语义。它与type配合使用:type决定处理路径,action决定处理逻辑。例如,同样是ACTION类型,CHAT、VOTE、NIGHT_ACTION三者的处理逻辑完全不同。

payload字段以JSON字符串的形式承载业务数据,其具体结构由action决定。这种"信封+载荷"的设计模式使得协议层与业务层解耦。

timestamp字段由客户端通过Date.now()在消息创建时填充,用于消息排序和延迟计算。在弱网环境下,消息可能乱序到达,接收端可依据timestamp进行重排。

3.4 序列化与反序列化

GameMessage的序列化采用toJson()方法,将所有字段映射为Record<string, string | number>类型后调用JSON.stringify()

toJson(): string {
  const obj: Record<string, string | number> = {
    'type': this.type, 'gameId': this.gameId, 'roomId': this.roomId,
    'fromUserId': this.fromUserId, 'toUserId': this.toUserId,
    'action': this.action, 'payload': this.payload, 'timestamp': this.timestamp
  }
  return JSON.stringify(obj)
}

注意ArkTS的要求:Record类型字面量必须使用引号键('type'而非type),这是arkts-no-untyped-obj-literals规则的强制要求。所有值类型限定为string | number联合类型,因为GameMessage的字段只有字符串和数字两种类型。

反序列化通过静态方法fromJson()实现,采用安全解析策略:

static fromJson(json: string): GameMessage {
  const m = new GameMessage()
  try {
    const obj: Record<string, string | number> = JSON.parse(json) as Record<string, string | number>
    m.type = (obj['type'] as number) ?? MessageType.ACTION
    m.gameId = (obj['gameId'] as string) ?? ''
    m.roomId = (obj['roomId'] as string) ?? ''
    m.fromUserId = (obj['fromUserId'] as string) ?? ''
    m.toUserId = (obj['toUserId'] as string) ?? ''
    m.action = (obj['action'] as string) ?? ''
    m.payload = (obj['payload'] as string) ?? ''
    m.timestamp = (obj['timestamp'] as number) ?? 0
  } catch (e) {
    // 解析失败返回默认值的GameMessage,而非抛出异常
  }
  return m
}

安全解析的核心原则是永不因消息格式问题导致运行时崩溃。即使收到格式错误或字段缺失的消息,fromJson()也会返回一个具有默认值的GameMessage实例,由上层逻辑决定如何处理无效消息。??运算符确保了即使某个字段在JSON中缺失或类型不匹配,也能回退到合理的默认值。

3.5 工厂方法

GameMessage.of()静态工厂方法简化了消息创建:

static of(type: MessageType, gameId: string, roomId: string,
          fromId: string, toId: string, action: string, payload: string): GameMessage {
  const m = new GameMessage()
  m.type = type; m.gameId = gameId; m.roomId = roomId
  m.fromUserId = fromId; m.toUserId = toId
  m.action = action; m.payload = payload; m.timestamp = Date.now()
  return m
}

工厂方法自动填充timestamp字段,避免调用方遗漏时间戳。业务代码只需关心消息的语义字段,无需手动管理基础设施字段。

3.6 协议帧结构

+-----------------------------------------------------------+
|                  GameMessage 协议帧结构                     |
+----------+----------+----------+-----------------------------+
|  字段     |  类型    |  长度     |  说明                      |
+----------+----------+----------+-----------------------------+
|  type    |  number  |  枚举值   |  消息大类(0-3)              |
|  gameId  |  string  |  变长    |  游戏标识                   |
|  roomId  |  string  |  变长    |  房间标识                   |
|fromUserId|  string  |  变长    |  发送者ID                   |
| toUserId |  string  |  变长    |  接收者ID(空=广播)          |
|  action  |  string  |  变长    |  操作类型                   |
|  payload |  string  |  变长    |  JSON业务数据               |
|timestamp |  number  |  8字节   |  毫秒时间戳                 |
+----------+----------+----------+-----------------------------+
|  整体封装为JSON字符串,通过WebSocket text frame传输           |
+-----------------------------------------------------------+

4. GameNetwork封装类

4.1 设计目标

GameNetwork是对HarmonyOS WebSocket API的面向对象封装,旨在提供以下能力:

  • 简化连接管理:隐藏webSocket.createWebSocket()connect()、事件订阅等底层细节,对外暴露connect(url)send(msg)close()三个核心方法
  • 自动重连:在连接意外断开时自动尝试重连,上层业务无需感知网络波动
  • 消息回调:将WebSocket原始的string | ArrayBuffer消息自动反序列化为GameMessage对象,通过回调机制分发
  • 状态通知:连接状态变化时通过回调通知上层,驱动UI更新

4.2 类结构概览

export class GameNetwork {
  private socket: webSocket.WebSocket | null = null
  private messageCallback: MessageCallback | null = null
  private statusCallback: StatusCallback | null = null
  private serverUrl: string = ''
  private reconnectAttempts: number = 0
  private maxReconnect: number = 5
  private isConnected: boolean = false
}
+---------------------------------------------------------+
|                    GameNetwork                           |
+---------------------------------------------------------+
|  - socket: WebSocket | null                             |
|  - messageCallback: MessageCallback | null              |
|  - statusCallback: StatusCallback | null                |
|  - serverUrl: string                                    |
|  - reconnectAttempts: number                            |
|  - maxReconnect: number (5)                             |
|  - isConnected: boolean                                 |
+---------------------------------------------------------+
|  + connect(url: string): void                           |
|  + send(msg: GameMessage): boolean                      |
|  + onMessage(cb: MessageCallback): void                 |
|  + onStatusChange(cb: StatusCallback): void             |
|  + close(): void                                        |
|  + connected: boolean (getter)                          |
+---------------------------------------------------------+
|  - doConnect(): void                                    |
|  - handleReconnect(): void                              |
+---------------------------------------------------------+

4.3 ConnectionState枚举(设计扩展)

当前实现使用isConnected: boolean表示连接状态,但这仅能区分"已连接"和"未连接"两种状态。在实际运行中,连接生命周期存在更多中间态。我们设计了ConnectionState枚举以支持更精细的状态管理:

export enum ConnectionState {
  DISCONNECTED = 0,   // 未连接(初始态或主动断开后)
  CONNECTING = 1,     // 正在建立连接(connect()已调用,等待回调)
  CONNECTED = 2,      // 已连接(open回调已触发)
  RECONNECTING = 3    // 重连中(意外断开后正在自动重连)
}

这四种状态的转换关系如下:

                    ConnectionState 状态机

  +--------------+     connect()      +--------------+
  | DISCONNECTED | ----------------> |  CONNECTING  |
  +------+-------+                   +------+-------+
         ^                                  |
         |                         +--------+--------+
         |                   open回调|                |connect失败/error
    close()/                        |      v                v
    max重试超限             +--------------+  +--------------+
         |                   |  CONNECTED   |  | RECONNECTING |
         |                   +------+-------+  +------+-------+
         |                          |                 |
         |                   close/error              |
         |                          |                 |
         |                          v                 |
         |                   +--------------+        |
         +----------------- | RECONNECTING | --------+
                             +--------------+  retry < max: doConnect()

DISCONNECTED:初始状态或主动关闭后的状态。此时不持有WebSocket实例,不尝试重连。

CONNECTING:调用connect()后进入此状态。此时已创建WebSocket实例并调用了connect()方法,正在等待服务端响应。此状态下不应调用send(),否则消息将丢失。

CONNECTED:WebSocket连接成功建立,on('open')回调已触发。此状态下可正常收发消息。

RECONNECTING:意外断开后正在自动重连。此状态下的重连尝试受到maxReconnect限制,超出限制后转入DISCONNECTED状态。

4.4 connect方法

connect(url: string): void {
  this.serverUrl = url
  this.reconnectAttempts = 0
  this.doConnect()
}

connect()是GameNetwork的入口方法。它保存服务器URL供重连使用,将重连计数器归零,然后委托给doConnect()执行实际的连接逻辑。将重连计数器归零是关键操作——当用户主动触发新连接时,应清除之前可能存在的重连计数,确保新的连接获得完整的重连配额。

4.5 doConnect内部方法

doConnect()是GameNetwork的核心私有方法,封装了WebSocket实例创建、事件订阅和连接发起的完整流程:

private doConnect(): void {
  if (this.socket !== null) {
    this.close()
  }
  try {
    this.socket = webSocket.createWebSocket()
    this.socket.on('open', (err: BusinessError, value: Object) => {
      if (err === undefined || err === null) {
        this.isConnected = true
      }
    })
    this.socket.on('message', (err: BusinessError, value: string | ArrayBuffer) => {
      const text = typeof value === 'string' ? value : ''
      if (text !== '' && this.messageCallback !== null) {
        const msg = GameMessage.fromJson(text)
        this.messageCallback(msg)
      }
    })
    this.socket.on('close', (err: BusinessError, value: webSocket.CloseResult) => {
      this.isConnected = false
      if (this.statusCallback !== null) {
        this.statusCallback(false)
      }
      this.handleReconnect()
    })
    this.socket.on('error', (err: BusinessError) => {
      this.isConnected = false
      if (this.statusCallback !== null) {
        this.statusCallback(false)
      }
      this.handleReconnect()
    })
    this.socket.connect(this.serverUrl, (err: BusinessError, value: boolean) => {
      if (!err) {
        this.isConnected = true
        this.reconnectAttempts = 0
        if (this.statusCallback !== null) {
          this.statusCallback(true)
        }
      }
    })
  } catch (e) {
    this.handleReconnect()
  }
}

该方法的执行流程如下:

  1. 清理旧连接:如果存在已有的WebSocket实例,先调用close()关闭,避免连接泄漏
  2. 创建新实例:调用webSocket.createWebSocket()创建全新的WebSocket对象
  3. 订阅四个核心事件:open、message、close、error
  4. 发起连接:调用socket.connect(url, callback),在回调中更新连接状态
  5. 异常兜底:如果整个try块抛出异常,触发重连流程

值得注意的是,事件订阅必须在connect()调用之前完成。这是因为WebSocket的连接过程是异步的,如果先调用connect()再订阅事件,可能在事件回调注册完成之前就已经触发了open或error事件,导致回调丢失。这是一个典型的竞态条件问题,解决方案就是确保"先订阅,后连接"的顺序。

4.6 send方法

send(msg: GameMessage): boolean {
  if (this.socket !== null && this.isConnected) {
    this.socket.send(msg.toJson())
    return true
  }
  return false
}

send()方法采用"尽力发送"策略:仅在连接有效时发送消息,否则返回false表示发送失败。上层调用方应根据返回值决定是否缓存消息待重连后重发。这种设计避免了在未连接状态下调用socket.send()可能抛出的异常。

需要注意的是,socket.send()也是异步方法,它有两种重载:callback版和Promise版。当前GameNetwork使用的是无回调的调用方式,这在实时游戏场景中是合理的——游戏消息对发送确认的实时性要求不高,即使个别消息因网络波动未能发出,服务端也可通过状态同步机制补偿。

4.7 close方法

close(): void {
  if (this.socket !== null) {
    this.socket.close()
    this.socket = null
  }
  this.isConnected = false
}

close()方法执行两步操作:调用WebSocket实例的close()方法发送关闭帧,然后将引用置空释放资源。将socket置为null是必要的——否则后续的doConnect()在检查socket !== null时会尝试关闭一个已关闭的实例。

特别注意:close()不会触发handleReconnect()。这是由设计的——主动关闭是用户意图的体现,不应自动重连。只有close和error事件回调中的断开才被视为意外断开,从而触发重连。这种区分对于避免"用户退出时还在重连"的问题至关重要。

4.8 回调注册

export type MessageCallback = (msg: GameMessage) => void
export type StatusCallback = (connected: boolean) => void

onMessage(cb: MessageCallback): void {
  this.messageCallback = cb
}

onStatusChange(cb: StatusCallback): void {
  this.statusCallback = cb
}

GameNetwork使用回调模式而非事件总线模式进行消息分发。这种选择基于以下考量:

  • 简单直接:在GameNetwork的使用场景中,通常只有一个消费者(游戏页面),回调模式避免了事件总线的注册/反注册生命周期管理开销
  • 类型安全:回调函数的类型签名在编译时即可检查,而事件总线通常使用字符串事件名,容易拼写错误
  • 生命周期友好:ArkUI组件的aboutToDisappear()中只需将callback置空,无需复杂的反注册逻辑

5. webSocket.connect回调模式

5.1 AsyncCallback模式解析

HarmonyOS的WebSocket connect()方法使用AsyncCallback<boolean>模式而非Promise模式,这是许多习惯了Promise/async-await的开发者容易踩的坑。其方法签名如下:

connect(url: string, callback: AsyncCallback<boolean>): void

AsyncCallback<T>的标准签名为(err: BusinessError, value: T) => void。因此,connect回调的实际签名为:

(err: BusinessError, value: boolean) => void

这意味着你不能这样使用:

// 错误!connect不返回Promise
const result = await webSocket.createWebSocket().connect(url)
// 错误!connect不返回Promise
webSocket.createWebSocket().connect(url).then(...)

虽然SDK也提供了返回Promise的重载connect(url: string, options?: WebSocketRequestOptions): Promise<boolean>,但在ArkTS严格模式下,选择callback版本更稳妥,原因有二:一是callback版本是SDK文档中的主要示例写法,社区支持更完善;二是ArkTS对Promise的使用有额外限制(如async函数必须显式标注返回类型Promise<T>),而callback模式避开了这些限制。

5.2 callback中boolean值的含义陷阱

connect()回调中的value: boolean存在一个极易误解的语义:

  • true连接请求创建成功(即TCP连接请求已发出,WebSocket握手帧已发送)
  • false连接请求创建失败(如URL格式错误、参数非法等前置条件不满足)

关键value: true不代表WebSocket连接已成功建立!它仅表示连接的"发起"过程成功。真正的连接建立需要等待on('open')事件回调。

+-----------------------------------------------------------+
|           WebSocket 连接生命周期 - 事件时序                  |
|                                                           |
|  connect()调用                                            |
|      |                                                    |
|      v                                                    |
|  +------------------+                                    |
|  | callback(true)   | <-- 连接请求创建成功                 |
|  | value=true       |     (并非连接已建立!)                |
|  +--------+---------+                                    |
|           |                                              |
|           |  TCP三次握手 + HTTP Upgrade握手               |
|           |  (耗时数十至数百毫秒)                          |
|           v                                              |
|  +------------------+                                    |
|  | on('open')回调    | <-- 连接真正建立成功                 |
|  | err=null         |     (可以开始send)                   |
|  +------------------+                                    |
|                                                           |
|  如果服务端拒绝升级:                                      |
|  +------------------+                                    |
|  | on('error')回调   | <-- 连接失败                        |
|  | err.code=...     |                                    |
|  +------------------+                                    |
+-----------------------------------------------------------+

这个设计的历史原因在于:WebSocket的握手过程本质上是一个HTTP请求-响应过程,connect()方法只能启动这个过程,无法同步等待服务端的升级响应。因此,SDK设计者将connect()的callback语义定义为"连接发起是否成功"而非"连接是否建立",是合理的异步设计。

在GameNetwork的实现中,我们在connect回调中设置isConnected = true,这其实是一个简化处理。严格来说,应该仅在on('open')回调中才将状态设为CONNECTED。connect回调中只应将状态设为CONNECTING。这是后续迭代需要修正的点。

5.3 回调参数详解

this.socket.connect(this.serverUrl, (err: BusinessError, value: boolean) => {
  if (!err) {
    this.isConnected = true
    this.reconnectAttempts = 0
    if (this.statusCallback !== null) {
      this.statusCallback(true)
    }
  }
})

err参数的处理采用!err判断,这是HarmonyOS AsyncCallback的标准模式:当err为空或undefined时表示操作成功,否则err包含错误信息。常见错误码包括:401(参数错误/URL格式错误)、201(权限被拒绝/缺少INTERNET权限)、2302001(WebSocket URL错误)、2302003(WebSocket连接已存在)、2302999(内部错误)。


6. webSocket.on(‘message’)双参数

6.1 回调签名解析

HarmonyOS WebSocket的on('message')事件回调采用双参数设计:

on('message', (err: BusinessError, value: string | ArrayBuffer) => void)

这个签名与浏览器端的WebSocket API存在显著差异。在浏览器环境中,message事件的回调签名为(event: MessageEvent) => void,只有一个参数。而HarmonyOS采用了与其他事件(open、close、error)一致的双参数模式:第一个参数为错误对象,第二个参数为数据值。

这种设计的优势在于统一性:所有事件回调都遵循(err, value)的模式,便于记忆和理解。但对于习惯了浏览器WebSocket API的开发者来说,很容易忽略err参数,直接将value作为消息内容处理,导致编译错误或运行时异常。

6.2 value的联合类型:string | ArrayBuffer

value参数的类型为string | ArrayBuffer,这是因为WebSocket协议支持两种帧类型:

  • Text Frame:承载UTF-8文本数据,回调中的value类型为string
  • Binary Frame:承载二进制数据,回调中的value类型为ArrayBuffer

NearPlay协议仅使用Text Frame(JSON字符串),因此GameNetwork在message回调中做了类型筛选:

this.socket.on('message', (err: BusinessError, value: string | ArrayBuffer) => {
  const text = typeof value === 'string' ? value : ''
  if (text !== '' && this.messageCallback !== null) {
    const msg = GameMessage.fromJson(text)
    this.messageCallback(msg)
  }
})

这段代码的逻辑是:如果收到的是字符串,正常处理;如果收到的是ArrayBuffer(二进制数据),则忽略。这种处理方式在当前场景下是合理的,因为NearPlay不使用二进制帧。如果未来需要支持二进制数据(如游戏快照的Protobuf编码),可以扩展这里的逻辑。

6.3 err参数的处理

在当前实现中,message回调未对err参数做显式检查。这是一个潜在的改进点——理想情况下,应先检查err是否为空,再处理value:

this.socket.on('message', (err: BusinessError, value: string | ArrayBuffer) => {
  if (err !== undefined && err !== null) {
    // 消息接收出错,记录日志或触发错误回调
    return
  }
  const text = typeof value === 'string' ? value : ''
  if (text !== '' && this.messageCallback !== null) {
    const msg = GameMessage.fromJson(text)
    this.messageCallback(msg)
  }
})

虽然message事件的err通常为空(消息成功接收时不会有错误),但在某些边界情况下(如消息解码失败、连接已半关闭时收到残留数据),err可能非空。正确处理err参数是健壮的网络层代码的基本要求。

6.4 与浏览器WebSocket API的对比

维度 浏览器WebSocket HarmonyOS WebSocket
message回调签名 (event: MessageEvent) => void (err: BusinessError, value: string | ArrayBuffer) => void
数据访问 event.data 直接通过value参数
类型判断 typeof event.data typeof value
错误处理 无err参数 第一个参数为err
Binary数据 Blob / ArrayBuffer ArrayBuffer

这种差异意味着:如果你从浏览器端移植WebSocket代码到HarmonyOS,必须重写message事件的回调签名。直接复制浏览器代码会导致编译错误,因为参数数量和类型都不匹配。


7. 自动重连机制

7.1 设计原理

移动网络环境的不稳定性是NearPlay必须面对的核心挑战。地铁隧道、电梯、信号盲区等场景都可能导致WebSocket连接意外断开。自动重连机制的目标是:在连接意外断开时,尽可能透明地恢复连接,让上层业务感知最小的中断。

GameNetwork当前的重连实现基于handleReconnect()方法,在close和error事件回调中触发:

private handleReconnect(): void {
  if (this.reconnectAttempts < this.maxReconnect) {
    this.reconnectAttempts++
    setTimeout(() => { this.doConnect() }, 3000)
  }
}

这段代码的逻辑简洁明了:检查重连次数是否超过上限(5次),未超过则延迟3秒后重新调用doConnect()

7.2 指数退避策略(设计扩展)

当前实现使用固定3秒的重连间隔,这在实际运行中存在两个问题:

  1. 网络拥塞加剧:如果服务端因过载而断开大量连接,所有客户端在同一时刻(3秒后)同时重连,会形成"重连风暴",进一步加剧服务端压力
  2. 用户体验不佳:在短暂网络波动(如切换WiFi热点,通常1-2秒恢复)的场景下,3秒的固定等待过于保守;而在长时间断网场景下,3秒一次的频繁重连又浪费电量和带宽

指数退避(Exponential Backoff)策略能有效解决上述问题。其核心思想是:每次重连的等待时间按指数增长,如1秒、2秒、4秒、8秒、16秒。这样在短暂断网时能快速恢复,在长时间断网时也不会过度消耗资源。

设计扩展代码如下:

private handleReconnect(): void {
  if (this.reconnectAttempts < this.maxReconnect) {
    this.reconnectAttempts++
    const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts - 1), 30000)
    const jitter = Math.random() * delay * 0.3
    setTimeout(() => { this.doConnect() }, delay + jitter)
  }
}

这里引入了两个关键改进:

  • 指数增长1000 * Math.pow(2, n - 1)产生1、2、4、8、16秒的等待序列,上限30秒
  • 随机抖动(Jitter):在计算出的延迟基础上增加0-30%的随机偏移,避免多个客户端在同一时刻同时重连

7.3 最大重试次数

maxReconnect = 5的设定基于以下考量:

  • 5次重连配合指数退避,总等待时间约为1+2+4+8+16=31秒,足以覆盖大多数临时性网络中断
  • 超过5次仍无法连接,通常意味着服务端故障或网络彻底不可用,继续重连已无意义
  • 用户在此期间可能已经切换到其他操作(如返回大厅),自动重连应适时终止

当重连次数超过上限后,GameNetwork进入DISCONNECTED状态,不再尝试重连。上层业务应在此状态下显示"连接已断开"的提示,并提供手动重连按钮。

7.4 重连后的状态恢复

重连成功后,服务端需要能够识别重连的客户端并恢复其游戏状态。这要求:

  1. 客户端在重连时携带之前的服务端分配的会话标识(如token或sessionId)
  2. 服务端在检测到同一客户端重连时,将其重新加入之前的房间,并推送最新的游戏快照
  3. 重连期间错过的消息由服务端缓存并在重连后补发,或通过状态快照机制覆盖

当前GameNetwork在重连时仅重新调用doConnect(),未携带会话标识。这是后续迭代需要增强的关键点——需要在connect时通过WebSocketRequestOptions的header字段传递会话信息。


8. 心跳设计

8.1 为什么需要心跳

WebSocket连接建立后,如果双方长时间不发送数据,中间的网络设备(如NAT网关、防火墙、负载均衡器)可能会因为连接空闲而主动关闭连接。这种"静默断连"对应用层是透明的——客户端和服务端都不知道连接已失效,直到下一次send操作失败。

心跳机制通过定期发送小数据包来保持连接活跃,同时也能用于检测连接是否仍然有效。HarmonyOS的WebSocket从API 9开始原生支持心跳检测机制,通过WebSocketRequestOptionspingIntervalpongTimeout参数配置:

const options: webSocket.WebSocketRequestOptions = {
  pingInterval: 30,   // 每30秒发送一次Ping帧
  pongTimeout: 10     // 10秒内未收到Pong帧则断开连接
}

8.2 Ping-Pong机制

WebSocket协议定义了Ping/Pong控制帧用于心跳检测:

  Client                              Server
    |                                    |
    |  -------- Ping Frame -------->    |
    |  (协议层自动发送,无需应用代码)      |
    |                                    |
    |  <------- Pong Frame ---------    |
    |  (服务端收到Ping后自动回复)          |
    |                                    |
    |  如果pongTimeout内未收到Pong:       |
    |  连接视为断开,触发close/error事件   |
    |                                    |

Ping帧由客户端协议层自动发送(无需应用代码调用send),服务端收到Ping后按照RFC 6455规范必须自动回复Pong帧。如果客户端在pongTimeout秒内未收到Pong帧,则认为连接已失效,主动断开并触发close事件。

8.3 当前实现的改进方向

当前GameNetwork未使用SDK内置的心跳机制,也未实现应用层心跳。建议的改进方案:

  1. 优先使用SDK内置心跳:在WebSocketRequestOptions中配置pingIntervalpongTimeout,让协议层自动维护心跳,无需应用代码介入。具体配置方式是在GameNetwork的doConnect方法中,将options对象传递给connect方法的第二个参数
  2. 应用层心跳作为补充:对于不支持WebSocket Ping/Pong的代理服务器(如某些HTTP反向代理会剥离WebSocket控制帧),可使用应用层心跳——定期发送一条type=SYSTEM、action="HEARTBEAT"的GameMessage,服务端收到后回复确认消息。应用层心跳的缺点是消息需要经过完整的序列化/反序列化流程,相比协议层Ping帧的零开销,性能开销更大
  3. 心跳间隔的选择策略:心跳间隔过短会浪费电量和带宽,过长则无法及时发现连接断开。对于NearPlay的实时游戏场景,建议pingInterval设为15-30秒——这个范围内既能及时发现连接异常,又不会对移动设备的续航产生明显影响。在游戏进行中(PLAYING状态)可缩短至15秒以确保实时性,在等待状态(WAITING)可放宽至30秒以节省电量

9. 房间管理

9.1 房间生命周期

NearPlay的房间管理围绕WebSocket连接展开,每个房间对应服务端的一个游戏实例,通过roomId唯一标识。房间的完整生命周期如下:

  +------------+     +------------+     +------------+     +------------+
  |  LOBBY     | --> | WAITING    | --> | PLAYING    | --> | FINISHED   |
  |  (大厅)     |     | (等待开始)  |     | (游戏中)    |     | (游戏结束)   |
  +------------+     +------------+     +------------+     +------------+
       |                   |                   |                   |
   创建房间            玩家加入           游戏逻辑执行          结果展示
   分配roomId          等待房主START      消息收发频繁          房间销毁或
                       人满自动提醒                            返回WAITING

LOBBY:玩家在大厅页面选择创建或加入房间。创建房间时,客户端向服务端发送CREATE_ROOM请求,服务端生成唯一roomId并返回。加入房间时,客户端发送JOIN消息携带目标roomId。

WAITING:房间创建后进入等待状态。此时玩家可以聊天(CHAT消息)、查看房间成员列表(STATE同步)。房主可以开始游戏(START消息),触发状态转换至PLAYING。

PLAYING:游戏进行中,消息收发最为频繁。根据游戏类型不同,消息模式各异:狼人杀以VOTE、NIGHT_ACTION、ROLE_ASSIGN为主;你来比划我来猜以SNAP、DESCRIBE、GUESS为主。

FINISHED:游戏结束,服务端发送REVEAL消息公布最终结果。房间可以选择销毁或重新开局(回到WAITING状态)。

9.2 房间消息路由

服务端以房间为单位管理WebSocket连接的广播范围:

                    服务端消息路由模型

  +--------+    +--------+    +--------+
  | Client |    | Client |    | Client |
  |  A     |    |  B     |    |  C     |
  +---+----+    +---+----+    +---+----+
      |             |             |
      +------+------+------+------+
             |             |
        +----+----+   +----+----+
        | Room 1  |   | Room 2  |
        |roomId=r1|   |roomId=r2|
        | [A,B]   |   | [C]     |
        +---------+   +---------+
             |
        消息路由规则:
        - roomId=r1的消息 -> 广播给A和B
        - roomId=r2的消息 -> 广播给C
        - toUserId非空 -> 仅推送给指定用户

9.3 房间状态同步

当玩家加入或离开房间时,服务端向房间内所有成员推送STATE类型消息,同步最新的房间状态(成员列表、游戏进度等)。这确保了所有客户端的房间视图一致。

断线重连的玩家在重新建立WebSocket连接后,服务端根据其携带的会话信息识别身份,将其重新加入之前的房间,并推送完整的房间状态快照,使其快速恢复到断线前的状态。


10. 错误处理体系

10.1 错误分类

NearPlay网络层的错误可分为三大类:

连接错误:发生在WebSocket连接建立阶段,由connect回调的err参数或on(‘error’)事件回调报告。常见错误码包括201(权限拒绝)、2302001(URL错误)、2302003(连接已存在)、2302999(内部错误)。此类错误的处理策略是自动重连。

通信错误:发生在连接建立后的消息收发阶段,由on(‘error’)事件回调报告。可能的原因包括网络中断、服务端异常关闭、消息编码错误等。此类错误触发连接状态转换为RECONNECTING,随后自动重连。

业务错误:发生在应用层,如消息格式错误、游戏逻辑违规等。此类错误不触发重连,而是通过错误消息(type=SYSTEM,action=“ERROR”)通知上层业务。

10.2 错误传播模型

  WebSocket底层          GameNetwork           业务层(UI)
  +-----------+         +-----------+         +-----------+
  | on('error')| -----> | handleRe  |         |           |
  | on('close')| -----> | connect() |         |           |
  |           |         |           | -----> |statusCb    |
  |           |         |           |         |(false)    |
  |           |         |           |         | -> 显示   |
  |           |         |           |         |   断线提示 |
  | on('open') | -----> |isConnected| -----> |statusCb    |
  |           |         |= true     |         |(true)     |
  |           |         |           |         | -> 隐藏   |
  |           |         |           |         |   断线提示 |
  | on('message')| ---> |fromJson   | -----> |messageCb   |
  |           |         |           |         | -> 处理   |
  |           |         |           |         |   游戏逻辑 |
  +-----------+         +-----------+         +-----------+

10.3 错误处理原则

  1. 永不崩溃:网络层代码必须对任何异常输入都具有容错能力。GameMessage.fromJson()的try-catch确保了格式错误的消息不会导致崩溃。对于服务端推送的非法JSON、缺失字段的半成品消息、甚至空字符串,fromJson都会返回一个带默认值的安全GameMessage对象,而不是抛出异常导致应用闪退
  2. 静默重连:自动重连过程对上层业务透明,仅在重连失败达到上限后才通知上层。重连期间,上层业务可以继续正常处理用户交互(如查看历史消息),只是新操作无法发送。这种设计避免了断线时界面突然弹出大量错误提示的糟糕体验
  3. 状态驱动UI:所有连接状态变化通过statusCallback传播到UI层,由UI决定如何展示(加载动画、断线提示、重连按钮等)。GameNetwork本身不持有任何UI引用,完全解耦,确保了网络层的可测试性和可复用性
  4. 错误码分类处理:不同错误码对应不同的处理策略,201错误提示用户检查权限,2302001错误提示URL配置问题,2302999错误触发自动重连。未来可以建立一个错误码到用户友好提示的映射表,将技术性的错误码转化为用户可理解的操作建议

11. 未来:分布式软总线迁移

11.1 迁移动机

虽然WebSocket作为NearPlay的主通信协议在跨网络场景下是最优选择,但在局域网多人游戏场景(如同一个家庭中的设备互连)中,分布式软总线提供了更优的通信性能。具体而言:

  • 延迟优势:分布式软总线在局域网环境下的通信延迟低于1毫秒,而WebSocket即使在本局域网内也需要经过TCP协议栈和HTTP升级握手,延迟通常在10-50毫秒
  • 带宽优势:分布式软总线的内核级传输无需HTTP/WebSocket协议封装,每条消息节省2-10字节的协议头部
  • 功耗优势:无需维护TCP长连接的心跳,降低无线网卡的活跃时间,延长设备续航

11.2 混合通信架构

未来的迁移方向是构建WebSocket + 分布式软总线的混合通信架构:

  +---------------------------------------------------------------+
  |              NearPlay 混合通信架构 (未来)                        |
  |                                                               |
  |   +-------+                    +-------+                      |
  |   |Device |  分布式软总线       |Device |                      |
  |   |  A    | <===============  |  B    |                      |
  |   +---+---+  (局域网, <1ms)    +---+---+                      |
  |       |                            |                          |
  |       | WebSocket                   | WebSocket                |
  |       v                            v                          |
  |   +-------------------------------------------+              |
  |   |              Adjudicator (Server)          |              |
  |   |                                           |              |
  |   |  - 局域网玩家: 通过分布式软总线直接通信     |              |
  |   |  - 远程玩家: 通过WebSocket通信              |              |
  |   |  - 混合房间: 两种通道并存, 透明路由          |              |
  |   +-------------------------------------------+              |
  |                          ^                                    |
  |                          | WebSocket                          |
  |                   +------+-------+                            |
  |                   |   Device C   |                            |
  |                   |  (远程玩家)   |                            |
  |                   +--------------+                            |
  +---------------------------------------------------------------+

11.3 迁移路径

实现混合通信架构需要以下步骤:

  1. 抽象通信接口:将GameNetwork的connect/send/close/onMessage抽象为IGameTransport接口,WebSocket和分布式软总线分别实现该接口
  2. 通道选择策略:在连接建立时检测是否处于同一局域网且同华为账号,满足条件则优先使用分布式软总线,否则回退到WebSocket
  3. 通道切换:在游戏过程中如果检测到局域网连接可用,自动将WebSocket通道切换为分布式软总线,无需中断游戏
  4. 消息格式兼容:两种通道使用相同的GameMessage JSON格式,确保服务端和客户端的消息处理逻辑无需修改

11.4 技术挑战

分布式软总线迁移面临的主要挑战包括:

  • 账号限制:当前分布式软总线仅支持同华为账号下的设备互连,跨账号多人游戏的场景仍需依赖WebSocket
  • API差异:分布式软总线的消息收发API(如@kit.DistributedServiceKit的分布式数据同步)与WebSocket的事件驱动模型差异较大,需要精心设计适配层
  • 调试困难:分布式软总线的通信链路在内核层建立,传统的网络抓包工具无法捕获其数据帧,增加了调试难度
  • 设备发现:使用分布式软总线前需要进行设备发现和认证,这个过程增加了连接建立的耗时

尽管存在上述挑战,分布式软总线迁移仍然是NearPlay通信架构的重要演进方向。随着HarmonyOS生态的成熟和分布式能力的开放,局域网场景下的通信性能提升将显著改善用户的游戏体验。特别是在家庭聚会、朋友面对面游戏等典型使用场景中,分布式软总线的超低延迟将为玩家带来接近面对面桌游的流畅交互体验,真正实现"多设备协同、一桌游戏"的愿景。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐