《HarmonyOS技术精讲-NearLink Kit(星闪服务)》第3篇:数据收发——点对点数据传输实战

开头:一个容易被忽视的连接状态问题

很多人第一次尝试用 NearLink Kit 做数据收发时,会发现自己陷入一个尴尬的局面:官方 demo 能跑通,但稍微改一下场景,比如页面切换后重新连接,或者同时处理多条连接,代码就开始频繁崩溃。这其实不是 API 设计的问题,而是开发者在理解连接生命周期和状态同步时容易忽视几个关键细节。

这篇文章会从一个实战的角度,完整演示如何用 NearLink Kit 建立一个安全、稳定的点对点连接,并完成字符串和二进制数据的双向收发。代码会覆盖连接创建、数据监听、分包发送、接收确认这几个核心环节,同时会重点讲清楚状态管理和异常处理的问题。

它解决什么问题

NearLink(星闪服务) 是 HarmonyOS 提供的一种近距无线通信能力,定位在蓝牙和 Wi-Fi Direct 之间。相比传统蓝牙,它的核心优势是低延迟高吞吐;相比 Wi-Fi Direct,它省掉了复杂的 IP 层配置,建立连接更直接。

适用场景:

  • 两个设备之间需要毫秒级的数据传输,比如游戏手柄、遥控器
  • 传输数据量不大(几 KB 到几 MB),但要求实时性高
  • 需要安全连接(加密传输)

不适合的场景:

  • 大文件传输(几百 MB 以上),这时候 Wi-Fi Direct 更合适
  • 需要广播或多播的场景(NearLink 当前只支持点对点)
  • 设备不在近距范围内(超过 100 米就会断链)

和传统蓝牙对比

维度 传统蓝牙 SPP NearLink
连接速度 2-5 秒 1 秒以内
最大传输速率 约 2 Mbps 约 30 Mbps
延迟 100-200ms 10-20ms
安全连接 需要额外实现 内建
状态回调 分散在多处 集中在一个回调

从实际项目经验看,如果你需要在两个设备间传输小数据包、高频率 的场景,NearLink 是目前 HarmonyOS 上最稳定的方案。

环境说明

text 复制代码
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:两台 HarmonyOS NEXT 手机(或手机+平板)

核心实现:双向数据传输

整个流程分为两个部分:客户端(Client)服务端(Server) 。服务端负责监听连接请求,客户端主动发起连接。连接建立后,双方都可以通过 write 发送数据,通过 on('data') 接收数据。

无论客户端还是服务端,第一步都是获取 NearLinkManager 实例。

typescript 复制代码
import { NearLinkManager } from '@kit.ConnectivityKit';

let nearlinkManager: NearLinkManager | undefined;

function initNearLink(): void {
  try {
    nearlinkManager = NearLinkManager.getInstance();
    console.info('NearLinkManager initialized');
  } catch (err) {
    console.error('Failed to get NearLinkManager: ' + JSON.stringify(err));
  }
}

这里有一个关键点:NearLinkManager单例 ,全局只需要初始化一次。不要在 aboutToAppear 或者页面构建时反复调用 getInstance()

2. 服务端:创建 NearLinkSocket 并监听连接

服务端需要创建一个 NearLinkSocket,然后调用 bindlisten 来等待客户端连接。

typescript 复制代码
import { NearLinkManager, NearLinkSocket, BusinessError } from '@kit.ConnectivityKit';

@Entry
@Component
struct ServerPage {
  private serverSocket: NearLinkSocket | undefined;
  private clientList: NearLinkSocket[] = [];

  aboutToAppear(): void {
    initNearLink();
    this.startServer();
  }

  startServer(): void {
    // 1. 创建 NearLinkSocket
    this.serverSocket = nearlinkManager!.createSocket('nearlink', 'server');
    if (!this.serverSocket) {
      console.error('createSocket failed');
      return;
    }

    // 2. 设置数据接收监听
    this.serverSocket.on('data', (data: Uint8Array) => {
      console.info('Server received data length: ' + data.length);
      // 处理接收到的数据
      this.handleReceivedData(data);
    });

    // 3. 设置新连接监听
    this.serverSocket.on('connect', (client: NearLinkSocket) => {
      console.info('New client connected');
      this.clientList.push(client);
      // 为新连接设置 data 监听
      client.on('data', (data: Uint8Array) => {
        console.info('Server received from client: ' + data.length);
      });
    });

    // 4. 设置连接断开监听
    this.serverSocket.on('disconnect', (socket: NearLinkSocket) => {
      console.info('Client disconnected');
      const index = this.clientList.indexOf(socket);
      if (index > -1) {
        this.clientList.splice(index, 1);
      }
    });

    // 5. 绑定端口并监听
    this.serverSocket.bind({ port: 12345 });
    this.serverSocket.listen({
      maxConnections: 5,  // 最多支持 5 个并发连接
    });
  }

  handleReceivedData(data: Uint8Array): void {
    // 将二进制数据转为字符串处理
    const decoder = new util.TextDecoder('utf-8');
    const message = decoder.decodeToString(data);
    console.info('Received message: ' + message);
  }
}

注意 :服务端有两个 data 监听。一个是 serverSocket.on('data'),这个回调在服务端自己没有建立连接前 是不生效的。另一个是 client.on('data'),它针对每个已连接的客户端实例。实际开发中,不要在服务端 serverSocket 上监听 data,客户端的数据是通过 client 对象接收的。

3. 客户端:连接服务端并发送数据

客户端同样需要创建一个 NearLinkSocket,然后调用 connect 方法。

typescript 复制代码
import { NearLinkManager, NearLinkSocket, BusinessError } from '@kit.ConnectivityKit';

@Entry
@Component
struct ClientPage {
  private clientSocket: NearLinkSocket | undefined;
  @State private connectionStatus: string = 'Disconnected';

  aboutToAppear(): void {
    initNearLink();
  }

  connectToServer(): void {
    // 1. 创建客户端 NearLinkSocket
    this.clientSocket = nearlinkManager!.createSocket('nearlink', 'client');
    if (!this.clientSocket) {
      console.error('createSocket failed');
      return;
    }

    // 2. 监听连接状态变化
    this.clientSocket.on('connect', () => {
      console.info('Connected to server');
      this.connectionStatus = 'Connected';
    });

    this.clientSocket.on('disconnect', () => {
      console.info('Disconnected from server');
      this.connectionStatus = 'Disconnected';
    });

    this.clientSocket.on('data', (data: Uint8Array) => {
      console.info('Client received data: ' + data);
      // 这里可以处理服务器下发的数据
    });

    // 3. 发起连接
    this.clientSocket.connect({
      address: '12:34:56:78:9A:BC',  // 服务端设备的蓝牙地址
      port: 12345,
    });
  }

  sendStringMessage(message: string): void {
    if (!this.clientSocket || this.connectionStatus !== 'Connected') {
      console.warn('Socket not connected');
      return;
    }

    const encoder = new util.TextEncoder();
    const data = encoder.encodeInto(message);
    this.clientSocket.write(data);
  }

  sendBinaryData(data: Uint8Array): void {
    if (!this.clientSocket || this.connectionStatus !== 'Connected') {
      console.warn('Socket not connected');
      return;
    }
    // 发送二进制数据,比如传感器数据
    this.clientSocket.write(data);
  }
}

这里有一个设计取舍:为什么用 connectionStatus 状态变量?

因为 write 方法需要确保 socket 处于已连接状态。如果直接写 this.clientSocket.write(),而 socket 还没成功连接,会抛出异常。通过 @State 管理这个状态,能确保 UI 和业务逻辑的一致性。

4. 分包发送与接收确认

NearLink 底层有最大传输单元(MTU)限制,大约在 1024 字节左右。如果需要发送超过 MTU 的数据,需要手动分包。

分包的核心思路:

  1. 在数据头部添加长度字段
  2. 对端根据长度字段拼接完整数据
  3. 发送确认包(ACK)
typescript 复制代码
// 分包发送大数据的辅助函数
function sendLargeData(socket: NearLinkSocket, data: Uint8Array): void {
  const MAX_PACKET_SIZE = 1000;  // 留一点余量
  const totalLength = data.length;
  
  // 先发送总长度(4 字节)
  const lengthBuffer = new Uint8Array(4);
  lengthBuffer[0] = (totalLength >> 24) & 0xFF;
  lengthBuffer[1] = (totalLength >> 16) & 0xFF;
  lengthBuffer[2] = (totalLength >> 8) & 0xFF;
  lengthBuffer[3] = totalLength & 0xFF;
  socket.write(lengthBuffer);

  // 分包发送实际数据
  let offset = 0;
  const sendNextPacket = () => {
    const remaining = totalLength - offset;
    if (remaining <= 0) {
      console.info('All data sent, waiting for ACK...');
      return;
    }
    const packetSize = Math.min(MAX_PACKET_SIZE, remaining);
    const packet = data.slice(offset, offset + packetSize);
    socket.write(packet);
    offset += packetSize;
    // 递归发送下一包
    sendNextPacket();
  };

  sendNextPacket();
}

接收端需要解析头部长度,然后根据长度拼接完整数据。

typescript 复制代码
let receivedDataBuffer: Uint8Array = new Uint8Array(0);
let expectedLength: number = -1;

socket.on('data', (data: Uint8Array) => {
  if (expectedLength === -1) {
    // 第一个包是长度信息
    if (data.length < 4) {
      console.warn('Invalid length prefix');
      return;
    }
    expectedLength = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3];
    // 剩余数据可能是第一个数据包的一部分
    const remaining = data.slice(4);
    if (remaining.length > 0) {
      receivedDataBuffer = remaining;
    }
  } else {
    // 拼接数据包
    const temp = new Uint8Array(receivedDataBuffer.length + data.length);
    temp.set(receivedDataBuffer);
    temp.set(data, receivedDataBuffer.length);
    receivedDataBuffer = temp;
  }

  if (expectedLength > 0 && receivedDataBuffer.length >= expectedLength) {
    // 数据接收完毕
    console.info('Full data received, length: ' + receivedDataBuffer.length);
    // 发送确认
    const ack = new Uint8Array([0x00, 0x01, 0x00, 0x01]); // 自定义确认码
    socket.write(ack);
    
    // 重置状态,准备接收下一条数据
    receivedDataBuffer = new Uint8Array(0);
    expectedLength = -1;
  }
});

常见问题 1:数据接收不完整或乱序

现象:发送一段字符串,接收端收到的数据缺斤少两,或者顺序错乱。

原因 :NearLink 底层传输不保证 TCP 那样的有序性。如果同时发送多个数据包,或者数据包被底层分片,接收端收到的 data 事件可能是一个包,也可能是多个包拼接的,还有可能是数据被拆成了多段。

解决方案 :严格按照"数据头+数据体"的协议格式处理。先接收 4 字节的长度字段,然后接收对应长度的数据,不要依赖单次 data 回调能拿到完整数据。代码参考上面的分包接收实现。

常见问题 2:页面销毁后,socket 回调仍然执行

现象 :用户返回上一页后,控制台继续打印 datadisconnect 日志,甚至导致崩溃。

原因aboutToDisappear 中没有主动释放 socket 资源。on('data') 的回调是异步注册的,不会因为页面销毁而自动解除。

解决方案 :在 aboutToDisappear 中显式调用 socket.close(),并清除所有监听。

typescript 复制代码
aboutToDisappear(): void {
  if (this.clientSocket) {
    // 移除所有监听
    this.clientSocket.off('data');
    this.clientSocket.off('connect');
    this.clientSocket.off('disconnect');
    // 关闭 socket
    this.clientSocket.close();
    this.clientSocket = undefined;
  }
}

最佳实践

  1. 不要在 build() 中创建 NearLinkSocket 。每个 NearLinkSocket 实例都对应一个系统资源,频繁创建和销毁会导致内存泄漏。建议在 aboutToAppear 中初始化,在 aboutToDisappear 中释放。

  2. 用 @State 管理连接状态,而不是直接依赖 on('connect') 回调修改 UIon('connect') 回调在子线程执行,直接修改 UI 状态可能导致状态不同步。通过 @State 变量驱动 UI,能保证页面渲染一致性。

  3. 分包发送时,严格控制每个包的大小。虽然 MTU 上限是 1024,但实际传输中容易受信号干扰,建议包大小控制在 900-1000 字节。如果包太大,可能导致底层重传,增加延迟。

Demo 入口

typescript 复制代码
@Entry
@Component
struct Index {
  @State private currentPage: string = 'main';

  build() {
    Column() {
      if (this.currentPage === 'main') {
        Button('启动服务端')
          .onClick(() => this.currentPage = 'server');
        Button('连接客户端')
          .onClick(() => this.currentPage = 'client');
      } else if (this.currentPage === 'server') {
        ServerPage()
      } else if (this.currentPage === 'client') {
        ClientPage()
      }
    }
  }
}

FAQ

Q:为什么真机可以正常连接,但模拟器上 createSocket 返回 undefined?

A:NearLink Kit 依赖底层硬件支持,模拟器没有蓝牙硬件,所以 createSocket 会返回 undefined。调试阶段建议直接用两台真机。

Q:为什么 write 方法调用后,对端没有收到数据?

A:检查两点:第一,确认对端是否已经注册了 on('data') 监听;第二,确认 socket 状态是 Connected。如果 socket 正在连接阶段调用 write,数据会被丢弃。

Q:如何判断传输是否加密安全?

A:NearLink 在 createSocket 时可以选择是否启用安全连接。如果需要加密,在 createSocket 的第二个参数中传入 'secured' 即可。默认是不加密的。

typescript 复制代码
this.clientSocket = nearlinkManager!.createSocket('nearlink', 'client', 'secured');
相关推荐
春卷同学1 小时前
HarmonyOS掌上记账APP开发实践第25篇:Image 组件高级用法 — 资源引用、网络图片与首字符占位方案
网络·华为·harmonyos
2501_919749031 小时前
华为鸿蒙免费记账与统计APP—小羊统计
华为·harmonyos
AD02272 小时前
32-Token明文存储-把凭据边界收到安全仓储里
harmonyos·arkts·鸿蒙开发
特立独行的猫a3 小时前
Node.js三方库鸿蒙 PC 移植:鸿蒙PC开发者面对面 · 线下课件分享
华为·harmonyos·三方库移植·鸿蒙pc
春卷同学4 小时前
HarmonyOS掌上记账APP开发实践第23篇:弹性动效与手势 — EdgeEffect.Spring 与手势系统实战
spring·华为·harmonyos
l134062082354 小时前
鸿蒙应用开发实战【88】— DevEco Studio高效开发技巧
华为·harmonyos·鸿蒙系统
一缕清烟在人间5 小时前
鸿蒙应用开发实战【54】— 应用详情页AppDetailPage开发(下)
华为·harmonyos·鸿蒙系统
l134062082356 小时前
鸿蒙应用开发实战【85】— 应用签名与正式HAP打包
华为·harmonyos·鸿蒙系统
l134062082356 小时前
鸿蒙应用开发实战【87】— 常见崩溃排查null指针与类型断言
华为·harmonyos·鸿蒙系统