文件互传功能实现

前言

前面的文章文件互传---发送文件完整流程-CSDN博客讲到了发送端想要发送文件互传,指令从应用层传到napi解析分发一步步传到软总线,再到专用proxy会话通道的建立开启(优先WiFi p2p,降级WiFi 5G),执行最底层的文件互传功能函数(本次的目的)。

文件发送的调度中心

/foundation/communication/dsoftbus/sdk/transmission/trans_channel/proxy/src/client_trans_proxy_file_manager.c

HandleFileSendingProcess 是文件发送的"调度中心"------用 do-while(false) 模式组织三段逻辑:① 创建发送上下文(获取会话信息、回调、加密配置等)② 调用 ProxyStartSendFile 发送所有文件 ③ 统一错误处理和清理。它本身不发送数据,只负责准备环境和善后。

源代码片段

复制代码
static int32_t HandleFileSendingProcess(
    int32_t channelId, const char *sFileList[], const char *dFileList[], uint32_t fileCnt)
{
    SendListenerInfo *sendInfo = NULL;
    int32_t ret = SOFTBUS_FILE_ERR;
    do {
        int32_t osType;
        (void)ClientTransProxyGetOsTypeByChannelId(channelId, &osType);
        ret = CreateSendListenerInfo(&sendInfo, channelId, osType);
        if (ret != SOFTBUS_OK || sendInfo == NULL) {
            TRANS_LOGE(TRANS_FILE, "create send listener info failed! ret=%{public}" PRId32, ret);
            break;
        }
        ret = ProxyStartSendFile(sendInfo, sFileList, dFileList, fileCnt);
        if (ret != SOFTBUS_OK) {
            TRANS_LOGE(TRANS_FILE, "proxy send file failed ret=%{public}" PRId32, ret);
            DeletePendingPacket(sendInfo->sessionId, sendInfo->waitSeq);
            ret = SOFTBUS_TRANS_PROXY_SENDMSG_ERR;
            break;
        }
        TRANS_LOGI(TRANS_FILE, "proxy send file trans ok");
    } while (false);

    if (ret != SOFTBUS_OK) {
        TRANS_LOGE(TRANS_FILE, "proxy send file trans error");
        if (sendInfo != NULL && sendInfo->fileListener.socketSendCallback != NULL) {
            FileEvent event = {
                .type = FILE_EVENT_SEND_ERROR,
                .files = sendInfo->totalInfo.files,
                .fileCnt = sendInfo->totalInfo.fileCnt,
                .bytesProcessed = 0,
                .bytesTotal = 0,
            };
            sendInfo->fileListener.socketSendCallback(sendInfo->sessionId, &event);
        } else if (sendInfo != NULL && sendInfo->fileListener.sendListener.OnFileTransError != NULL) {
            sendInfo->fileListener.sendListener.OnFileTransError(sendInfo->sessionId);
        }
    }

    if (sendInfo != NULL) {
        ReleaseSendListenerInfo(sendInfo);
        sendInfo = NULL;
    }

    return ret;
}

源代码解析

复制代码
static int32_t HandleFileSendingProcess(
    int32_t channelId, const char *sFileList[], const char *dFileList[], uint32_t fileCnt)
{
    SendListenerInfo *sendInfo = NULL;
    int32_t ret = SOFTBUS_FILE_ERR;
    
    do {
        // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        // 第1步:获取对端操作系统类型
        // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        int32_t osType;
        ClientTransProxyGetOsTypeByChannelId(channelId, &osType);
        //  ↑ 对端是 Android?Linux?Windows?
        //  不同系统的文件路径格式不同,发送时需要适配

        // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        // 第2步:创建发送上下文 ⭐
        // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        ret = CreateSendListenerInfo(&sendInfo, channelId, osType);
        //  内部做了:
        //  ├─ 获取 sessionId(会话ID)
        //  ├─ 获取 sessionName(会话名)
        //  ├─ 获取文件加密配置(是否加密、算法、CRC校验)
        //  ├─ 获取文件监听器(进度回调、完成回调、错误回调)
        //  ├─ 获取链路类型(BR蓝牙?WiFi?)→ 决定分片大小
        //  └─ 加入全局发送监听列表
        
        if (ret != SOFTBUS_OK || sendInfo == NULL) {
            break;  // 创建失败,跳到错误处理
        }

        // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        // 第3步:⭐ 真正发送文件
        // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        ret = ProxyStartSendFile(sendInfo, sFileList, dFileList, fileCnt);
        //  内部做了:
        //  ├─ CalcAllFilesInfo()     计算所有文件总大小
        //  ├─ for 循环 SendSingleFile()  逐个发送
        //  ├─ SendFileList()         发送文件列表确认
        //  └─ 回调通知完成
        
        if (ret != SOFTBUS_OK) {
            DeletePendingPacket(sendInfo->sessionId, sendInfo->waitSeq);
            //  ↑ 清理等待中的 ACK 包
            break;
        }
    } while (false);
    //       ↑ do-while(false) 技巧:
    //         用 break 代替 goto 跳到统一错误处理

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第4步:错误处理
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    if (ret != SOFTBUS_OK) {
        if (sendInfo->fileListener.socketSendCallback != NULL) {
            // 新版回调:FileEvent 方式
            FileEvent event = {
                .type = FILE_EVENT_SEND_ERROR,
                .files = sendInfo->totalInfo.files,
                .fileCnt = sendInfo->totalInfo.fileCnt,
            };
            sendInfo->fileListener.socketSendCallback(sendInfo->sessionId, &event);
        } else if (sendInfo->fileListener.sendListener.OnFileTransError != NULL) {
            // 旧版回调:OnFileTransError 方式
            sendInfo->fileListener.sendListener.OnFileTransError(sendInfo->sessionId);
        }
    }

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第5步:清理发送上下文
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    if (sendInfo != NULL) {
        ReleaseSendListenerInfo(sendInfo);
        //  ↑ 从全局监听列表移除 + 释放内存
    }

    return ret;
}

SendListenerInfo 上下文里有什么

复制代码
SendListenerInfo
├── channelId        ← 通道ID
├── sessionId        ← 会话ID
├── osType           ← 对端系统类型
├── packetSize       ← 分片大小(蓝牙小/WiFi大)
├── fileEncrypt      ← 是否加密
├── algorithm        ← 加密算法
├── crc              ← CRC校验开关
├── waitSeq          ← 等待确认的序列号
├── result           ← 发送结果
├── totalInfo        ← 文件总大小、总数量、已处理字节
└── fileListener     ← 回调函数集合
     ├── socketSendCallback      ← 新版回调(FileEvent)
     └── sendListener
          ├── OnFileTransError   ← 旧版错误回调
          └── OnSendFileFinished ← 旧版完成回调

当前在整个proxy链路中的位置

复制代码
ProxyChannelSendFile()          ← 加锁
  └─ HandleFileSendingProcess() ← 🎯 你在这里:创建上下文+调度
       ├─ CreateSendListenerInfo()   ← 准备上下文
       ├─ ProxyStartSendFile()       ← 真正发送
       │    ├─ CalcAllFilesInfo()     ← 算总大小
       │    ├─ SendSingleFile() ×N    ← 逐个发送
       │    └─ SendFileList()         ← 发确认
       ├─ 错误回调                     ← 失败时通知上层
       └─ ReleaseSendListenerInfo()   ← 清理上下文

读取对端操作系统类型

职责:从全局proxy列表中,根据channelid找到对应通道,读取对端操作系统的类型

流程

复制代码
int32_t ClientTransProxyGetOsTypeByChannelId(int32_t channelId, int32_t *osType)
{
    // ① 参数校验
    if (osType == NULL || g_proxyChannelInfoList == NULL) {
        return SOFTBUS_INVALID_PARAM;
    }

    // ② 加锁(保护全局链表)
    SoftBusMutexLock(&g_proxyChannelInfoList->lock);

    // ③ 遍历链表查找
    LIST_FOR_EACH_ENTRY(item, &g_proxyChannelInfoList->list, ClientProxyChannelInfo, node) {
        if (item->channelId == channelId) {
            *osType = item->detail.osType;   // ← 找到了,取出 osType
            SoftBusMutexUnlock(&g_proxyChannelInfoList->lock);
            return SOFTBUS_OK;               // ← 提前返回
        }
    }

    // ④ 没找到
    SoftBusMutexUnlock(&g_proxyChannelInfoList->lock);
    return SOFTBUS_NOT_FIND;
}

全局链表长什么样

复制代码
g_proxyChannelInfoList(全局 Proxy 通道信息链表)
┌─────────────────────────────────────────────────────┐
│  lock: 🔒                                           │
│  list: ──→ [ChannelInfo] ──→ [ChannelInfo] ──→ ...  │
└─────────────────────────────────────────────────────┘

每个 ChannelInfo 节点:
┌──────────────────────────┐
│  channelId = 1            │
│  detail:                  │
│    osType = OS_ANDROID    │  ← 对端是 Android
│    ...                    │
│  node: → 下一个节点       │
└──────────────────────────┘
┌──────────────────────────┐
│  channelId = 2            │
│  detail:                  │
│    osType = OS_LINUX      │  ← 对端是 Linux
│    ...                    │
│  node: → 下一个节点       │
└──────────────────────────┘

为什么需要对端系统类型

复制代码
不同操作系统的文件路径格式不同:

Android/Linux:  /data/app/photo.jpg
Windows:        C:\Users\photo.jpg
HarmonyOS:      /data/app/el2/100/com.gallery/photo.jpg

发送文件时需要根据对端 OS:
  ├─ 路径分隔符适配(/ vs \)
  ├─ 文件名长度限制不同
  └─ 沙箱路径规则不同

创建文件发送的完整工具箱

CreateSendListenerInfo 是创建文件发送的"完整工具箱" ------分配内存后,通过 GetSendListenerInfoByChannelId 填充所有必要配置(会话ID、加密设置、回调函数、分片大小等),然后通过 AddSendListenerInfo 注册到全局监听列表。这样后续发送过程中收到ACK或需要回调时都能找到对应的上下文。

职责分配内存 + 初始化所有发送所需的配置信息(会话、回调、加密、分片大小等),并注册到全局监听列表

源码流程

复制代码
static int32_t CreateSendListenerInfo(
    SendListenerInfo **sendListenerInfo,   // ← 二级指针,输出参数
    int32_t channelId, 
    int32_t osType)
{
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第1步:分配内存
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    SendListenerInfo *sendInfo = SoftBusCalloc(sizeof(SendListenerInfo));
    if (sendInfo == NULL) {
        return SOFTBUS_MALLOC_ERR;          // 内存不足
    }

    int32_t ret = SOFTBUS_FILE_ERR;
    do {
        // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        // 第2步:填充发送上下文 ⭐
        // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        ret = GetSendListenerInfoByChannelId(channelId, sendInfo, osType);
        if (ret != SOFTBUS_OK) break;
        
        // 内部填充了:
        //  ├─ sessionId(从 channelId 查出)
        //  ├─ sessionName(会话名)
        //  ├─ fileEncrypt / algorithm / crc(加密和校验配置)
        //  ├─ fileListener(回调函数集合)
        //  ├─ packetSize(分片大小:蓝牙小/WiFi大)
        //  ├─ channelId / sessionId / osType
        //  └─ 初始化链表节点

        // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        // 第3步:注册到全局监听列表 ⭐⭐
        // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        ret = AddSendListenerInfo(sendInfo);
        if (ret != SOFTBUS_OK) break;
        //  ↑ 加入全局链表,后续收到 ACK 时能找到这个上下文
        
    } while (false);

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第4步:失败时清理内存
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    if (ret != SOFTBUS_OK) {
        SoftBusFree(sendInfo);              // ← 失败就释放
        sendInfo = NULL;                    // ← 置空
    }

    *sendListenerInfo = sendInfo;           // ← 输出给调用者
    return ret;
}

源码片段

复制代码
static int32_t CreateSendListenerInfo(SendListenerInfo **sendListenerInfo, int32_t channelId, int32_t osType)
{
    SendListenerInfo *sendInfo = (SendListenerInfo *)SoftBusCalloc(sizeof(SendListenerInfo));
    if (sendInfo == NULL) {
        return SOFTBUS_MALLOC_ERR;
    }
    int32_t ret = SOFTBUS_FILE_ERR;
    do {
        ret = GetSendListenerInfoByChannelId(channelId, sendInfo, osType);
        if (ret != SOFTBUS_OK) {
            break;
        }

        ret = AddSendListenerInfo(sendInfo);
        if (ret != SOFTBUS_OK) {
            break;
        }
    } while (false);

    if (ret != SOFTBUS_OK) {
        SoftBusFree(sendInfo);
        sendInfo = NULL;
    }

    *sendListenerInfo = sendInfo;
    return ret;
}

为什么使用的是二级指针

间接操作调用者的指针

创建成功后的 SendListenerInfo 长什么样?

复制代码
CreateSendListenerInfo(&sendInfo, channelId, osType)
                    │
                    ▼
┌──────────────────────────────────────────────────────┐
│ SendListenerInfo *sendInfo                            │
├──────────────────────────────────────────────────────┤
│ channelId     = 1                                     │
│ sessionId     = 5                                     │
│ osType        = OS_ANDROID                            │
│ fileEncrypt   = true                                  │
│ algorithm     = AES_256                               │
│ crc           = true                                  │
│ packetSize    = 4096(WiFi)或 1024(蓝牙)             │
│ waitSeq       = 0                                     │
│ result        = SOFTBUS_OK                            │
│                                                      │
│ totalInfo:                                            │
│   files[]      = ["photo1.jpg", "doc.pdf"]            │
│   fileCnt      = 2                                    │
│   bytesTotal   = 10485760                             │
│   bytesProcessed = 0                                  │
│                                                      │
│ fileListener:                                         │
│   socketSendCallback = OnSendFileProgress()           │
│   sendListener.OnFileTransError = OnError()           │
│   sendListener.OnSendFileFinished = OnFinished()      │
│                                                      │
│ node: → 已加入全局监听链表                             │
└──────────────────────────────────────────────────────┘

注册到全局列表的意义

复制代码
全局监听链表:
┌────────────────────────────────────────┐
│ [sendInfo_A] → [sendInfo_B] → ...     │
│  (发photo1.jpg)   (发doc.pdf)          │
└────────────────────────────────────────┘

当收到 ACK 确认包时:
  ACK(seq=5, channelId=1)
       ↓
  遍历全局监听链表
       ↓
  找到 sendInfo_A(channelId=1)
       ↓
  更新 sendInfo_A->waitSeq = 6
  唤醒等待中的发送线程继续发送

在整个流程中的作用

复制代码
HandleFileSendingProcess()
  │
  ├─ CreateSendListenerInfo(&sendInfo, ...)   ← 🎯 创建"工具箱"
  │     ├─ 分配内存
  │     ├─ 填充所有配置(加密/回调/分片大小)
  │     └─ 注册到全局监听列表
  │
  ├─ ProxyStartSendFile(sendInfo, ...)         ← 用"工具箱"发送
  │     ├─ CalcAllFilesInfo(sendInfo->totalInfo)
  │     ├─ SendSingleFile(sendInfo, ...)
  │     └─ 回调 sendInfo->fileListener(...)
  │
  └─ ReleaseSendListenerInfo(sendInfo)         ← 归还"工具箱"
        ├─ 从全局监听列表移除
        └─ 释放内存

文件分片配置

这里只分两种情况,这里其实是个存疑点,为什么带宽更大的反而分片更小。这个问题我网上搜了下,976字节是经典配置,是软总线底层的稳定优先的保底配置,有一个专业SFFT传输库专门解决open Harmony的高速传输问题,这个后面我再了解下。

复制代码
if (linkType == LANE_BR) {
        info->packetSize = PROXY_BR_MAX_PACKET_SIZE; //4096-48
    } else {
        info->packetSize = PROXY_BLE_MAX_PACKET_SIZE; //1024-48
    }

文件发送总指挥

职责:统筹整个文件发送流程:计算总大小 → 逐个发送 → 发确认列表 → 检查结果 → 回调通知。

流程

复制代码
static int32_t ProxyStartSendFile(
    const SendListenerInfo *sendInfo, const char *sFileList[], const char *dFileList[], uint32_t fileCnt)
{
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第1步:计算所有文件的总大小
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    if (CalcAllFilesInfo(&sendInfo->totalInfo, sFileList, fileCnt)) {
        return SOFTBUS_FILE_ERR;
    }
    //  内部:
    //    for (i = 0; i < fileCnt; i++)
    //      GetFileSize(sFileList[i]) → 累加到 bytesTotal
    //    
    //  结果:
    //    totalInfo.files = ["photo1.jpg", "doc.pdf"]
    //    totalInfo.fileCnt = 2
    //    totalInfo.bytesTotal = 10485760  ← 所有文件总大小
    //    totalInfo.bytesProcessed = 0     ← 已发送0

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第2步:逐个发送文件 ⭐
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    for (uint32_t index = 0; index < fileCnt; index++) {
        ret = SendSingleFile(sendInfo, sFileList[index], dFileList[index]);
        //              ↑ 一个文件发完再发下一个
        //              ↑ 失败则立即终止整个发送
        if (ret != SOFTBUS_OK) {
            return SOFTBUS_FILE_ERR;   // ← 任何一个失败,全部终止
        }
    }

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第3步:发送文件列表确认 ⭐⭐
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    ret = SendFileList(sendInfo->channelId, dFileList, fileCnt);
    //  ↑ 把所有接收端文件名打包成一个列表帧发过去
    //    告诉对端:"这些文件我都发完了,你核对一下"
    //
    //    帧类型:TRANS_SESSION_FILE_ALLFILE_SENT
    //    内容:["photo1.jpg", "doc.pdf"]
    if (ret != SOFTBUS_OK) {
        return SOFTBUS_FILE_ERR;
    }

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第4步:检查接收端结果
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    if (sendInfo->result != SOFTBUS_OK) {
        //  ↑ result 是在发送过程中,接收端通过 ACK 反馈的结果
        //    比如:磁盘满、写入失败、校验失败等
        return SOFTBUS_FILE_ERR;
    }

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第5步:回调通知发送完成
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    if (sendInfo->fileListener.socketSendCallback != NULL) {
        // 新版回调:FileEvent 方式(信息更丰富)
        FileEvent event = {
            .type = FILE_EVENT_SEND_FINISH,
            .files = sendInfo->totalInfo.files,
            .fileCnt = sendInfo->totalInfo.fileCnt,
            .bytesProcessed = sendInfo->totalInfo.bytesProcessed,
            .bytesTotal = sendInfo->totalInfo.bytesTotal,
        };
        sendInfo->fileListener.socketSendCallback(sendInfo->sessionId, &event);
    } else if (sendInfo->fileListener.sendListener.OnSendFileFinished != NULL) {
        // 旧版回调:只传文件名
        sendInfo->fileListener.sendListener.OnSendFileFinished(sendInfo->sessionId, dFileList[0]);
    }

    return SOFTBUS_OK;
}

源码片段

复制代码
static int32_t ProxyStartSendFile(
    const SendListenerInfo *sendInfo, const char *sFileList[], const char *dFileList[], uint32_t fileCnt)
{
    int32_t ret;
    if (CalcAllFilesInfo((FilesInfo *)&sendInfo->totalInfo, sFileList, fileCnt)) {
        TRANS_LOGE(TRANS_SDK, "calculate all files information failed");
        return SOFTBUS_FILE_ERR;
    }

    for (uint32_t index = 0; index < fileCnt; index++) {
        ret = SendSingleFile(sendInfo, sFileList[index], dFileList[index]);
        if (ret != SOFTBUS_OK) {
            TRANS_LOGE(
                TRANS_FILE, "send file failed. sendFile=%{private}s, ret=%{public}" PRId32, sFileList[index], ret);
            return SOFTBUS_FILE_ERR;
        }
    }
    ret = SendFileList(sendInfo->channelId, dFileList, fileCnt);
    if (ret != SOFTBUS_OK) {
        TRANS_LOGE(TRANS_FILE, "SendFileList failed");
        return SOFTBUS_FILE_ERR;
    }
    if (sendInfo->result != SOFTBUS_OK) {
        TRANS_LOGE(TRANS_FILE, "send file recv side fail channelId=%{public}d, errCode=%{public}d",
            sendInfo->channelId, sendInfo->result);
        return SOFTBUS_FILE_ERR;
    }
    if (sendInfo->fileListener.socketSendCallback != NULL) {
        FileEvent event = {
            .type = FILE_EVENT_SEND_FINISH,
            .files = sendInfo->totalInfo.files,
            .fileCnt = sendInfo->totalInfo.fileCnt,
            .bytesProcessed = sendInfo->totalInfo.bytesProcessed,
            .bytesTotal = sendInfo->totalInfo.bytesTotal,
            .UpdateRecvPath = NULL,
        };
        sendInfo->fileListener.socketSendCallback(sendInfo->sessionId, &event);
    } else if (sendInfo->fileListener.sendListener.OnSendFileFinished != NULL) {
        sendInfo->fileListener.sendListener.OnSendFileFinished(sendInfo->sessionId, dFileList[0]);
    }
    return SOFTBUS_OK;
}

时序图

发送单个文件

FileToFrameAndSendFile 是单文件发送的"准备+启动"函数 ------校验路径、获取绝对路径、计算帧数、打开文件、加读锁防止并发写入,然后调用 FileToFrame 进行真正的分帧发送。它保证了文件在发送过程中内容不会被修改,路径不会被伪造。

职责:打开文件 → 加文件锁 → 计算帧数 → 调用分片发送 → 清理资源。 它是 SendSingleFile 的核心实现,负责把一个文件从磁盘读出来分帧发送。

流程

复制代码
static int32_t FileToFrameAndSendFile(
    SendListenerInfo *sendInfo, const char *sourceFile, const char *destFile)
{
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第1步:校验接收端路径
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    if (!CheckDestFilePathValid(destFile)) {
        return SOFTBUS_FILE_ERR;
    }

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第2步:获取源文件的绝对路径
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    char *absSrcPath = SoftBusCalloc(PATH_MAX + 1);
    GetAndCheckRealPath(sourceFile, absSrcPath);
    //  "photo1.jpg" → "/data/app/el2/100/com.gallery/photo1.jpg"
    //  ↑ 相对路径转绝对路径,同时检查路径是否合法
    //  防止路径穿越攻击(如 "../../etc/passwd")

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第3步:计算文件大小和帧数 ⭐
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    uint64_t fileSize = 0;
    uint64_t frameNum = 0;
    GetAndCheckFileSize(absSrcPath, &fileSize, &frameNum, sendInfo->crc, sendInfo->packetSize);
    //  内部计算:
    //    fileSize = 10485760(10MB)
    //    oneFrameSize = packetSize - 协议头开销
    //    frameNum = fileSize / oneFrameSize + 1(向上取整)
    //    frameNum = 10485760 / 928 + 1 = 11300 帧
    //
    //  同时检查:
    //    fileSize > MAX_FILE_SIZE?→ 文件太大,拒绝

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第4步:打开文件 ⭐
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    int32_t fd = SoftBusOpenFile(absSrcPath, SOFTBUS_O_RDONLY);
    //  ↑ 只读模式打开
    if (fd < 0) {
        return SOFTBUS_FILE_ERR;   // 文件不存在或无权限
    }

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第5步:加文件锁 ⭐⭐
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    if (TryFileLock(fd, SOFTBUS_F_RDLCK, RETRY_READ_LOCK_TIMES) != SOFTBUS_OK) {
        SoftBusCloseFile(fd);
        return SOFTBUS_FILE_ERR;
    }
    //  ↑ 加读锁(F_RDLCK),防止发送过程中文件被别人写入
    //    重试 2 次(RETRY_READ_LOCK_TIMES = 2)
    //
    //    场景:你正在发 photo1.jpg,同时另一个进程在修改它
    //    → 读锁阻止写入,保证发送的数据一致性

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第6步:填充发送信息
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    sendInfo->fd = fd;
    sendInfo->fileSize = fileSize;
    sendInfo->frameNum = frameNum;

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第7步:⭐ 分帧发送
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    int32_t ret = FileToFrame(sendInfo, frameNum, destFile, fileSize);
    //  ↑ 真正的分帧+发送逻辑:
    //    第0帧:起始帧(文件名、大小)
    //    第1~N-1帧:数据帧(文件内容)
    //    第N帧:结束帧
    //    CRC校验帧

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第8步:释放绝对路径内存
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    SoftBusFree(absSrcPath);

    return ret;
}

源码

复制代码
static int32_t FileToFrameAndSendFile(SendListenerInfo *sendInfo, const char *sourceFile, const char *destFile)
{
#define RETRY_READ_LOCK_TIMES 2
    TRANS_CHECK_AND_RETURN_RET_LOGE(sendInfo != NULL, SOFTBUS_INVALID_PARAM, TRANS_FILE, "invalid param.");
    if (!CheckDestFilePathValid(destFile)) {
        TRANS_LOGE(TRANS_FILE, "dest path is wrong. channelId=%{public}d", sendInfo->channelId);
        return SOFTBUS_FILE_ERR;
    }
    char *absSrcPath = (char *)SoftBusCalloc(PATH_MAX + 1);
    TRANS_CHECK_AND_RETURN_RET_LOGE(absSrcPath != NULL, SOFTBUS_MALLOC_ERR, TRANS_FILE, "calloc absFullDir failed");
    if (GetAndCheckRealPath(sourceFile, absSrcPath) != SOFTBUS_OK) {
        TRANS_LOGE(TRANS_FILE, "get src abs file fail. channelId=%{public}d", sendInfo->channelId);
        SoftBusFree(absSrcPath);
        return SOFTBUS_FILE_ERR;
    }
    uint64_t fileSize = 0;
    uint64_t frameNum = 0;
    if (GetAndCheckFileSize(absSrcPath, &fileSize, &frameNum, sendInfo->crc, sendInfo->packetSize) != SOFTBUS_OK) {
        TRANS_LOGE(TRANS_FILE,
            "absSrcPath size err. channelId=%{public}d, absSrcPath=%{private}s", sendInfo->channelId, absSrcPath);
        SoftBusFree(absSrcPath);
        return SOFTBUS_FILE_ERR;
    }
    TRANS_LOGI(TRANS_FILE,
        "channelId=%{public}d, srcPath=%{private}s, srcAbsPath=%{private}s, destPath=%{private}s, "
        "fileSize=%{public}" PRIu64 ", frameNum=%{public}" PRIu64,
        sendInfo->channelId, sourceFile, absSrcPath, destFile, fileSize, frameNum);
    int32_t fd = SoftBusOpenFile(absSrcPath, SOFTBUS_O_RDONLY);
    if (fd < 0) {
        TRANS_LOGE(TRANS_FILE, "open file fail. channelId=%{public}d", sendInfo->channelId);
        SoftBusFree(absSrcPath);
        return SOFTBUS_FILE_ERR;
    }
    if (TryFileLock(fd, SOFTBUS_F_RDLCK, RETRY_READ_LOCK_TIMES) != SOFTBUS_OK) {
        TRANS_LOGE(TRANS_FILE, "file is writing");
        SoftBusCloseFile(fd);
        SoftBusFree(absSrcPath);
        return SOFTBUS_FILE_ERR;
    }
    sendInfo->fd = fd;
    sendInfo->fileSize = fileSize;
    sendInfo->frameNum = frameNum;
    int32_t ret = FileToFrame(sendInfo, frameNum, destFile, fileSize);
    SoftBusFree(absSrcPath);
    TRANS_LOGI(TRANS_FILE, "send file end. channelId=%{public}d, ret=%{public}d", sendInfo->channelId, ret);
    return ret;
}

帧计算

GetAndCheckFileSize 是"算帧器" ------获取文件大小、检查是否超限,然后根据 packetSize 和 CRC 配置计算每帧有效载荷,用 文件大小 / 每帧有效载荷 向上取整算出数据帧数,再加 1 预留起始帧,得到总帧数。这个帧数决定了 FileToFrame 循环发送多少次。

职责:获取文件大小 → 检查是否超限 → 计算每帧有效载荷 → 计算总帧数。

流程

复制代码
static int32_t GetAndCheckFileSize(
    const char *sourceFile,    // 源文件路径
    uint64_t *fileSize,        // 输出:文件大小
    uint64_t *frameNum,        // 输出:总帧数
    int32_t crc,               // 是否开启CRC校验
    uint32_t packetSize)       // 分片大小(976 或 4048)
{
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第1步:获取文件大小
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    SoftBusGetFileSize(sourceFile, fileSize);
    //  fileSize = 10485760(10MB)

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第2步:检查文件是否过大
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    if (*fileSize > MAX_FILE_SIZE) {
        return SOFTBUS_FILE_ERR;   // 超过最大限制,拒绝发送
    }

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第3步:计算每帧有效载荷 ⭐
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    uint64_t oneFrameSize = packetSize - FRAME_DATA_SEQ_OFFSET;
    //  ↑ 减去帧序列号等固定协议头开销
    //  例如:976 - 48 = 928 字节

    if (crc == APP_INFO_FILE_FEATURES_SUPPORT) {
        oneFrameSize -= (FRAME_HEAD_LEN + FRAME_CRC_LEN);
        //  ↑ 开启CRC还要减去帧头和CRC校验的额外开销
        //  例如:928 - 4 - 4 = 920 字节
    }

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第4步:计算总帧数 ⭐⭐
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    frameNumTemp = (*fileSize) / oneFrameSize;      // 整除部分
    if (((*fileSize) % oneFrameSize) != 0) {
        frameNumTemp++;                              // 有余数,多1帧
    }
    //  例如:10485760 / 920 = 11397 余 640
    //  frameNumTemp = 11397 + 1 = 11398

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第5步:+1 预留起始帧
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    frameNumTemp++;
    //  ↑ 第0帧是起始帧(不发文件数据,发文件名、大小等元信息)
    //  所以总帧数 = 数据帧数 + 1
    //  frameNumTemp = 11398 + 1 = 11399

    *frameNum = frameNumTemp;
    return SOFTBUS_OK;
}

源码

复制代码
static int32_t GetAndCheckFileSize(
    const char *sourceFile, uint64_t *fileSize, uint64_t *frameNum, int32_t crc, uint32_t packetSize)
{
    if ((sourceFile == NULL) || (fileSize == NULL) || (frameNum == NULL)) {
        TRANS_LOGE(TRANS_FILE, "get file size num params invalid");
        return SOFTBUS_INVALID_PARAM;
    }

    if (SoftBusGetFileSize(sourceFile, fileSize) != SOFTBUS_OK) {
        TRANS_LOGE(TRANS_FILE, "get file size fail");
        return SOFTBUS_FILE_ERR;
    }

    if (*fileSize > MAX_FILE_SIZE) {
        TRANS_LOGE(TRANS_FILE, "file is too large, fileSize=%{public}" PRIu64, *fileSize);
        return SOFTBUS_FILE_ERR;
    }

    uint64_t oneFrameSize = packetSize - FRAME_DATA_SEQ_OFFSET;
    if (crc == APP_INFO_FILE_FEATURES_SUPPORT) {
        oneFrameSize -= (FRAME_HEAD_LEN + FRAME_CRC_LEN);
    }
    uint64_t frameNumTemp;
    if (oneFrameSize != 0) {
        frameNumTemp = (*fileSize) / oneFrameSize;
        if (((*fileSize) % oneFrameSize) != 0) {
            frameNumTemp++;
        }
    } else {
        TRANS_LOGE(TRANS_FILE, "there's division by zero risk");
        return SOFTBUS_FILE_ERR;
    }

    /* add 1 means reserve frame to send destFile string */
    frameNumTemp++;
    *frameNum = frameNumTemp;
    return SOFTBUS_OK;
}

例子

复制代码
文件大小:10000 字节
packetSize:976 字节
FRAME_DATA_SEQ_OFFSET:48 字节
开启CRC:FRAME_HEAD_LEN=4, FRAME_CRC_LEN=4

计算过程:
  oneFrameSize = 976 - 48 = 928
  开启CRC:oneFrameSize = 928 - 4 - 4 = 920

  数据帧数 = 10000 / 920 = 10 余 800
  余数不为0 → +1 = 11 帧(数据帧)

  起始帧 → +1 = 12 帧(总帧数)

帧分布:
  第0帧:起始帧(文件名、大小等元信息)    ← 不携带文件数据
  第1帧:920 字节数据
  第2帧:920 字节数据
  ...
  第10帧:920 字节数据
  第11帧:800 字节数据(最后一帧,不满)    ← 之前讲的末帧适配

为什么每帧有效载荷要减这么多

note

这里有个地方值得注意,filesize和framenum,由于C语言返回只能返回一个参数,通过return方式吧这两个值给调用者只有两种方式,一种是用结构体封装返回,还有一种是如上,传入参数直接把调用者对应局部变量的地址传进来,用解引号*直接让这个地址上的值变为指定的值

传输层处理

此处补充消息帧发送相关的后续

在文件发送完后会发送文件列表的帧给接收端核对完整性,在filelisttobuffer函数中会实现消息帧的设计和数据装载,但是我看到后面的传输层,前面的是业务层,传输层会决定处理分片策略和是否加密,在此做一个补充。

分片处理

分片打包会有两条路径,一条是TLV打包,一种是传统打包。这个打包加密的算法后面单独分析。

接收端接收到后会在缓冲区中将分片重组,解密拿到原始帧,再解析头部组装消息。

  • PacketHead:明文协议头,接收端先读它来识别帧
  • IV:AES-GCM 初始化向量(12字节)
  • 密文:原始数据加密后的结果
  • Tag:AES-GCM 认证标签(16字节),用于解密时验证完整性

疑问

看到这里,其实最核心的设计看的差不多了,但是有个问题,在这个位置我们只看到文件数据的发送,之前是了解到,软总线在处理文件互传的时候会创建两条会话通道,一条是消息通道,另一条是文件通道,++发送端和接收端是有发送进度同步的操作++的,这里没有看到任何相关的处理。

进度同步的两条路径

复制代码
路径1:本地回调(当前代码看到的)
  发送端每发一帧 → 回调通知本地应用进度
  接收端每收一帧 → 回调通知本地应用进度
  → 两端各自计算自己的进度,不需要互相通知

路径2:消息会话同步(file_share 层做的)
  发送端通过消息会话 → 发送进度消息给接收端
  接收端收到 → 更新UI显示
  → 这是应用层的额外逻辑,不在 SoftBus SDK 层

具体代码位置对比

复制代码
SoftBus SDK 层(我们看的):
  client_trans_proxy_file_manager.c
    └─ SendSingleFile()
         └─ FileToFrame()
              └─ 每发一帧后:
                   socketSendCallback(FILE_EVENT_SEND_PROGRESS)
                   ↑ 本地回调,通知本应用的进度
                   ↑ 不经过消息会话

file_share 应用层(上层):
  send_manager.cpp
    ├─ 文件会话:::SendFile()     → 走 SoftBus 文件通道
    └─ 消息会话:::SendMessage()  → 走 SoftBus 消息通道
         └─ 发送 JSON 进度消息:
              {"type":"progress", "bytesSent":5242880, "totalSize":10485760}
              ↑ 通过消息会话发给对端

消息回调设计

复制代码
SDK 层只提供 3 种回调事件:

① FILE_EVENT_SEND_PROGRESS   ← 每发一帧触发,报告累计进度
② FILE_EVENT_SEND_FINISH     ← 所有文件发完触发
③ FILE_EVENT_SEND_ERROR      ← 出错触发

❌ 没有 FILE_EVENT_SINGLE_FILE_FINISH

回调进度的数据

这里面每发送一帧回调的进度只有累计已发送的字节和所有文件总大小,没有单个文件大小,这个靠应用层自己推断。

复制代码
// 每发一帧后触发
FileEvent event = {
    .type = FILE_EVENT_SEND_PROGRESS,
    .bytesProcessed = sendInfo->totalInfo.bytesProcessed,  // ← 累计已发送字节
    .bytesTotal = sendInfo->totalInfo.bytesTotal,          // ← 所有文件总大小
};
socketSendCallback(sessionId, &event);

为什么 SoftBus SDK 层不需要消息会话?

复制代码
SoftBus SDK 是底层传输引擎:
  • 只负责把数据从A搬到B
  • 进度通过本地回调通知就够了
  • 不关心业务逻辑(进度同步、取消指令等)

file_share 是业务应用层:
  • 需要两端进度同步(用户看到的进度条)
  • 需要控制消息(取消、暂停、重试)
  • 所以开了消息会话来传这些业务数据

完整架构

相关推荐
charlee441 个月前
Unity项目适配华为鸿蒙系统的原生库加载问题排查与解决
华为·unity3d·鸿蒙·cmake·c/c++·relro
YYYing.1 个月前
【C++大型项目之高性能服务器框架 (一) 】一切物语的开始:日志系统&配置系统篇
服务器·高并发·高性能·c/c++·后端框架
小六学编程1 个月前
二分查找详解:从普通二分到左右边界
算法·c/c++
小六学编程1 个月前
Linux 下 gcc / g++ 编译过程详解:从编译到链接
linux·c/c++
daidaidaiyu1 个月前
嵌入式开发入门知识
c/c++
拾光Ծ2 个月前
【Linux系统】线程(上)
java·linux·运维·jvm·线程·c/c++
故事还在继续吗2 个月前
高性能网络
服务器·网络·c/c++
YYYing.2 个月前
【C++项目之高并发内存池 (三)】万字解析CentralCache与PageCache的初步实现
c++·笔记·哈希算法·高并发·c/c++·内存池