完整源码:NearLinkFileTransfer
本文涵盖了权限申请、设备发现、连接、自定义协议、分块传输、确认/取消、MTU 优化、进度反馈、完整性校验,附完整可运行代码与踩坑总结。
一、为什么要自己写一个星闪文件传输工具?
华为星闪(NearLink)技术低延迟、高吞吐、抗干扰,传输速度远超传统蓝牙。以前做过蓝牙模块开发,流程相似,但星闪的 API 设计、UUID 规范、MTU 限制等细节有诸多不同。本以为很简单就能写完,没想到各种问题让我直接把星闪代码放了一周没碰。又花了一天时间解决所有问题并完成测试,才开始写这篇帖子。
完整的大文件传输整体内容量太大,我将分两篇写:本篇聚焦星闪通信部分(设备发现、连接、自定义协议、分块传输、确认/取消、MTU 优化)下一篇专门讲文件数据处理细节。
二、整体工作流程
整个传输过程分为发送端(A) (负责开启广播、选择文件并发送)和接收端(B)(负责扫描设备、连接并接收文件)。
发送端 接收端
│ │
├─ 开启广播(携带服务标识) │
│ ├─ 启动扫描(用 manufacturerId 过滤)
│ ├─ 发现设备,发起连接
├─ 接收端连接 ◄──────────────────────┤
├─ 发送文件元数据(名称/大小)────────►│
│ ├─ 弹窗询问用户
│ ◄──────── 同意(0xEE)/拒绝(0xEF) ──┤
├─ 若同意,分块传输数据(2KB/块)────►│
│ ├─ 重组数据块(Map 按序号)
└─ 发送完成标记(0xF3) ─────────────►└─ 完整性校验 → 保存文件
| 接收端 | 发送端 |
|---|---|
![]() |
![]() |
2.1 根页面(Index)职责
- 调用
NearLink.init(context)统一初始化:权限申请、端口注册、状态监听。 - 订阅星闪开关、连接状态,同步到
AppStorage供子页面使用。 - 页面销毁时调用
NearLink.destroy()释放资源。
2.2 发送端流程(SenderPage)
- 准备:监听连接状态,查询星闪开关(若未开启则引导用户打开)。
- 选文件 :调用
NearLink.pickFile()读取为ArrayBuffer。 - 开启广播 :
NearLink.startAdvertising(),携带服务 UUID 和厂商自定义数据manufacturerData。 - 等待连接:接收端主动连接,发送端收到连接状态回调,记录远端地址。
- 发送元数据 :发送元数据包
0xF1+ 文件名长度(2字节) + 文件大小(8字节) + 文件名(UTF-8)。接收端据此弹出询问弹窗。 - 等待确认 :发送端通过 Promise 阻塞等待接收端回复
0xEE(同意)或0xEF(拒绝)。无超时,可手动取消。 - 分块发送数据 :每块大小 2KB (实测稳定),每块前加
0xF2+ 4字节序号(小端) + 数据。每发一块 delay(10ms)(官方要求间隔 ≥10ms)。 - 发送完成标记 :发送
0xF3单字节包,通知接收端结束。 - 进度回调:每发送一块计算实时速度、剩余时间,更新 UI。
- 取消发送 :用户点击"取消发送" →
NearLink.cancelSend()→ 发送0xEC取消包 → 清理本地会话。
2.3 接收端流程(ReceiverPage)
- 准备:注册文件接收请求、进度、完成、错误回调。
- 扫描设备 :
NearLink.startScan(),使用manufacturerId=0x1234过滤,设备列表展示。 - 连接设备 :用户点击设备 →
NearLink.connect(address),连接成功后发送端自动停止广播。 - 接收元数据 :收到
0xF1包 → 解析文件名和大小 → 触发onFileReceiveRequest弹窗。 - 用户同意/拒绝 :点击同意 →
NearLink.acceptFile()发送0xEE;拒绝 →NearLink.rejectFile()发送0xEF。 - 接收数据块 :收到
0xF2包 → 解析序号和数据体 → 暂存到Map<序号, ArrayBuffer>。 - 完成标记 :收到
0xF3单字节 → 按序号顺序拼接所有块 → 完整性校验 (实际字节数 vs 预期大小)→ 触发onFileReceiveComplete。 - 保存文件 :调用
NearLink.saveFile()弹出系统保存对话框。 - 处理取消 :收到
0xEC取消包 → 关闭弹窗 → 提示"对方已取消传输"。
三、整体设计
3.1 应用架构
将职责划分为六个独立模块,各司其职:
PermissionManager:权限申请NearLinkStateManager:星闪开关状态AdvertisingManager:广播ScanManager:扫描NearLinkDataTransfer:数据传输底层封装FileTransferHandler:文件协议处理(分块、重组、确认/取消)
为简化 UI 层调用,再增加一个外观类 NearLink,内部封装上述六个模块,对外提供统一的静态 API。
3.2 核心技术栈
| 模块 | 核心 API | 说明 |
|---|---|---|
| 权限管理 | abilityAccessCtrl |
动态申请 ohos.permission.ACCESS_NEARLINK |
| 星闪状态 | manager.getState() / on('stateChange') |
查询/订阅开关状态 |
| 广播 | advertising.startAdvertising() |
发送端广播服务标识 |
| 扫描 | scan.startScan() |
接收端发现设备,用 manufacturerId 过滤 |
| 数据传输 | dataTransfer 模块 |
端口创建、连接、分块读写,关键参数 mtu |
| 文件操作 | DocumentViewPicker, fileIo |
选择本地文件、保存接收文件 |
3.3 数据协议设计
设计了一个可靠的应用层协议,每个包首字节标识类型:
| 包类型 | 首字节 | 后续内容 |
|---|---|---|
| 元数据包 | 0xF1 |
2字节文件名长度(小端)+ 8字节文件大小(小端)+ 文件名(UTF-8) |
| 数据块包 | 0xF2 |
4字节块序号(小端)+ 块数据(≤2KB) |
| 完成标记 | 0xF3 单字节 |
无 |
| 同意确认 | 0xEE 单字节 |
无 |
| 拒绝确认 | 0xEF 单字节 |
无 |
| 取消传输 | 0xEC 单字节 |
无 |
接收方根据首字节区分类型,按序号重组数据,并在完成时校验完整性。
3.4 星闪服务 UUID
星闪使用 128 位 UUID 标识服务,格式:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx(32 个十六进制字符,分为 5 段)。
- 前 112 位 :星闪联盟固定分配,基础标识为 37BEA880-FC70-11EA-B720-00000000(即前 4 段 + 第 5 段的前 8 个十六进制字符),不可修改不可修改。
- 后 16 位(第 5 段的最后 4 个字符):开发者自定义,用于区分不同服务。
示例:
javascript
static readonly FILE_TRANSFER_SERVICE_UUID: string = '37BEA880-FC70-11EA-B720-000000007895';
- 固定部分:
37BEA880-FC70-11EA-B720-00000000 - 自定义部分:
78957895(第 5 段共 12 字符,前 8 位固定为零,后 4 位自定义)
格式错误会导致端口注册失败。此设计保证全局唯一性,扫描时可快速识别标准服务。
3.5 项目结构
text
NearLinkFileTransfer
└── src
└── main
└── ets
├── common
│ ├── bean // 数据模型层
│ │ ├── ConnectionState.ets // 连接状态枚举
│ │ ├── FileData.ets // 文件数据(文件名 + ArrayBuffer)
│ │ ├── NearLinkDevice.ets // 星闪设备信息(地址、名称、信号强度)
│ │ └── ProgressData.ets // 传输进度(已传/总大小、速度、剩余时间、百分比)
│ ├── constants
│ │ └── NearLinkConstants.ets // 常量配置:厂商ID、MTU、分块大小、发送间隔、UUID、协议标志等
│ ├── managers // 核心业务管理层
│ │ ├── AdvertisingManager.ets // 广播管理:开启/停止广播,携带厂商数据
│ │ ├── DataTransferManager.ets // 数据传输底层封装:端口创建、连接、writeData、数据接收回调
│ │ ├── FileTransferHandler.ets // 文件传输协议核心:分块发送、接收重组、确认/取消、完整性校验、会话隔离
│ │ ├── NearLink.ets // 对外统一API,封装所有底层管理器,提供事件监听
│ │ ├── NearLinkStateManager.ets // 星闪开关状态:查询、订阅状态变化
│ │ ├── PermissionManager.ets // 权限管理:动态申请 ohos.permission.ACCESS_NEARLINK
│ │ └── ScanManager.ets // 扫描管理:启动/停止扫描,用 manufacturerId 过滤设备
│ └── utils
│ ├── FileUtil.ets // 文件工具:调用系统文件选择器读取 ArrayBuffer,保存文件到用户目录
│ └── Logger.ets // 日志封装,统一 TAG 和级别控制
├── entryability
│ └── EntryAbility.ets
├── pages
│ ├── Index.ets // 根页面:Tab 切换发送/接收,全局初始化 NearLink,状态同步
│ ├── ReceiverPage.ets // 接收端页面:扫描设备、连接、接收文件弹窗、进度、保存、错误处理
│ ├── SenderPage.ets // 发送端页面:选择文件、开启广播、发送、进度、取消发送
│ └── SimpleTransferTest.ets // 调试页面:简化版三次握手测试,用于定位问题(未在主流程使用)
├── view // 可复用 UI 组件
│ ├── BroadcastControlCard.ets // 广播控制卡片:开启/停止广播按钮及状态显示
│ ├── ConnectedDeviceCard.ets // 已连接设备卡片:显示设备信息、断开按钮
│ ├── DeviceList.ets // 设备列表组件:展示扫描到的星闪设备,支持点击连接
│ ├── FileRequestCard.ets // 文件请求卡片:接收端弹窗,显示文件名/大小,同意/拒绝按钮
│ ├── FileSelectCard.ets // 文件选择卡片:发送端显示已选文件,提供浏览、发送、取消按钮
│ ├── ScanControlCard.ets // 扫描控制卡片:接收端开启/停止扫描按钮及状态显示
│ └── TransferProgress.ets // 传输进度组件:进度条、速度、剩余时间、百分比
└── resources
四、关键代码实现
4.1 常量配置(NearLinkConstants.ets)
集中管理所有可调参数和协议标志,MTU 显式设置是提速的关键。
javascript
export class NearLinkConstants {
static readonly MANUFACTURER_ID: number = 0x1234;
// 星闪连接 MTU(最大传输单元),默认512字节,可取值 [0,65535]
static readonly DEFAULT_MTU: number = 2048;
// 分块大小 = MTU - 协议头开销(1字节类型 + 4字节序号 = 5字节)
static readonly DEFAULT_CHUNK_SIZE: number = 2048 - 5; // 2043 字节 ≈ 2KB
static readonly WRITE_DATA_INTERVAL_MS: number = 10; // 官方要求 ≥10ms
static readonly ADVERTISING_INTERVAL_MS: number = 500;
static readonly SCAN_TIMEOUT_SEC: number = 30;
// 协议控制包标识
static readonly CONFIRM_AGREE: number = 0xEE;
static readonly CONFIRM_REJECT: number = 0xEF;
static readonly FLAG_CANCEL: number = 0xEC;
// 协议数据包标识
static readonly FLAG_METADATA: number = 0xF1;
static readonly FLAG_DATA_CHUNK: number = 0xF2;
static readonly FLAG_FINISH: number = 0xF3;
// 传输限制
static readonly MAX_FILE_SIZE_BYTES: number = 100 * 1024 * 1024; // 100MB
static readonly MAX_FILENAME_LENGTH: number = 255;
static readonly FILE_TRANSFER_SERVICE_UUID: string = '37BEA880-FC70-11EA-B720-000000007895';
}
4.2 权限管理器(PermissionManager.ets)
首先在 module.json5 中声明权限,然后动态申请。
javascript
import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { Logger } from '../utils/Logger';
const TAG = 'PermissionManager';
export class PermissionManager {
static async checkAndRequestNearLinkPermission(context: Context): Promise<boolean> {
const atManager = abilityAccessCtrl.createAtManager();
const permission: Permissions = 'ohos.permission.ACCESS_NEARLINK';
try {
const grantStatus = await atManager.checkAccessToken(context.applicationInfo.accessTokenId, permission);
if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
Logger.i(TAG, '星闪权限已授予');
return true;
}
Logger.i(TAG, '请求星闪权限');
const result = await atManager.requestPermissionsFromUser(context, [permission]);
const granted = result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
Logger.i(TAG, `权限请求结果: ${granted}`);
return granted;
} catch (err) {
Logger.e(TAG, `权限检查失败: ${(err as BusinessError).code}`);
return false;
}
}
}
4.3 星闪状态管理器(NearLinkStateManager.ets)
javascript
import { manager } from "@kit.NearLinkKit";
import { Logger } from "../utils/Logger";
const TAG = 'NearLinkStateManager';
export class NearLinkStateManager {
private static stateChangeHandler?: (data: manager.NearlinkState) => void;
static getState(): boolean {
try {
const state = manager.getState();
const isOn = state === manager.NearlinkState.STATE_ON;
Logger.i(TAG, `星闪状态查询: ${isOn ? '开启' : '关闭'}`);
return isOn;
} catch (err) {
Logger.e(TAG, `状态查询失败: ${(err as BusinessError).code}`);
return false;
}
}
static subscribeStateChange(callback: (isOn: boolean) => void): void {
const onStateChange = (data: manager.NearlinkState) => {
const isOn = data === manager.NearlinkState.STATE_ON;
Logger.i(TAG, `星闪状态变化: ${isOn ? '开启' : '关闭'}`);
callback(isOn);
};
try {
manager.on('stateChange', onStateChange);
NearLinkStateManager.stateChangeHandler = onStateChange;
} catch (err) {
Logger.e(TAG, `订阅状态变化失败: ${(err as BusinessError).code}`);
}
}
static unsubscribeStateChange(): void {
if (NearLinkStateManager.stateChangeHandler) {
try {
manager.off('stateChange', NearLinkStateManager.stateChangeHandler);
NearLinkStateManager.stateChangeHandler = undefined;
} catch (err) {
Logger.e(TAG, `取消订阅失败: ${(err as BusinessError).code}`);
}
}
}
}
4.4 扫描管理器(ScanManager.ets)
使用 manufacturerId 过滤只扫描我们的服务。
javascript
import { scan } from '@kit.NearLinkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { Logger } from '../utils/Logger';
import { NearLinkConstants } from '../constants/NearLinkConstants';
import { NearLinkDevice } from '../bean/NearLinkDevice';
const TAG = 'ScanManager';
export class ScanManager {
private static isScanning: boolean = false;
private static deviceFoundCallback?: (device: NearLinkDevice) => void;
private static deviceFoundHandler?: (data: Array<scan.ScanResults>) => void;
static onDeviceFound(callback: (device: NearLinkDevice) => void): void {
ScanManager.deviceFoundCallback = callback;
}
static async startScan(): Promise<boolean> {
if (ScanManager.isScanning) {
Logger.w(TAG, '扫描已在进行');
return true;
}
const onDeviceFound = (data: Array<scan.ScanResults>) => {
for (let device of data) {
const nearDevice: NearLinkDevice = {
address: device.address,
name: device.deviceName || '未知设备',
rssi: device.rssi || -100
};
Logger.i(TAG, `发现设备: ${nearDevice.name}, 地址: ${nearDevice.address}, RSSI: ${nearDevice.rssi}`);
ScanManager.deviceFoundCallback?.(nearDevice);
}
};
try {
scan.on('deviceFound', onDeviceFound);
// 使用 manufacturerId 过滤,只扫描我们定义的服务
const filters: scan.ScanFilters[] = [{
manufacturerId: NearLinkConstants.MANUFACTURER_ID
}];
const options: scan.ScanOptions = {
scanMode: scan.ScanMode.SCAN_MODE_BALANCED,
duration: NearLinkConstants.SCAN_TIMEOUT_SEC // 扫描30秒后自动停止
};
await scan.startScan(filters, options);
ScanManager.isScanning = true;
ScanManager.deviceFoundHandler = onDeviceFound;
Logger.i(TAG, '扫描已启动');
return true;
} catch (err) {
Logger.e(TAG, `启动扫描失败: ${(err as BusinessError).code}`);
return false;
}
}
static async stopScan(): Promise<void> {
if (!ScanManager.isScanning) {
return;
}
try {
await scan.stopScan();
if (ScanManager.deviceFoundHandler) {
scan.off('deviceFound', ScanManager.deviceFoundHandler);
ScanManager.deviceFoundHandler = undefined;
}
ScanManager.isScanning = false;
Logger.i(TAG, '扫描已停止');
} catch (err) {
Logger.e(TAG, `停止扫描失败: ${(err as BusinessError).code}`);
}
}
static getIsScanning(): boolean {
return ScanManager.isScanning;
}
}
4.5 广播管理器(AdvertisingManager.ets)
开启广播时携带 manufacturerData,让接收端能通过厂商 ID 过滤。
javascript
import { advertising } from '@kit.NearLinkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { Logger } from '../utils/Logger';
import { NearLinkConstants } from '../constants/NearLinkConstants';
const TAG = 'AdvertisingManager';
export class AdvertisingManager {
private static advertisingId: number = -1;
private static stateChangeHandler?: (info: advertising.AdvertisingStateChangeInfo) => void;
static subscribeStateChange(callback: (isAdvertising: boolean) => void): void {
const onStateChange = (info: advertising.AdvertisingStateChangeInfo) => {
const isAdvertising = info.state === advertising.AdvertisingState.STARTED;
Logger.i(TAG, `广播状态变化: ${isAdvertising ? '开启' : '停止'}, id=${info.advertisingId}`);
callback(isAdvertising);
};
try {
advertising.on('advertisingStateChange', onStateChange);
AdvertisingManager.stateChangeHandler = onStateChange;
} catch (err) {
Logger.e(TAG, `订阅广播状态失败: ${(err as BusinessError).code}`);
}
}
static async startAdvertising(): Promise<boolean> {
if (AdvertisingManager.advertisingId !== -1) {
Logger.w(TAG, '广播已开启,无需重复');
return true;
}
try {
const settings: advertising.AdvertisingSettings = {
interval: NearLinkConstants.ADVERTISING_INTERVAL_MS,
power: advertising.TxPowerMode.ADV_TX_POWER_HIGH,
isConnectable: true
};
// 构造厂商数据
const manufacturerValueBuffer = new Uint8Array(4);
manufacturerValueBuffer[0] = 0x01; // 自定义标识,可任意
manufacturerValueBuffer[1] = 0x02;
manufacturerValueBuffer[2] = 0x03;
manufacturerValueBuffer[3] = 0x04;
const manufacturerDataUnit: advertising.ManufacturerData = {
manufacturerId: NearLinkConstants.MANUFACTURER_ID, // 例如 0x1234
manufacturerData: manufacturerValueBuffer.buffer
};
const advData: advertising.AdvertisingData = {
serviceUuids: [NearLinkConstants.FILE_TRANSFER_SERVICE_UUID],
manufacturerData: [manufacturerDataUnit] // 添加这一行
};
const params: advertising.AdvertisingParams = {
advertisingSettings: settings,
advertisingData: advData
};
AdvertisingManager.advertisingId = await advertising.startAdvertising(params);
Logger.i(TAG, `广播已开启,ID=${AdvertisingManager.advertisingId}`);
return true;
} catch (err) {
Logger.e(TAG, `开启广播失败: ${(err as BusinessError).code}`);
return false;
}
}
static async stopAdvertising(): Promise<void> {
if (AdvertisingManager.advertisingId === -1) return;
try {
await advertising.stopAdvertising(AdvertisingManager.advertisingId);
Logger.i(TAG, `广播已停止,ID=${AdvertisingManager.advertisingId}`);
AdvertisingManager.advertisingId = -1;
} catch (err) {
Logger.e(TAG, `停止广播失败: ${(err as BusinessError).code}`);
}
}
static unsubscribeStateChange(): void {
if (AdvertisingManager.stateChangeHandler) {
try {
advertising.off('advertisingStateChange', AdvertisingManager.stateChangeHandler);
AdvertisingManager.stateChangeHandler = undefined;
} catch (err) {
Logger.e(TAG, `取消广播订阅失败: ${(err as BusinessError).code}`);
}
}
}
}
4.6 数据传输管理器(NearLinkDataTransfer.ets)
封装底层 dataTransfer,提供 init、connect、writeData、onRawData 等接口。
javascript
import { dataTransfer } from '@kit.NearLinkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { Logger } from '../utils/Logger';
import { NearLinkConstants } from '../constants/NearLinkConstants';
const TAG = 'NearLinkDataTransfer';
export class NearLinkDataTransfer {
private static connectedAddress: string = '';
private static connectionStateCallback?: (connected: boolean) => void;
private static rawDataCallback?: (data: ArrayBuffer) => void;
static init(): void {
try {
dataTransfer.createPort(NearLinkConstants.FILE_TRANSFER_SERVICE_UUID);
Logger.i(TAG, '端口注册成功');
} catch (err) {
Logger.e(TAG, `端口注册失败: ${(err as BusinessError).code}`);
throw new Error(`端口注册失败: ${(err as BusinessError).code}`);
}
dataTransfer.on('connectionStateChanged', (result) => {
const connected = result.state === 1;
if (connected) {
NearLinkDataTransfer.connectedAddress = result.address;
} else {
NearLinkDataTransfer.connectedAddress = '';
}
NearLinkDataTransfer.connectionStateCallback?.(connected);
});
dataTransfer.on('readData', (params) => {
Logger.i(TAG, `收到原始数据,长度=${params.data.byteLength}`);
NearLinkDataTransfer.rawDataCallback?.(params.data);
});
}
static onConnectionStateChange(callback: (connected: boolean) => void): void {
NearLinkDataTransfer.connectionStateCallback = callback;
}
static onRawData(callback: (data: ArrayBuffer) => void): void {
NearLinkDataTransfer.rawDataCallback = callback;
}
static getConnectedAddress(): string {
return NearLinkDataTransfer.connectedAddress;
}
static async connect(address: string): Promise<void> {
await dataTransfer.connect({
address: address,
uuid: NearLinkConstants.FILE_TRANSFER_SERVICE_UUID,
transferMode: dataTransfer.TransferMode.RELIABLE,
mtu: NearLinkConstants.DEFAULT_MTU
});
NearLinkDataTransfer.connectedAddress = address;
Logger.i(TAG, `连接成功: ${address}`);
}
static async disconnect(): Promise<void> {
if (!NearLinkDataTransfer.connectedAddress) return;
await dataTransfer.disconnect({
address: NearLinkDataTransfer.connectedAddress,
uuid: NearLinkConstants.FILE_TRANSFER_SERVICE_UUID
});
NearLinkDataTransfer.connectedAddress = '';
Logger.i(TAG, '已断开连接');
}
static async writeData(data: ArrayBuffer): Promise<void> {
if (!NearLinkDataTransfer.connectedAddress) {
throw new Error('未连接');
}
await dataTransfer.writeData({
address: NearLinkDataTransfer.connectedAddress,
uuid: NearLinkConstants.FILE_TRANSFER_SERVICE_UUID,
data: data
});
}
static destroy(): void {
try {
dataTransfer.destroyPort(NearLinkConstants.FILE_TRANSFER_SERVICE_UUID);
Logger.i(TAG, '端口已销毁');
} catch (err) {
Logger.e(TAG, `销毁端口失败: ${(err as BusinessError).code}`);
}
NearLinkDataTransfer.connectedAddress = '';
NearLinkDataTransfer.connectionStateCallback = undefined;
NearLinkDataTransfer.rawDataCallback = undefined;
}
}
4.7 文件传输协议处理器(FileTransferHandler.ets)------核心
由于代码较长,可下载完整工程查看这里只展示关键设计。文件数据分割合并完整设计细节下一篇讲,本次只详细分享星闪。
4.7.1 会话隔离
使用 ReceiveSession 和 SendSession 类分别管理发送和接收状态,避免静态变量污染导致并发问题。
4.7.2 发送端主流程
javascript
static async sendFile(fileBuffer, fileName, onProgress) {
// 发送元数据
await this.sendMetadata(...);
// 等待对方确认(无超时)
const agreed = await new Promise<boolean>((resolve) => {
session.confirmResolver = resolve;
});
if (!agreed) throw new Error('接收端拒绝接收');
// 分块发送
for (let i = 0; i < totalChunks; i++) {
if (session.isCancelled) throw new Error('发送已被取消');
await this.sendDataChunk(...);
onProgress(...);
await delay(10);
}
await this.sendFinishMarker();
}
4.7.3 取消发送实现
javascript
static async cancelSend(): Promise<void> {
const session = this.currentSendSession;
if (!session) return;
session.isCancelled = true;
if (session.confirmResolver) {
session.confirmResolver(false); // 立即结束等待
session.confirmResolver = undefined;
}
await this.sendCancelPacket();
this.currentSendSession = null;
}
4.7.4 接收端数据重组与完整性校验
javascript
private static handleFinishMarker(): void {
// 校验接收字节数是否等于预期
if (session.receivedBytes !== session.expectedTotalBytes) {
this.onErrorCallback?.(`文件完整性校验失败`);
return;
}
// 按序号拼接
const sorted = Array.from(session.receivedChunks.keys()).sort((a,b)=>a-b);
const result = new ArrayBuffer(session.expectedTotalBytes);
let offset = 0;
for (const idx of sorted) {
const chunk = session.receivedChunks.get(idx)!;
new Uint8Array(result).set(new Uint8Array(chunk), offset);
offset += chunk.byteLength;
}
this.onFileReceiveCompleteCallback?.(result, session.expectedFileName);
this.clearReceiveSession();
}
4.8 外观类(NearLink.ets)
对外提供统一的静态 API,封装所有底层细节。UI 层只需调用 NearLink.init()、NearLink.startAdvertising()、NearLink.sendFile()、NearLink.onFileReceiveRequest() 等即可。内部使用 Set 管理多监听器,支持多个页面同时订阅。
javascript
import { FileTransferHandler } from './FileTransferHandler';
import { AdvertisingManager, ScanManager, NearLinkDataTransfer, PermissionManager, NearLinkStateManager } from './...';
export class NearLink {
// 事件监听器集合(支持多订阅)
private static stateChangeListeners = new Set<(isOn: boolean) => void>();
private static deviceFoundListeners = new Set<(device: NearLinkDevice) => void>();
private static connectionStateChangeListeners = new Set<(state: ConnectionState, address: string) => void>();
private static fileReceiveRequestListeners = new Set<(fileName: string, fileSize: number) => void>();
private static fileReceiveProgressListeners = new Set<(progress: ProgressData) => void>();
private static fileReceiveCompleteListeners = new Set<(fileBuffer: ArrayBuffer, fileName: string) => void>();
private static sendConfirmListeners = new Set<(agreed: boolean) => void>();
private static sendProgressListeners = new Set<(progress: ProgressData) => void>();
private static onFileReceiveErrorCallback?: (errorMsg: string) => void;
// 初始化(权限、端口、回调转发)
static async init(context: Context): Promise<boolean> {
// 权限申请、端口注册、订阅底层回调并转发到监听器集合
}
// 广播与扫描
static async startAdvertising(): Promise<boolean> { return AdvertisingManager.startAdvertising(); }
static async startScan(): Promise<boolean> { return ScanManager.startScan(); }
// 文件传输
static async sendFile(fileBuffer: ArrayBuffer, fileName: string): Promise<void> {
await FileTransferHandler.sendFile(fileBuffer, fileName, (progress) => {
this.sendProgressListeners.forEach(cb => cb(progress));
});
}
static cancelSend(): Promise<void> { return FileTransferHandler.cancelSend(); }
static acceptFile(): void { FileTransferHandler.acceptFile(); }
static rejectFile(): void { FileTransferHandler.rejectFile(); }
// 事件注册(示例)
static onFileReceiveRequest(callback: (fileName: string, fileSize: number) => void): void {
this.fileReceiveRequestListeners.add(callback);
}
static offFileReceiveRequest(callback: (fileName: string, fileSize: number) => void): void {
this.fileReceiveRequestListeners.delete(callback);
}
static onFileReceiveError(callback: (errorMsg: string) => void): void {
this.onFileReceiveErrorCallback = callback;
}
// 其他 on/off 方法类似...
}
完整代码请参考仓库代码,这里仅展示核心结构。通过外观类,UI 层无需关心
FileTransferHandler、AdvertisingManager等内部模块,只需调用NearLink的静态方法并注册相应回调即可。
4.9 根页面
根页面负责初始化 NearLink以及状态、连接变化将数据同步给发送端页面和接收端。两个子页面再通过NearLink做本职工作发送接收等工作就不一一展示了。
javascript
async aboutToAppear(): Promise<void> {
// 1. 初始化 NearLink(内部完成权限请求和所有底层初始化)
const initSuccess = await NearLink.init(this.context);
if (!initSuccess) {
promptAction.showToast({ message: '星闪初始化失败,请检查权限和星闪开关' });
Logger.e(TAG, 'NearLink 初始化失败');
return;
}
// 2. 监听星闪开关状态,同步到全局状态(供子页面使用)
NearLink.onStateChange((isOn: boolean) => {
AppStorage.setOrCreate('nearLinkAvailable', isOn);
AppStorage.setOrCreate('nearLinkStatusText', isOn ? '就绪' : '请开启星闪');
if (!isOn) {
promptAction.showToast({ message: '星闪已关闭,请重新开启' });
}
});
// 3. 监听连接状态变化,同步到全局状态
NearLink.onConnectionStateChange((state: ConnectionState, address: string) => {
AppStorage.setOrCreate('connectionState', state);
AppStorage.setOrCreate('connectedAddress', address);
});
// 4. 初始化全局状态变量(默认值)
AppStorage.setOrCreate('nearLinkAvailable', NearLink.isNearLinkOn());
AppStorage.setOrCreate('nearLinkStatusText', NearLink.isNearLinkOn() ? '就绪' : '请开启星闪');
AppStorage.setOrCreate('connectionState', ConnectionState.IDLE);
AppStorage.setOrCreate('connectedAddress', '');
}
aboutToDisappear(): void {
// 应用退出时销毁资源
NearLink.destroy();
}
五、踩坑与经验总结
代码越写多,几个bug定位不到问题,大的问题基本都在文件数据处理这块。我写了一个 SimpleTransferTest 页面,不对数据做任何过多干涉:两台设备连接后模拟三次握手(A:我给你发文件 → B:你发吧 → A:开始发数据包)。通过这个最小测试,逐步定位到了所有问题。
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 扫描不到设备 | 广播未开启或过滤条件不匹配 | 确保发送端已调用 startAdvertising;扫描时使用相同的 manufacturerId |
| 端口注册失败 | UUID 格式不符合 128 位规范 | 固定前缀 37BEA880-FC70-11EA-B720-00000000 + 4 位自定义,最后一段共12字符 |
| 发送失败或丢包 | 连续 writeData 没有间隔 |
每次 writeData 后必须 await delay(10),间隔 ≥10ms |
| 大于 1KB 的分块传输失败 | 未显式设置 MTU,使用默认值 512 字节,大数据块被底层丢弃 | 必须在 connect 时显式设置 mtu 参数 (如 mtu: 2048),并将分块大小设为 ≤ MTU |
| 对方取消后弹窗不关闭 | 未处理取消包 | 增加 FLAG_CANCEL 包,接收端收到后关闭弹窗并提示 |
| "已有发送任务在进行中"误报 | 取消发送时未清空会话状态 | 在 cancelSend 中立即清理 currentSendSession 并结束等待中的 Promise |
| 接收端文件完整性校验失败 | 丢包后未校验 | 接收端在收到完成标记后比较 receivedBytes 与 expectedTotalBytes,不一致则报错 |
| 协议标志冲突导致解析错误 | FLAG_METADATA 与 FLAG_FINISH 同为 0xFF |
重新分配独立标志值(0xF1, 0xF2, 0xF3) |
| 接收端进度回调性能差 | 每次收到块都遍历 Map 计算总大小 | 维护 receivedBytes 增量累加,避免遍历 |
| 传输速度慢(约100KB/s) | 分块太小(1KB)且 MTU 未设置 | 显式设置 MTU=2048,分块大小提至 2KB,速度提升至 约200KB/s |
特别提醒:MTU 和发送间隔非常重要。如果不设置 MTU,系统默认 512 字节;如果分块大小超过 1024 且未正确设置,数据传输队列拥堵或其他的问题,导致长时间无法发送新数据。使用 1024 字节分块时速度很慢,想提速必须调整 MTU 并适当增大分块(但不要超过 MTU 减去协议头)。
六、结语
本文完整展示了基于鸿蒙星闪实现可靠文件传输工具的全过程,重点说明了提升传输速度的关键点以及限制。下载代码即可运行演示。
关于文件数据处理,很多地方都会用到不仅仅是星闪传输,所以针对这部分内容我也会做个详细的讲解,如何把一个大文件一点点切割发送出去最后再合并。如果觉得有用,请点赞、收藏、转发支持!

