鸿蒙星闪实战:从零实现高速可靠的跨设备文件传输 - 星闪篇

完整源码: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)

  1. 准备:监听连接状态,查询星闪开关(若未开启则引导用户打开)。
  2. 选文件 :调用 NearLink.pickFile() 读取为 ArrayBuffer
  3. 开启广播NearLink.startAdvertising(),携带服务 UUID 和厂商自定义数据 manufacturerData
  4. 等待连接:接收端主动连接,发送端收到连接状态回调,记录远端地址。
  5. 发送元数据 :发送元数据包 0xF1 + 文件名长度(2字节) + 文件大小(8字节) + 文件名(UTF-8)。接收端据此弹出询问弹窗。
  6. 等待确认 :发送端通过 Promise 阻塞等待接收端回复 0xEE(同意)或 0xEF(拒绝)。无超时,可手动取消
  7. 分块发送数据 :每块大小 2KB (实测稳定),每块前加 0xF2 + 4字节序号(小端) + 数据。每发一块 delay(10ms)(官方要求间隔 ≥10ms)。
  8. 发送完成标记 :发送 0xF3 单字节包,通知接收端结束。
  9. 进度回调:每发送一块计算实时速度、剩余时间,更新 UI。
  10. 取消发送 :用户点击"取消发送" → NearLink.cancelSend() → 发送 0xEC 取消包 → 清理本地会话。

2.3 接收端流程(ReceiverPage)

  1. 准备:注册文件接收请求、进度、完成、错误回调。
  2. 扫描设备NearLink.startScan(),使用 manufacturerId=0x1234 过滤,设备列表展示。
  3. 连接设备 :用户点击设备 → NearLink.connect(address),连接成功后发送端自动停止广播。
  4. 接收元数据 :收到 0xF1 包 → 解析文件名和大小 → 触发 onFileReceiveRequest 弹窗。
  5. 用户同意/拒绝 :点击同意 → NearLink.acceptFile() 发送 0xEE;拒绝 → NearLink.rejectFile() 发送 0xEF
  6. 接收数据块 :收到 0xF2 包 → 解析序号和数据体 → 暂存到 Map<序号, ArrayBuffer>
  7. 完成标记 :收到 0xF3 单字节 → 按序号顺序拼接所有块 → 完整性校验 (实际字节数 vs 预期大小)→ 触发 onFileReceiveComplete
  8. 保存文件 :调用 NearLink.saveFile() 弹出系统保存对话框。
  9. 处理取消 :收到 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,提供 initconnectwriteDataonRawData 等接口。

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 会话隔离

使用 ReceiveSessionSendSession 类分别管理发送和接收状态,避免静态变量污染导致并发问题。

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 层无需关心 FileTransferHandlerAdvertisingManager 等内部模块,只需调用 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
接收端文件完整性校验失败 丢包后未校验 接收端在收到完成标记后比较 receivedBytesexpectedTotalBytes,不一致则报错
协议标志冲突导致解析错误 FLAG_METADATAFLAG_FINISH 同为 0xFF 重新分配独立标志值(0xF1, 0xF2, 0xF3
接收端进度回调性能差 每次收到块都遍历 Map 计算总大小 维护 receivedBytes 增量累加,避免遍历
传输速度慢(约100KB/s) 分块太小(1KB)且 MTU 未设置 显式设置 MTU=2048,分块大小提至 2KB,速度提升至 约200KB/s

特别提醒:MTU 和发送间隔非常重要。如果不设置 MTU,系统默认 512 字节;如果分块大小超过 1024 且未正确设置,数据传输队列拥堵或其他的问题,导致长时间无法发送新数据。使用 1024 字节分块时速度很慢,想提速必须调整 MTU 并适当增大分块(但不要超过 MTU 减去协议头)。

六、结语

本文完整展示了基于鸿蒙星闪实现可靠文件传输工具的全过程,重点说明了提升传输速度的关键点以及限制。下载代码即可运行演示。

关于文件数据处理,很多地方都会用到不仅仅是星闪传输,所以针对这部分内容我也会做个详细的讲解,如何把一个大文件一点点切割发送出去最后再合并。如果觉得有用,请点赞、收藏、转发支持!

相关推荐
小红星闪啊闪2 小时前
鸿蒙开发速通(一)
harmonyos
特立独行的猫a2 小时前
HarmonyOS鸿蒙PC开源QT软件移植:移植开源文本编辑器 NotePad--(Ndd)到鸿蒙 PC实践总结
qt·开源·notepad++·harmonyos·notepad--·鸿蒙pc
IntMainJhy2 小时前
【futter for open harmony】Flutter 聊天应用实战:Material Design 3 全局 UI 规范落地指南✨
flutter·华为·harmonyos
IntMainJhy2 小时前
【flutter for open harmony】Flutter 聊天应用实战:go_router 路由管理完全实现指南
flutter·华为·harmonyos
liulian09162 小时前
【Flutter For OpenHarmony第三方库】Flutter 页面导航的鸿蒙化适配实战
flutter·华为·学习方法·harmonyos
南村群童欺我老无力.2 小时前
鸿蒙PC开发的borderWidth_API签名的类型陷阱
华为·harmonyos
前端不太难2 小时前
鸿蒙游戏 + AI:自动测试与自动发布
人工智能·游戏·harmonyos
Lanren的编程日记3 小时前
Flutter 鸿蒙应用用户反馈功能实战:快速收集用户意见与建议
flutter·华为·harmonyos
刘大猫.16 小时前
华为昇腾芯片将为DeepSeek-V4推理,通往国产算力自由
华为·ai·大模型·算力·deepseek·deepseek-v4·昇腾芯片