我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~

前言

老实说,“同一账号多设备互联”做到局域网(同网段)很轻松,一出小区网关就“失联”,多数团队下意识地全量走云中转,既贵又卡。问题不在天时地利,问题在“没有把控制面与数据面拆开”“没把候选链路排兵布阵”“RPC 层没做成可插拔”。
  这篇就沿着你给的大纲,分布式网络 → NAT 穿透 → RPC 机制,基于鸿蒙场景给出
能直接落地的模块骨架
:上层用 IPCProxy 做到“像调本地接口一样调远端”,下层用一个抽象的 DistributedNetworkSoftBus/本地直连/跨网洞穿/云中继 排成阶梯式优先级。写完你会发现:跨网段不必等于“全部走云”,最贵的路只做兜底。💪

总图先立起来:三层两面,心里有数

  • 应用层:你的业务服务(KV、文件、白板、媒体控制…)

  • RPC 层IPCProxy(调用方) ↔ IPCStub(被调方),可在任意“传输”上跑

  • 传输层(数据面)DistributedNetwork 聚合多种“通道”

    • L0:DSoftBus / Nearby(同网段/近距优先)
    • L1:直连 UDP(NAT 打洞成功)
    • L2:直连 TCP/QUIC(对称 NAT 或防火墙受限时的 TCP 同时打开/QUIC)
    • L3:云中继 WebSocket/TURN(永远兜底)
  • 控制面:设备发现、会话信令、候选收集与协商(像简化版的 ICE)

  • 安全面:设备身份、密钥交换、流量加密(Noise/TLS)

策略一句话“近优、廉优、稳优,贵的最后上”。先 SoftBus 探测,再 UDP 打洞,不行才走云。

一、分布式网络(DistributedNetwork):把链路做成“可编排”的梯子

1.1 模块职责

  • 统一的 Transport 抽象:connect/send/close/onMessage
  • 策略调度:按照优先级并行探测,择优建流 + 故障切换
  • 端到端加密:握手后派生会话密钥
  • 多路复用:一条底层连接跑多个逻辑通道(RPC/信令/文件)

1.2 传输抽象与实现(ArkTS/示意)

// network/Transport.ts
export interface Transport {
  readonly name: string
  connect(target: Endpoint, opts?: ConnectOptions): Promise<void>
  send(data: Uint8Array): Promise<void>
  close(code?: number): Promise<void>
  onMessage?: (data: Uint8Array) => void
  onClose?: (reason?: string) => void
}

export interface Endpoint {
  deviceId: string
  // 候选地址:SoftBus-id、(ip,port,proto)、relayUrl...
  candidates: Candidate[]
}

export type Candidate =
  | { type: 'softbus', busName: string }
  | { type: 'udp', ip: string, port: number }
  | { type: 'tcp', ip: string, port: number }
  | { type: 'quic', ip: string, port: number }
  | { type: 'relay', url: string, token: string }

export interface ConnectOptions { timeoutMs?: number }
// network/transports/SoftBusTransport.ts(示意:包装鸿蒙分布式/近距能力)
export class SoftBusTransport implements Transport { /* connect by busName … */ name='softbus' /* … */ }
// network/transports/UdpTransport.ts(示意:打洞成功后走 UDP)
export class UdpTransport implements Transport { /* dgram-like,握手后加密帧 … */ name='udp' /* … */ }
// network/transports/RelayWsTransport.ts(兜底:云中继)
export class RelayWsTransport implements Transport { /* WebSocket over TLS … */ name='relay-ws' /* … */ }

1.3 策略调度器

// network/DistributedNetwork.ts
export class DistributedNetwork {
  private tr?: Transport

  async smartConnect(ep: Endpoint, deadlineMs=6000) {
    // 并行发起:SoftBus(0ms), UDP-Hole(0ms), TCP/QUIC(300ms), Relay(800ms)
    const attempts = [
      () => this.trySoftBus(ep),
      () => this.tryUdpHole(ep),
      () => this.tryTcpOrQuic(ep),
      () => this.tryRelay(ep),
    ]
    const winner = await raceWithStagger(attempts, deadlineMs)
    this.tr = winner
    return winner
  }

  send(data: Uint8Array) { return this.tr?.send(data) }
  close() { return this.tr?.close() }

  // ……各 tryXxx 内部会用相应 Transport.connect 并完成握手 + 加密
}

这一步的意义是:你的上层不需要知道“我现在走哪条路”DistributedNetwork 会自动选最优并在出问题时切换。


二、NAT 穿透:跨网段的“关键一脚”

2.1 思维框架:小型 ICE

  • 候选收集:每端向“信令服务”上报

    • 内网地址(多网卡)
    • 公网候选(经 STUN 获取)
    • 中继候选(云 relay/TURN)
  • 连通性检查:UDPHole → TCP Simultaneous Open/QUIC → Relay

  • 保活:UDP 打洞成功后 15–25s 心跳,TCP/QUIC 也要 keepalive

  • NAT 类型差异

    • Full/Restricted Cone:UDP 打洞成功率高
    • Symmetric NAT:基本上需要中继或 QUIC/TCP 的某些运气

2.2 穿透握手流程(简化时序)

A,B 向信令服务注册: 交换 candidates + ufrag/pwd(或临时 token)
并行尝试:
1) UDP:A→B 发穿透包、B→A 回打洞包,多对多探测
2) 若失败,尝试 TCP 同时打开 or QUIC
3) 若仍失败,双方连 Relay(WebSocket/TURN),由 Relay 转发数据
选择最先成功的通道,公布为 primary,其他降级为备用

2.3 打洞实现(示意)

// network/holepunch/UdpHolePuncher.ts
export class UdpHolePuncher {
  async punch(local: UdpSocket, candA: UdpCand, candB: UdpCand, timeoutMs=2000): Promise<{ip:string,port:number}> {
    // 并发向对方所有 UDP 候选发“同步包”
    const tickets = allPairs(candA, candB).map(pair => tryPair(local, pair))
    const ok = await Promise.any(tickets.map(t => withTimeout(t, timeoutMs)))
    return ok // 返回可用对端 (ip,port)
  }
}

要点并行 + 收敛;不要单点串行试错,超时一长,体验就“漏拍”。

2.4 失败预案:中继不丢脸,丢的是体验

  • 优先选 就近地域 的中继(延迟敏感业务甚至自建省内/城域节点)
  • 数据面用 WebSocket over TLSTURN/UDP
  • 费用可控:只在无法直连时启用,业务层感知“通道类型”,比如白板/文本的阈值 <100ms,用 relay 也可接受;音视频则提醒用户“网络不佳自动降质”。

三、RPC 机制:IPCProxy 让“远端像本地”

3.1 为什么别直接塞 JSON 字符串

  • 需要 请求-响应配对id
  • 需要 流控与背压(避免一端溢出)
  • 需要 方法版本/扩展字段(向后兼容)
  • 需要 压缩/二进制(CBOR/Protobuf,比 JSON 省带宽)

3.2 抽象:可插拔传输上的通道复用

// rpc/Framing.ts —— 简易帧/路由
export interface Frame {
  ch: number   // channel id
  type: 'req'|'res'|'push'
  id?: number
  method?: string
  payload?: Uint8Array
  ok?: boolean
  err?: string
}
// rpc/IPCProxy.ts —— 生成“远程对象”的代理
export class IPCProxy<T extends object> {
  private nextId = 1
  constructor(private sendRaw: (f: Frame)=>void, private ch=1) {}

  call<R=unknown>(method: string, payload: Uint8Array, timeoutMs=5000): Promise<R> {
    const id = this.nextId++
    const fut = defer<R>()
    // 发送请求帧
    this.sendRaw({ ch: this.ch, type:'req', id, method, payload })
    // 等待相同 id 的响应帧(此处省略路由器实现)
    return withTimeout(fut.promise, timeoutMs)
  }

  // 语法糖:把对象方法“映射”为 call
  bind<K extends keyof T>(name: K) {
    return async (arg: any) => this.call(name as string, encode(arg))
  }
}
// rpc/IPCStub.ts —— 服务端桩:注册方法与处理
export class IPCStub {
  private handlers = new Map<string,(arg:any)=>Promise<any>>()

  on(method: string, fn: (arg:any)=>Promise<any>) { this.handlers.set(method, fn) }

  async handle(frame: Frame, reply: (f: Frame)=>void) {
    if (frame.type !== 'req' || !frame.method || !frame.id) return
    try {
      const fn = this.handlers.get(frame.method)
      if (!fn) throw new Error(`No such method: ${frame.method}`)
      const result = await fn(decode(frame.payload))
      reply({ ch: frame.ch, type:'res', id: frame.id, ok: true, payload: encode(result) })
    } catch (e:any) {
      reply({ ch: frame.ch, type:'res', id: frame.id, ok: false, err: e?.message ?? 'ERR' })
    }
  }
}

这样,同一个 DistributedNetwork 连接可以复用很多个 channel(1=RPC,2=文件传输,3=心跳…),上层完全不知道下层走的是 SoftBus、洞穿,还是中继

3.3 与鸿蒙 IPC 的关系

  • 同设备/同进程间:照用 IRemoteObject/RemoteObject
  • 跨设备IPCProxy 跑在 DistributedNetwork 之上;
  • 如果在局域网且有 DSoftBus,可以把 SoftBusTransport 做成透明通道,不改 RPC 层一行代码

3.4 一个“跨网段 KV 服务”小样

服务端(被调设备)

// app/KVServer.ts
import { IPCStub } from './rpc/IPCStub'
import { DistributedNetwork } from './network/DistributedNetwork'

const kv = new Map<string,string>()
const stub = new IPCStub()
stub.on('kv.put', async ({k,v}) => { kv.set(k,v); return { ok: true } })
stub.on('kv.get', async ({k}) => ({ v: kv.get(k) ?? null }))

// 绑定网络
const dn = new DistributedNetwork()
dn.onMessage = (bytes) => router.route(decodeFrame(bytes), (f)=> dn.send(encodeFrame(f)))

客户端(发起设备)

// app/KVClient.ts
import { IPCProxy } from './rpc/IPCProxy'
import { DistributedNetwork } from './network/DistributedNetwork'

const dn = new DistributedNetwork()
await dn.smartConnect(targetEndpoint)

const proxy = new IPCProxy<{ 'kv.put': (arg:{k:string,v:string})=>Promise<any>,
                             'kv.get': (arg:{k:string})=>Promise<{v:string|null}> }>(
  (f)=> dn.send(encodeFrame(f))
)

await proxy.bind('kv.put')({ k:'hello', v:'Harmony' })
const { v } = await proxy.bind('kv.get')({ k:'hello' })
console.log('value =', v) // => Harmony

上面这套,不管你是 SoftBus 直连、UDP 打洞直连还是云中继,调用姿势完全一致。这就是“可插拔的 RPC”。


四、安全 & 可观测:不能偷工减料的两件事

  • 设备身份:首次配对走 PAKE/扫码 交换公钥,之后用 证书/指纹 快速信任

  • 握手协议:推荐 Noise XK(或 TLS 1.3),握手完成后派生会话密钥

  • 密钥轮换:长会话(>1h)或传输数据量阈值后自动轮换

  • 速率限制:信令服务对每设备做 token bucket,避免探测风暴

  • 埋点

    • 直连命中率(SoftBus/UDP/TCP/Relay)
    • 首包到达时延 P50/P95
    • 会话存活时间/重连次数
    • NAT 类型分布(匿名统计)
  • 降级策略:同一会话连续 2 次丢包率 > 20% 且 RTT 抖动大 → 自动切到 relay,并在后台继续探测直连机会


五、把它放进项目:最小落地步骤(Checklist)

  1. 抽象传输:把现有“网络调用”改成走 Transport 接口(WebSocket 也行)
  2. 接入 DistributedNetwork:加上 SoftBus 封装与洞穿/relay 的并行探测
  3. 封 RPC:引入 IPCProxy/IPCStub;业务只认“方法名 + 参数”
  4. 信令与 STUN:选一套最省心的(自建或现成);先跑起来再逐步优化 NAT 覆盖
  5. 加密/鉴权:至少先用 TLS + 设备 token,随后升级到 Noise/双向证书
  6. 埋点与容灾:所有通道都上指标 + 自动降级
  7. 灰度:先对内测设备开启“直连优先”,观察 1~2 周,再扩大

六、常见坑(提前替你踩)🕳️

  • 把信令与数据放同一条中继:信令拥堵会拖垮数据通道;分开
  • 单线程串行探测:一条条试,超时累加,体验雪崩;并行 + 竞速收敛
  • UDP 打洞成功却被网关回收:没有心跳;20s 左右心跳保活。
  • RPC 直接 JSON:大对象开销大、无背压,掉线重入困难;二进制帧 + 请求 id
  • 只做直连不做兜底:一旦遇到对称 NAT 就“黑屏”;relay 必须常备
  • 不做密钥轮换:长连泄露风险高;量/时轮换
  • DSoftBus 成功就万事大吉:跨网后必然失败;把 SoftBus 作为 L0 候选,不要把它当唯一解

尾声:跨网段不等于“投降给云”,是“按阶梯拿回控制权”

当你把 DistributedNetwork 做成会挑路的梯子,把 NAT 穿透做成并行竞速,再把 IPCProxy 做成传输无感的 RPC,跨网段这件事就从“玄学”变成“工程活”。

(未完待续)

Logo

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

更多推荐