《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 上最稳定的方案。
环境说明
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,然后调用bind和listen来等待客户端连接。
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 的数据,需要手动分包。
分包的核心思路:
- 在数据头部添加长度字段
- 对端根据长度字段拼接完整数据
- 发送确认包(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 回调仍然执行
现象:用户返回上一页后,控制台继续打印data或disconnect日志,甚至导致崩溃。
原因: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;}}最佳实践
不要在 build() 中创建 NearLinkSocket。每个
NearLinkSocket实例都对应一个系统资源,频繁创建和销毁会导致内存泄漏。建议在aboutToAppear中初始化,在aboutToDisappear中释放。用 @State 管理连接状态,而不是直接依赖 on(‘connect’) 回调修改 UI。
on('connect')回调在子线程执行,直接修改 UI 状态可能导致状态不同步。通过@State变量驱动 UI,能保证页面渲染一致性。分包发送时,严格控制每个包的大小。虽然 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');