news 2026/7/17 1:19:44

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
《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 米就会断链)

和传统蓝牙对比

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

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

环境说明

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

核心实现:双向数据传输

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

1. 初始化 NearLink Manager

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

import{NearLinkManager}from'@kit.ConnectivityKit';letnearlinkManager:NearLinkManager|undefined;functioninitNearLink():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来等待客户端连接。

import{NearLinkManager,NearLinkSocket,BusinessError}from'@kit.ConnectivityKit';@Entry@Componentstruct ServerPage{privateserverSocket:NearLinkSocket|undefined;privateclientList:NearLinkSocket[]=[];aboutToAppear():void{initNearLink();this.startServer();}startServer():void{// 1. 创建 NearLinkSocketthis.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');constindex=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{// 将二进制数据转为字符串处理constdecoder=newutil.TextDecoder('utf-8');constmessage=decoder.decodeToString(data);console.info('Received message: '+message);}}

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

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

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

import{NearLinkManager,NearLinkSocket,BusinessError}from'@kit.ConnectivityKit';@Entry@Componentstruct ClientPage{privateclientSocket:NearLinkSocket|undefined;@StateprivateconnectionStatus:string='Disconnected';aboutToAppear():void{initNearLink();}connectToServer():void{// 1. 创建客户端 NearLinkSocketthis.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;}constencoder=newutil.TextEncoder();constdata=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)
// 分包发送大数据的辅助函数functionsendLargeData(socket:NearLinkSocket,data:Uint8Array):void{constMAX_PACKET_SIZE=1000;// 留一点余量consttotalLength=data.length;// 先发送总长度(4 字节)constlengthBuffer=newUint8Array(4);lengthBuffer[0]=(totalLength>>24)&0xFF;lengthBuffer[1]=(totalLength>>16)&0xFF;lengthBuffer[2]=(totalLength>>8)&0xFF;lengthBuffer[3]=totalLength&0xFF;socket.write(lengthBuffer);// 分包发送实际数据letoffset=0;constsendNextPacket=()=>{constremaining=totalLength-offset;if(remaining<=0){console.info('All data sent, waiting for ACK...');return;}constpacketSize=Math.min(MAX_PACKET_SIZE,remaining);constpacket=data.slice(offset,offset+packetSize);socket.write(packet);offset+=packetSize;// 递归发送下一包sendNextPacket();};sendNextPacket();}

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

letreceivedDataBuffer:Uint8Array=newUint8Array(0);letexpectedLength: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];// 剩余数据可能是第一个数据包的一部分constremaining=data.slice(4);if(remaining.length>0){receivedDataBuffer=remaining;}}else{// 拼接数据包consttemp=newUint8Array(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);// 发送确认constack=newUint8Array([0x00,0x01,0x00,0x01]);// 自定义确认码socket.write(ack);// 重置状态,准备接收下一条数据receivedDataBuffer=newUint8Array(0);expectedLength=-1;}});

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

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

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

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

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

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

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

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

aboutToDisappear():void{if(this.clientSocket){// 移除所有监听this.clientSocket.off('data');this.clientSocket.off('connect');this.clientSocket.off('disconnect');// 关闭 socketthis.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 入口

@Entry@Componentstruct Index{@StateprivatecurrentPage:string='main';build(){Column(){if(this.currentPage==='main'){Button('启动服务端').onClick(()=>this.currentPage='server');Button('连接客户端').onClick(()=>this.currentPage='client');}elseif(this.currentPage==='server'){ServerPage()}elseif(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'即可。默认是不加密的。

this.clientSocket=nearlinkManager!.createSocket('nearlink','client','secured');
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/17 1:19:05

KEGG通路可视化:从基础到高级定制技巧

1. KEGG通路可视化基础与需求场景在生物信息学分析中&#xff0c;KEGG通路富集分析是解读基因功能的重要方法。常规分析流程通常依赖现成的工具生成标准图表&#xff0c;但实际科研中我们经常遇到这些痛点&#xff1a;发表级图表需要个性化调整&#xff08;如突出关键通路、合并…

作者头像 李华
网站建设 2026/7/17 1:18:53

YOLOv1网络结构与目标检测原理解析

1. YOLOv1网络结构全景解析YOLOv1作为目标检测领域的里程碑式工作&#xff0c;其网络结构设计理念至今仍影响着计算机视觉的发展方向。当我第一次拆解这个网络时&#xff0c;最让我震撼的是它用单次前向传播就完成了传统方法需要多阶段处理的任务。整个网络可以看作是一个精心设…

作者头像 李华
网站建设 2026/7/17 1:17:36

Windows 11与Ubuntu 22.04双系统安装全指南

1. 双系统安装前的关键准备工作在开始安装Windows 11和Ubuntu 22.04双系统之前&#xff0c;有几个关键步骤必须完成。这些准备工作看似简单&#xff0c;但往往决定了整个安装过程的成败。我曾在多次安装过程中因为忽略这些细节而不得不重头再来&#xff0c;浪费了大量时间。1.1…

作者头像 李华
网站建设 2026/7/17 1:17:20

坚持打造最好的日期控件,My97 DatePicker 4.7 Release

4.7更新的内容 [新增]输入智能调整,在输入日期时,会根据日期格式自动调整[新增]默认显示快速选择的选项[新增]周数算法选择[新增]全键盘操作(键盘控制开关),不用鼠标也可以选择日期[新增]dateFmt可以直接使用%y %M %d %ld等内置常量[新增]isShowOK属性,可以隐藏确定按钮[新增]…

作者头像 李华
网站建设 2026/7/17 1:15:35

华为OD机试 新系统真题 【毕业旅行】多语言题解

毕业旅行(Java /Py/C++/Go/C/Js)题解 华为OD机试新系统真题 华为OD上机考试新系统真题 7月15号 200分题型 华为OD机试新系统真题目录点击查看: 华为OD机试新系统真题题库目录|机考题库 + 算法考点详解 题目内容 四年校园时光即将结束,小明和室友计划一次难忘的毕业旅行。他…

作者头像 李华
网站建设 2026/7/17 1:15:26

眼镜清洗维修门店如何提升附近到店频次?餐宝盈小程序结合GEO服务获客,含零代码SAAS、AI编程、源码定制

眼镜清洗维修门店小程序制作工具汇总表 工具更适合谁价格开发方式核心特点餐宝盈适合所有行业的商家&#xff0c;尤其是拥有自己实体门店的商家&#xff0c;如餐饮、茶饮、烘焙、便利店、生鲜、社区零售门店、教培门店&#xff0c;尤其适合先把点单、预约、会员、发券和复购做…

作者头像 李华