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

发布时间:2026/7/17 1:19:48
《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}fromkit.ConnectivityKit;letnearlinkManager:NearLinkManager|undefined;functioninitNearLink():void{try{nearlinkManagerNearLinkManager.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}fromkit.ConnectivityKit;EntryComponentstruct ServerPage{privateserverSocket:NearLinkSocket|undefined;privateclientList:NearLinkSocket[][];aboutToAppear():void{initNearLink();this.startServer();}startServer():void{// 1. 创建 NearLinkSocketthis.serverSocketnearlinkManager!.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);constindexthis.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{// 将二进制数据转为字符串处理constdecodernewutil.TextDecoder(utf-8);constmessagedecoder.decodeToString(data);console.info(Received message: message);}}注意服务端有两个data监听。一个是serverSocket.on(data)这个回调在服务端自己没有建立连接前是不生效的。另一个是client.on(data)它针对每个已连接的客户端实例。实际开发中不要在服务端serverSocket上监听data客户端的数据是通过client对象接收的。3. 客户端连接服务端并发送数据客户端同样需要创建一个NearLinkSocket然后调用connect方法。import{NearLinkManager,NearLinkSocket,BusinessError}fromkit.ConnectivityKit;EntryComponentstruct ClientPage{privateclientSocket:NearLinkSocket|undefined;StateprivateconnectionStatus:stringDisconnected;aboutToAppear():void{initNearLink();}connectToServer():void{// 1. 创建客户端 NearLinkSocketthis.clientSocketnearlinkManager!.createSocket(nearlink,client);if(!this.clientSocket){console.error(createSocket failed);return;}// 2. 监听连接状态变化this.clientSocket.on(connect,(){console.info(Connected to server);this.connectionStatusConnected;});this.clientSocket.on(disconnect,(){console.info(Disconnected from server);this.connectionStatusDisconnected;});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;}constencodernewutil.TextEncoder();constdataencoder.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_SIZE1000;// 留一点余量consttotalLengthdata.length;// 先发送总长度4 字节constlengthBuffernewUint8Array(4);lengthBuffer[0](totalLength24)0xFF;lengthBuffer[1](totalLength16)0xFF;lengthBuffer[2](totalLength8)0xFF;lengthBuffer[3]totalLength0xFF;socket.write(lengthBuffer);// 分包发送实际数据letoffset0;constsendNextPacket(){constremainingtotalLength-offset;if(remaining0){console.info(All data sent, waiting for ACK...);return;}constpacketSizeMath.min(MAX_PACKET_SIZE,remaining);constpacketdata.slice(offset,offsetpacketSize);socket.write(packet);offsetpacketSize;// 递归发送下一包sendNextPacket();};sendNextPacket();}接收端需要解析头部长度然后根据长度拼接完整数据。letreceivedDataBuffer:Uint8ArraynewUint8Array(0);letexpectedLength:number-1;socket.on(data,(data:Uint8Array){if(expectedLength-1){// 第一个包是长度信息if(data.length4){console.warn(Invalid length prefix);return;}expectedLength(data[0]24)|(data[1]16)|(data[2]8)|data[3];// 剩余数据可能是第一个数据包的一部分constremainingdata.slice(4);if(remaining.length0){receivedDataBufferremaining;}}else{// 拼接数据包consttempnewUint8Array(receivedDataBuffer.lengthdata.length);temp.set(receivedDataBuffer);temp.set(data,receivedDataBuffer.length);receivedDataBuffertemp;}if(expectedLength0receivedDataBuffer.lengthexpectedLength){// 数据接收完毕console.info(Full data received, length: receivedDataBuffer.length);// 发送确认constacknewUint8Array([0x00,0x01,0x00,0x01]);// 自定义确认码socket.write(ack);// 重置状态准备接收下一条数据receivedDataBuffernewUint8Array(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.clientSocketundefined;}}最佳实践不要在 build() 中创建 NearLinkSocket。每个NearLinkSocket实例都对应一个系统资源频繁创建和销毁会导致内存泄漏。建议在aboutToAppear中初始化在aboutToDisappear中释放。用 State 管理连接状态而不是直接依赖 on(‘connect’) 回调修改 UI。on(connect)回调在子线程执行直接修改 UI 状态可能导致状态不同步。通过State变量驱动 UI能保证页面渲染一致性。分包发送时严格控制每个包的大小。虽然 MTU 上限是 1024但实际传输中容易受信号干扰建议包大小控制在 900-1000 字节。如果包太大可能导致底层重传增加延迟。Demo 入口EntryComponentstruct Index{StateprivatecurrentPage:stringmain;build(){Column(){if(this.currentPagemain){Button(启动服务端).onClick(()this.currentPageserver);Button(连接客户端).onClick(()this.currentPageclient);}elseif(this.currentPageserver){ServerPage()}elseif(this.currentPageclient){ClientPage()}}}}FAQQ为什么真机可以正常连接但模拟器上createSocket返回 undefinedANearLink Kit 依赖底层硬件支持模拟器没有蓝牙硬件所以createSocket会返回undefined。调试阶段建议直接用两台真机。Q为什么write方法调用后对端没有收到数据A检查两点第一确认对端是否已经注册了on(data)监听第二确认 socket 状态是Connected。如果 socket 正在连接阶段调用write数据会被丢弃。Q如何判断传输是否加密安全ANearLink 在createSocket时可以选择是否启用安全连接。如果需要加密在createSocket的第二个参数中传入secured即可。默认是不加密的。this.clientSocketnearlinkManager!.createSocket(nearlink,client,secured);