请添加图片描述

前言

在 HarmonyOS(鸿蒙操作系统)的宏大愿景中,“万物互联”与“分布式流转”是其区别于传统移动操作系统的绝对核心。当我们谈论“跨端协同”、“应用接续”或者“多机位模式”时,支撑这些炫酷上层体验的底层基石,正是高效、低延迟、高并发的近场通信技术

随着华为星闪(NearLink)技术的全面商用,近场通信的带宽、时延和抗干扰能力实现了代际飞跃。但在软件应用层,无论是基于 Wi-Fi Direct、传统蓝牙,还是最新的星闪底层,开发者面对的通信抽象模型依然是经典的 Socket 编程范式:通过 UDP 广播进行设备发现(Device Discovery),通过 TCP/原生协议栈建立可靠连接(Reliable Connection),最终完成双向的数据传输(Data Transmission)

本文将基于一段高度浓缩且结构完整的 ArkUI 跨端通信模拟源码,为您进行逐行、底层原理级别的深度拆解。我们将剥开 UI 的外衣,深入探讨设备扫描、三次握手、状态机流转以及应用层 ACK 确认机制,带您全面掌握 HarmonyOS 下一代分布式跨端通信的核心开发范式。


一、 通信拓扑架构与领域数据模型

在编写跨端通信应用时,应用本身既是“客户端(Client)”,也可能是“服务端(Server)”。为了管理复杂的网络拓扑和通信状态,我们首先需要建立严谨的数据契约。

1. 核心数据模型定义
// 远端对等设备实体
interface PeerDevice { 
  name: string;      // 设备名称 (如:真机A)
  ip: string;        // 局域网/近场 IP 地址
  port: number;      // 监听的通信端口
  rssi: number;      // 信号强度指示 (Received Signal Strength Indication)
  available: boolean;// 设备当前是否可连接
}

// 通信日志记录实体
interface LogItem { id: number; time: string; text: string; color: string; }

// 会话消息实体
interface ChatItem { id: number; from: string; text: string; mine: boolean; time: string; }

// UI 选项卡实体
interface TabItem { i: number; t: string; }

底层设计原理解析:
在这组接口中,PeerDevice 是最具分布式特征的模型。

  • **ipport**:构成了套接字(Socket)通信的绝对寻址坐标。在近场通信中,这通常是局域网 IP(如 192.168.x.x)或由 P2P 协议自动分配的虚拟 IP。
  • rssi(信号强度):这是一个非常关键的物理层数据。在真实的设备发现中,应用层往往会根据 RSSI 的值来判断设备的物理距离。RSSI 值越大(负数越接近0),信号越强。系统通常会根据 RSSI 进行自动排序,优先向用户推荐距离最近的设备。
2. 全局网络状态机
@Entry
@Component
struct Index {
  // 本机网络身份标识
  @State myName: string = '本机-' + Math.floor(Math.random() * 900 + 100)
  @State myIp: string = '192.168.1.100'
  
  // 目标设备连接状态
  @State targetName: string = '未连接'
  @State targetIp: string = ''
  @State targetPort: number = 8848
  
  // 宏观网络流转状态锁
  @State isScanning: boolean = false
  @State isConnected: boolean = false
  
  // 数据容器
  @State peers: PeerDevice[] = [ ... ] // 模拟发现的设备列表
  @State logs: LogItem[] = []
  @State chats: ChatItem[] = []
  
  // 原生 Socket 句柄保留(为真实生产环境准备)
  private udpSocket: socket.UDPSocket | null = null
  private tcpSocket: socket.TCPSocket | null = null
  
  // ...
}

状态机架构思考:
在 UI 层,我们通过 @State isScanning@State isConnected 构筑了严格的互斥状态锁。网络通信是典型的异步 IO 操作,用户极有可能在扫描尚未结束时点击连接,或在连接尚未断开时强行关闭应用。这些状态锁将直接绑定到 UI 按钮的可用性上,防止底层 Socket 句柄发生竞态崩溃。


二、 网络能力申请与生命周期管理

在 HarmonyOS 中,网络操作属于敏感权限。进行任何近场广播或连接前,必须进行底层网络能力的检查与声明。

1. 初始化与能力校验
import connection from '@ohos.net.connection';

  private initNetwork(): void {
    try {
      // 声明需要的网络能力:支持 Wi-Fi 或 蜂窝网络,且具备互联网/局域网互通能力
      const netCap: connection.NetCapabilities = {
        bearerTypes: [connection.NetBearType.BEARER_WIFI, connection.NetBearType.BEARER_CELLULAR],
        networkCap: [connection.NetCap.NET_CAPABILITY_INTERNET]
      }
      void netCap // 压制未使用警告,真实环境需调用 connection.hasDefaultNet()
      this.appendLog('[Network] 已申请 INTERNET / WIFI 能力', '#26A69A')
    } catch (e) {
      this.appendLog('[Network] 能力查询失败,使用模拟数据', '#EF5350')
    }
  }

2. Socket 句柄的安全释放
  aboutToDisappear(): void {
    try {
      // 【极客铁律】:应用退出或组件销毁时,必须强制关闭所有底层文件描述符(FD)
      if (this.udpSocket) { this.udpSocket.close(); this.udpSocket = null }
      if (this.tcpSocket) { this.tcpSocket.close(); this.tcpSocket = null }
    } catch (e) {
      this.appendLog('资源清理异常', '#EF5350')
    }
  }

操作系统级避坑指南:
Socket 是一种极其昂贵的操作系统内核级资源。在 aboutToDisappear 中调用 .close() 是防止“端口占用(Address already in use)”的唯一手段。如果在开发中不捕获并关闭这些句柄,当你第二次打开 App 时,绑定同一端口的监听服务将直接抛出系统级异常。


三、 设备发现机制:基于 UDP 广播的空间探测

在两台设备知道彼此的存在之前,它们如何建立联系?答案是广播(Broadcast)多播(Multicast)。这段代码利用 startScan 函数完美模拟了基于 UDP 的设备发现过程。

  private startScan(): void {
    if (this.isScanning) { return } // 防抖拦截
    this.isScanning = true
    
    // 1. 模拟底层 UDP 广播发射
    this.appendLog('[Scan] UDP 广播 255.255.255.255:' + this.targetPort + ' · HELLO', '#26A69A')
    this.appendLog('[Scan] 开始模拟设备发现...', '#26A69A')

    // 2. 模拟网络异步响应与设备解析
    const scanPeer = (idx: number): void => {
      // 递归终止条件
      if (idx >= this.peers.length) {
        this.isScanning = false
        this.appendLog('[Scan] 扫描完成,共 ' + this.peers.length + ' 台设备', '#5C6BC0')
        return
      }
      
      // 解析单个物理设备
      const p: PeerDevice = this.peers[idx]
      this.appendLog('[Scan] → ' + p.name + ' ' + p.ip + ':' + p.port + ' RSSI ' + p.rssi + 'dBm',
        p.available ? '#444' : '#BBB')
        
      // 模拟 650ms 的网络延迟与设备握手时间
      setTimeout(() => { scanPeer(idx + 1) }, 650)
    }
    
    // 延迟启动扫描任务
    setTimeout(() => { scanPeer(0) }, 400)
  }

UDP 广播原理深度透视:

  • 为什么设备发现用 UDP 而不是 TCP?
    TCP 是面向连接的,必须知道对方的精确 IP 才能发起握手。而在发现阶段,我们根本不知道周围有谁。UDP 是无连接的,允许向 255.255.255.255(全局广播地址)或特定的子网广播地址发送数据报(Datagram)。
  • 通信心跳包(HELLO):应用向局域网内疯狂大喊 HELLO(附带自己的设备名和端口)。周围同样运行了该 App 的设备,其底层正在监听该 UDP 端口。一旦收到广播,它们会向发送方的 IP 悄悄回复一个 ACKI_AM_HERE,从而完成了设备的相互发现。
  • 异步递归扫描 (scanPeer):代码中没有使用简单的 for 循环,而是使用了带有 setTimeout 的递归函数。这是对真实网络环境极度逼真的模拟——在真实空间中,由于物理距离和信号折射,设备响应广播的时间是有先后顺序和延迟的。

四、 建立可靠连接:TCP 三次握手的代码具象化

当用户在设备列表中选择了一台心仪的设备,我们需要将通信模式从不可靠的 UDP 切换到极其可靠的 TCP,建立一个持续的数据流通道。

  private connect(peer: PeerDevice): void {
    // 1. 客户端状态机拦截
    if (this.isConnected) {
      this.appendLog('[TCP] 已存在连接,请先断开', '#EF5350')
      return
    }
    if (!peer.available) {
      this.appendLog('[TCP] ' + peer.name + ' 设备离线,无法连接', '#EF5350')
      return
    }
    
    // 2. 发起三次握手请求
    this.appendLog('[TCP] connect(' + peer.ip + ':' + peer.port + ')...', '#FF9800')
    
    // 3. 模拟握手耗时与状态确立
    setTimeout(() => {
      this.isConnected = true
      this.targetName = peer.name
      this.targetIp = peer.ip
      this.targetPort = peer.port
      
      // UI 联动:连接成功后自动跳转到“消息会话”选项卡
      this.currentTabIndex = 1 
      
      this.appendLog('[TCP] ✓ 三次握手完成,已连接到 ' + peer.name, '#26A69A')
      this.chats = [] // 清空旧会话
      this.appendChat('系统', '已与 ' + peer.name + ' (' + peer.ip + ') 建立 TCP 连接,可发送文本数据', false)
    }, 900) // 900ms 模拟网络寻址与安全层校验延迟
  }

连接架构剖析:
在这个阶段,我们的应用正式成为了 TCP Client。在真实的鸿蒙 socket API 中,这将触发系统底层的 SYN -> SYN/ACK -> ACK 协议包交互。一旦 isConnected 被置为 true,意味着一条全双工(Full-Duplex)的虚拟管道已经建立,无论我们发送多么巨大的文件或复杂的 JSON 指令,TCP 底层都会为我们保证数据的顺序性与完整性。

表 1:跨端通信协议选型对照表

在鸿蒙分布式开发中,如何选择底层协议?

协议类型 ArkTS API 模块 传输特性与可靠性 典型分布式应用场景
UDP @ohos.net.socket (UDPSocket) 无连接、不保序、可能丢包、延迟极低。 设备发现(局域网广播扫描)、跨端音视频推流(允许轻微丢帧的实时画面同步)。
TCP @ohos.net.socket (TCPSocket) 面向连接、绝对保序、无丢包、有握手延迟。 指令同步(发送播放/暂停指令)、文件快传(图片/文档流转)、聊天与数据同步

五、 全双工通信与应用层 ACK 确认机制

连接建立后,我们迎来了跨端流转最激动人心的部分:数据的发送与接收。

  private send(): void {
    // 1. 发送前置校验
    if (!this.isConnected) { ... return }
    if (this.inputText.length === 0) { ... return }
    
    const payload: string = this.inputText
    
    // 2. UI 预渲染(乐观更新)
    this.appendChat('我', payload, true)
    this.appendLog('[TCP] send("' + payload + '") → ' + this.targetIp + ':' + this.targetPort + ' [len=' + payload.length + ']', '#5C6BC0')
    this.inputText = '' // 清空输入框

    const self: Index = this
    
    // 3. 模拟底层协议的 ACK 确认回调
    setTimeout(() => {
      self.appendChat(self.targetName, 'ACK · 已收到 "' + payload + '"', false)
      self.appendLog('[TCP] ← ACK 接收方已应答', '#26A69A')
    }, 700)

    // 4. 模拟远端设备的业务逻辑反弹 (Reply)
    setTimeout(() => {
      const demoReplies: string[] = [
        '收到,谢谢你!',
        '鸿蒙 ArkTS 真棒 👍',
        // ...
      ]
      const idx: number = Math.floor(Math.random() * demoReplies.length)
      self.appendChat(self.targetName, demoReplies[idx], false)
      self.appendLog('[TCP] ← ' + demoReplies[idx], '#888')
    }, 1600)
  }

通信机制的高级理解:

  • 应用层 ACK(Acknowledgment):这是极其关键的一步。TCP 本身自带底层 ACK 保证数据到达操作系统的缓冲区,但这不代表对方的 App 已经成功处理了数据。在高级的分布式通信(如鸿蒙的分布式软总线 RPC 调用)中,接收端 App 必须在解析完 Payload 后,主动回传一个业务级 ACK。这段代码利用 700ms 的延迟,完美重现了这一机制,让发送方确信指令已被消费。
  • 乐观更新策略(Optimistic UI):在发送数据的一瞬间,代码立刻调用了 this.appendChat('我', payload, true)。它没有等待网络响应就先渲染了 UI,极大地消除了用户的网络延迟感知,这是现代高性能 IM(即时通讯)软件的标准做法。

六、 UI 架构:数据驱动的微观排版与环形缓冲日志

除了底层网络逻辑,这份源码在 ArkUI 的组件排版与内存管理上也展现出了极高的素养。

1. 高性能的环形日志收集器 (Ring Buffer)
  private appendLog(text: string, color: string): void {
    // ... 格式化时间
    this.logSeq++
    // 将新日志插入数组头部
    this.logs.unshift({ id: this.logSeq, time: hh + ':' + mm + ':' + ss, text: text, color: color })
    
    // 【架构级内存保护】:防 OOM 截断
    if (this.logs.length > 60) { this.logs.pop() }
  }

网络通信往往伴随极其密集的状态变更(每秒可能打印数十条日志)。如果无限制地 unshift 数据,底层的响应式引擎会因为追踪庞大的数组而导致内存溢出(OOM)和严重的掉帧。代码中严格将数组长度维持在 60,旧数据自动被 pop 淘汰。配合 Scroll 容器,构成了一个极其稳定且高性能的滚动监控面板。

2. ChatBubble 动态气泡组件的流式布局

在渲染聊天对话时,如何优雅地区分“我发出的”和“对方发来的”消息?

  @Builder
  ChatBubble(chat: ChatItem) {
    Row() {
      // 对方发来的消息:靠左排列
      if (!chat.mine) {
        Column() {
          Text(chat.from).fontSize(10).fontColor('#888').width('100%')
          Text(chat.text)
            .backgroundColor('#F0F3F8')
            // ...
        }
        .layoutWeight(1)
        .margin({ right: 20 }) // 压迫右侧空间
      }
      
      // 自己发出的消息:靠右排列
      if (chat.mine) {
        Blank() // 【布局神技】:利用 Blank 自动挤压左侧所有的多余空间
        Column() {
          Text(chat.from).fontSize(10).fontColor('#FFFFFF99').width('100%')
          Text(chat.text)
            .backgroundColor('#5C6BC0')
            // ...
        }
        .layoutWeight(1)
        .margin({ left: 20 })
        .alignItems(HorizontalAlign.End) // 内部元素全部右对齐
      }
    }
    .width('100%').margin({ bottom: 10 })
  }

这段 @Builder 充分展现了声明式 UI 的灵活性。通过 if 语句,直接在底层 DOM 结构上分离了收发两端的排版。对于右侧(本机)的消息,巧妙地利用 Blank() 配合外层 Row,不使用任何绝对坐标或复杂的浮动逻辑,就实现了完美的信息靠右对齐,兼顾了多屏幕尺寸的自适应能力。


完整代码

import socket from '@ohos.net.socket';
import connection from '@ohos.net.connection';

interface PeerDevice { name: string; ip: string; port: number; rssi: number; available: boolean; }
interface LogItem { id: number; time: string; text: string; color: string; }
interface ChatItem { id: number; from: string; text: string; mine: boolean; time: string; }
interface TabItem { i: number; t: string; }



struct Index {
   myName: string = '本机-' + Math.floor(Math.random() * 900 + 100)
   myIp: string = '192.168.1.100'
   targetName: string = '未连接'
   targetIp: string = ''
   targetPort: number = 8848
   isScanning: boolean = false
   isConnected: boolean = false
   inputText: string = ''
   peers: PeerDevice[] = [
    { name: '真机 A', ip: '192.168.1.201', port: 8848, rssi: -42, available: true },
    { name: '模拟器 B', ip: '10.0.2.15', port: 8848, rssi: -68, available: true },
    { name: '鸿蒙平板 C', ip: '192.168.1.199', port: 8848, rssi: -75, available: true },
    { name: '智能手表 D', ip: '192.168.1.220', port: 8848, rssi: -81, available: false }
  ]
   logs: LogItem[] = []
   chats: ChatItem[] = []
   logSeq: number = 0
   chatSeq: number = 0
   currentTabIndex: number = 0

  private udpSocket: socket.UDPSocket | null = null
  private tcpSocket: socket.TCPSocket | null = null

  aboutToAppear(): void {
    this.appendLog('系统已就绪 · ' + this.myName, '#5C6BC0')
    this.appendLog('等待扫描 / 连接 / 发送指令...', '#888')
    this.initNetwork()
  }

  aboutToDisappear(): void {
    try {
      if (this.udpSocket) { this.udpSocket.close(); this.udpSocket = null }
      if (this.tcpSocket) { this.tcpSocket.close(); this.tcpSocket = null }
    } catch (e) {
      this.appendLog('资源清理异常', '#EF5350')
    }
  }

  private initNetwork(): void {
    try {
      const netCap: connection.NetCapabilities = {
        bearerTypes: [connection.NetBearType.BEARER_WIFI, connection.NetBearType.BEARER_CELLULAR],
        networkCap: [connection.NetCap.NET_CAPABILITY_INTERNET]
      }
      void netCap
      this.appendLog('[Network] 已申请 INTERNET / WIFI 能力', '#26A69A')
    } catch (e) {
      this.appendLog('[Network] 能力查询失败,使用模拟数据', '#EF5350')
    }
  }

  private appendLog(text: string, color: string): void {
    const d: Date = new Date()
    const hh: string = d.getHours() < 10 ? '0' + d.getHours() : d.getHours().toString()
    const mm: string = d.getMinutes() < 10 ? '0' + d.getMinutes() : d.getMinutes().toString()
    const ss: string = d.getSeconds() < 10 ? '0' + d.getSeconds() : d.getSeconds().toString()
    this.logSeq++
    this.logs.unshift({ id: this.logSeq, time: hh + ':' + mm + ':' + ss, text: text, color: color })
    if (this.logs.length > 60) { this.logs.pop() }
  }

  private appendChat(from: string, text: string, mine: boolean): void {
    const d: Date = new Date()
    const hh: string = d.getHours() < 10 ? '0' + d.getHours() : d.getHours().toString()
    const mm: string = d.getMinutes() < 10 ? '0' + d.getMinutes() : d.getMinutes().toString()
    this.chatSeq++
    this.chats.unshift({ id: this.chatSeq, from: from, text: text, mine: mine, time: hh + ':' + mm })
    if (this.chats.length > 50) { this.chats.pop() }
  }

  private startScan(): void {
    if (this.isScanning) { return }
    this.isScanning = true
    this.appendLog('[Scan] UDP 广播 255.255.255.255:' + this.targetPort + ' · HELLO', '#26A69A')
    this.appendLog('[Scan] 开始模拟设备发现...', '#26A69A')

    const scanPeer = (idx: number): void => {
      if (idx >= this.peers.length) {
        this.isScanning = false
        this.appendLog('[Scan] 扫描完成,共 ' + this.peers.length + ' 台设备', '#5C6BC0')
        return
      }
      const p: PeerDevice = this.peers[idx]
      this.appendLog('[Scan] → ' + p.name + ' ' + p.ip + ':' + p.port + ' RSSI ' + p.rssi + 'dBm',
        p.available ? '#444' : '#BBB')
      setTimeout(() => { scanPeer(idx + 1) }, 650)
    }
    setTimeout(() => { scanPeer(0) }, 400)
  }

  private connect(peer: PeerDevice): void {
    if (this.isConnected) {
      this.appendLog('[TCP] 已存在连接,请先断开', '#EF5350')
      return
    }
    if (!peer.available) {
      this.appendLog('[TCP] ' + peer.name + ' 设备离线,无法连接', '#EF5350')
      return
    }
    this.appendLog('[TCP] connect(' + peer.ip + ':' + peer.port + ')...', '#FF9800')
    setTimeout(() => {
      this.isConnected = true
      this.targetName = peer.name
      this.targetIp = peer.ip
      this.targetPort = peer.port
      this.currentTabIndex = 1
      this.appendLog('[TCP] ✓ 三次握手完成,已连接到 ' + peer.name, '#26A69A')
      this.chats = []
      this.appendChat('系统', '已与 ' + peer.name + ' (' + peer.ip + ') 建立 TCP 连接,可发送文本数据', false)
    }, 900)
  }

  private disconnect(): void {
    if (!this.isConnected) {
      this.appendLog('[TCP] 当前无连接', '#888')
      return
    }
    this.appendLog('[TCP] 关闭连接 → ' + this.targetName, '#EF5350')
    this.isConnected = false
    this.targetName = '未连接'
    this.targetIp = ''
    this.currentTabIndex = 0
  }

  private send(): void {
    if (!this.isConnected) {
      this.appendLog('[TCP] 请先连接一台设备', '#EF5350')
      return
    }
    if (this.inputText.length === 0) {
      this.appendLog('[TCP] 发送内容为空', '#EF5350')
      return
    }
    const payload: string = this.inputText
    this.appendChat('我', payload, true)
    this.appendLog('[TCP] send("' + payload + '") → ' + this.targetIp + ':' + this.targetPort + ' [len=' + payload.length + ']', '#5C6BC0')
    this.inputText = ''

    const self: Index = this
    setTimeout(() => {
      self.appendChat(self.targetName, 'ACK · 已收到 "' + payload + '"', false)
      self.appendLog('[TCP] ← ACK 接收方已应答', '#26A69A')
    }, 700)

    setTimeout(() => {
      const demoReplies: string[] = [
        '收到,谢谢你!',
        '鸿蒙 ArkTS 真棒 👍',
        '这是来自 ' + self.targetName + ' 的回复',
        '当前消息长度 ' + payload.length + ' 字节',
        '继续测试 · 继续发送 ✨'
      ]
      const idx: number = Math.floor(Math.random() * demoReplies.length)
      self.appendChat(self.targetName, demoReplies[idx], false)
      self.appendLog('[TCP] ← ' + demoReplies[idx], '#888')
    }, 1600)
  }

  
  DeviceCard(peer: PeerDevice) {
    Row() {
      Column() {
        Text(peer.available ? '●' : '○')
          .fontSize(12).fontColor(peer.available ? '#26A69A' : '#999')
      }
      .width(16).justifyContent(FlexAlign.Center)
      .margin({ right: 10 })

      Column() {
        Text(peer.name).fontSize(14).fontColor('#222').fontWeight(FontWeight.Bold).width('100%')
        Text(peer.ip + ':' + peer.port + ' · RSSI ' + peer.rssi + 'dBm')
          .fontSize(10).fontColor('#888').width('100%').margin({ top: 3 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)

      Column() {
        Text(peer.available ? '连接' : '离线')
          .fontSize(11).fontColor('#FFFFFF')
      }
      .width(56).height(30).borderRadius(15)
      .backgroundColor(peer.available ? '#5C6BC0' : '#CCCCCC')
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
      .onClick(() => this.connect(peer))
    }
    .width('100%').padding(12)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .margin({ bottom: 8 })
    .shadow({ radius: 8, color: '#1A237E15', offsetY: 2 })
  }

  
  LogCard(log: LogItem) {
    Row() {
      Text(log.time).fontSize(10).fontColor('#888').margin({ right: 10 })
      Text(log.text).fontSize(11).fontColor(log.color).layoutWeight(1)
    }
    .width('100%').padding({ left: 10, right: 10, top: 6, bottom: 6 })
  }

  
  ChatBubble(chat: ChatItem) {
    Row() {
      if (!chat.mine) {
        Column() {
          Text(chat.from).fontSize(10).fontColor('#888').width('100%')
          Text(chat.text).fontSize(13).fontColor('#222')
            .width('100%')
            .margin({ top: 4 })
            .padding(10)
            .backgroundColor('#F0F3F8')
            .borderRadius(12)
          Text(chat.time).fontSize(9).fontColor('#888').width('100%').margin({ top: 4 })
        }
        .layoutWeight(1)
        .margin({ right: 20 })
      }
      if (chat.mine) {
        Blank()
        Column() {
          Text(chat.from).fontSize(10).fontColor('#FFFFFF99').width('100%')
          Text(chat.text).fontSize(13).fontColor('#FFFFFF')
            .width('100%')
            .margin({ top: 4 })
            .padding(10)
            .backgroundColor('#5C6BC0')
            .borderRadius(12)
          Text(chat.time).fontSize(9).fontColor('#FFFFFF99').width('100%').margin({ top: 4 })
        }
        .layoutWeight(1)
        .margin({ left: 20 })
        .alignItems(HorizontalAlign.End)
      }
    }
    .width('100%').margin({ bottom: 10 })
  }

  build() {
    Column() {
      Row() {
        Column() {
          Text('跨设备通信').fontSize(18).fontColor('#111').fontWeight(FontWeight.Bold)
          Text(this.myName + ' · ' + this.myIp).fontSize(10).fontColor('#888').margin({ top: 3 })
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Start)

        Column() {
          Text(this.isConnected ? '已连接 · ' + this.targetName : '未连接')
            .fontSize(10).fontColor('#FFFFFF')
            .padding({ left: 10, right: 10, top: 6, bottom: 6 })
            .backgroundColor(this.isConnected ? '#26A69A' : '#999')
            .borderRadius(12)
        }
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })

      Row() {
        ForEach([
          { i: 0, t: '设备列表' },
          { i: 1, t: '消息会话' },
          { i: 2, t: '通信日志' }
        ], (item: TabItem, _idx: number) => {
          Column() {
            Text(item.t).fontSize(12)
              .fontColor(this.currentTabIndex === item.i ? '#FFFFFF' : '#5C6BC0')
          }
          .layoutWeight(1).height(36)
          .backgroundColor(this.currentTabIndex === item.i ? '#5C6BC0' : '#FFFFFF')
          .borderRadius(18)
          .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
          .margin({ right: item.i < 2 ? 8 : 0 })
          .onClick(() => { this.currentTabIndex = item.i })
        })
      }
      .width('100%').padding({ left: 16, right: 16, bottom: 8 })

      if (this.currentTabIndex === 0) {
        Column() {
          Row() {
            Column() { Text('📡 扫描设备').fontSize(11).fontColor('#FFFFFF') }
              .layoutWeight(1).height(38)
              .backgroundColor(this.isScanning ? '#999' : '#FF9800')
              .borderRadius(19)
              .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
              .onClick(() => this.startScan())
              .margin({ right: 10 })

            Column() { Text(this.isConnected ? '✕ 断开连接' : '暂未连接').fontSize(11).fontColor('#FFFFFF') }
              .layoutWeight(1).height(38)
              .backgroundColor(this.isConnected ? '#EF5350' : '#CCC')
              .borderRadius(19)
              .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
              .onClick(() => this.disconnect())
          }
          .width('100%').margin({ bottom: 10 })

          Scroll() {
            Column() {
              ForEach(this.peers, (p: PeerDevice, _k: number) => { this.DeviceCard(p) })
            }
            .width('100%')
          }
          .layoutWeight(1).scrollBar(BarState.Off)

          Column() {
            Text('架构:UDP 广播发现 → TCP 发送文本数据 → ACK 回复').fontSize(10).fontColor('#888')
          }
          .width('100%').padding(10).backgroundColor('#F0F3F8').borderRadius(10).margin({ top: 10 })
        }
        .layoutWeight(1).width('100%').padding({ left: 16, right: 16, bottom: 10 })
      }

      if (this.currentTabIndex === 1) {
        Column() {
          if (!this.isConnected) {
            Column() {
              Text('尚未连接任何设备').fontSize(13).fontColor('#666')
              Text('请先在「设备列表」中点击一台在线设备').fontSize(10).fontColor('#888').margin({ top: 6 })
            }
            .width('100%').height(220)
            .backgroundColor('#FFFFFF').borderRadius(12)
            .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
            .margin({ bottom: 10 })
          } else {
            Scroll() {
              Column() {
                ForEach(this.chats, (c: ChatItem, _k: number) => { this.ChatBubble(c) })
              }
              .width('100%')
            }
            .layoutWeight(1).scrollBar(BarState.Off)

            Row() {
              TextInput({ placeholder: '输入要发送的文本...', text: this.inputText })
                .onChange((val: string) => { this.inputText = val })
                .layoutWeight(1).height(38).backgroundColor('#FFFFFF').borderRadius(19)
                .padding({ left: 14, right: 14 }).fontSize(12)

              Column() {
                Text('发送').fontSize(11).fontColor('#FFFFFF')
              }
              .width(64).height(38).margin({ left: 10 })
              .backgroundColor('#5C6BC0').borderRadius(19)
              .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
              .onClick(() => this.send())
            }
            .width('100%').margin({ top: 10 })
          }
        }
        .layoutWeight(1).width('100%').padding({ left: 16, right: 16, bottom: 10 })
      }

      if (this.currentTabIndex === 2) {
        Column() {
          Scroll() {
            Column() {
              ForEach(this.logs, (l: LogItem, _k: number) => { this.LogCard(l) })
            }
            .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
          }
          .layoutWeight(1).scrollBar(BarState.Off)
        }
        .layoutWeight(1).width('100%').padding({ left: 16, right: 16, bottom: 10 })
      }
    }
    .width('100%').height('100%').backgroundColor('#F5F7FA')
  }
}

运行界面
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

七、 从 Demo 走向真机实战:生产环境的最佳实践

本文通过精密的定时器与状态机,在单机环境中完美复现了跨端通信的完整生命周期。但如果您准备将这段逻辑真正接入 @ohos.net.socket 投入商业生产环境,您必须对架构进行以下升维:

  1. 子线程解析(TaskPool / Worker)
    Socket 的 .on('message') 回调会收到大量的 ArrayBuffer 数据流。在 UI 主线程直接对这些二进制流进行解析、拼包、解包(处理 TCP 的粘包和半包问题),会导致主线程阻塞,界面滑动卡顿。必须将数据协议的解析工作下沉到鸿蒙的 TaskPool 或独立的 Worker 线程中处理。
  2. 心跳保活机制(Keep-Alive Heartbeat)
    局域网与近场通信的环境极其恶劣(微波炉干扰、Wi-Fi 漫游断流)。TCP 连接虽然可靠,但在极端断网情况下,系统底层可能需要数十分钟才能感知连接已断开(TCP Keepalive 超时机制)。应用层必须每隔 3-5 秒互相发送一个极其轻量的 PING/PONG 心跳包。如果连续三次未收到心跳,应用层必须主动调用 disconnect() 并将界面状态置为离线。
  3. 拥抱鸿蒙分布式软总线(Distributed SoftBus)
    如果您开发的是纯纯的鸿蒙原生应用阵列(不需要与 Android/Windows 互通),强烈建议放弃手写 Socket,转而使用系统级别的 @ohos.distributedHardware.deviceManager 和分布式数据对象。底层星闪与软总线已经帮您屏蔽了发现、建连、甚至 IP 寻址的所有细节,您只需要像调用本地方法一样,就能实现跨设备的丝滑流转。

总而言之,无论上层的 API 如何封装变迁,掌握 UDP 广播寻址、TCP 握手保序、状态机同步这些网络底层的硬核内功,是每一位资深开发者驾驭“万物互联”星辰大海的关键所在。希望这套精密的跨端通信演练架构,能成为您探索 HarmonyOS 全场景流转技术的绝佳跳板。

Logo

一站式 AI 云服务平台

更多推荐