前言

应用端调用的是底层封装好的接口,这里分析就是从js接口开始往下分析。

第一层:Napi

Js调用的发送文件的函数叫SendFile,由napi层封装提供,在napi会解析js传入的参数并转换成C++参数,唤起真正的业务调度入口

  return SendFileInner(env, type, deviceId, files); 

第二层:业务调度入口

构造业务对象task,交给业务层

枚举名 含义 接收端行为
-1 UNKNOWN 无效/未指定 不应出现,作为默认值或错误标记
1 SAVE_TO_MEDIA_ASSET 媒体资产模式 文件接收后自动导入系统媒体库(相册、视频库等),JS 侧可通过媒体库 API 访问
2 SAVE_AS_FILE 单文件模式 文件接收后保存为独立文件,存放在 ShareStoragePath 目录下
10 SAVE_AS_FOLDER 文件夹模式 多个文件打包为一个文件夹传输,需要提供 folderName
SendTask task {};
    task.type = type;
    task.deviceId = deviceId;
    task.folderName = folderName;
    task.files = std::move(files);

第三层:业务层

客户端

参数校验,通过IPC将任务丢给服务端

int32_t ShareManager::SendFile(SendTask &task)
{
    // ① 校验:deviceId 和文件列表不能为空
    if (task.deviceId.empty() || task.files.empty()) {
        return FS_INVALID_PARAM;
    }

    // ② 校验:ShareType 必须是合法值(1/2/10)
    int32_t ret = CheckShareType(task.type);
    if (ret != FS_SUCCESS) {
        return ret;
    }

    // ③ 校验:文件夹模式必须有文件夹名
    if (task.type == ShareType::SAVE_AS_FOLDER && task.folderName.empty()) {
        return FS_INVALID_PARAM;
    }

    // ⭐ 核心:通过 IPC 代理转发给 SA 服务端
    ret = FileShareClient::GetInstance().SendFile(task);
    if (ret != FS_SUCCESS) {
        return ret;
    }

    return FS_SUCCESS;
}

在整个链路的位置

NAPI 层: NapiFileShare::SendFile()      ← JS 参数拆包
    │
    ▼
NAPI 层: NapiFileShare::SendFileInner()  ← 构造 SendTask
    │
    ▼
┌──────────────────────────────────────────┐
│ Inner API: ShareManager::SendFile()      │ ← 🎯 你在这里
│                                          │
│ 职责:参数校验 + IPC 转发                  │
│ 不做任何实际传输!                          │
│                                          │
│ FileShareClient::GetInstance().SendFile() │ ← IPC 代理调用
└──────────────────────────────────────────┘
    │  (跨进程 IPC)
    ▼
SA 服务端: FileShareService::SendFile()   ← 服务端接收
    │
    ▼
SA 服务端: SendManager::SendFile()        ← 真正的传输逻辑

三层校验的意义

校验 为什么需要
deviceId 非空 没有目标设备,无法建立 SoftBus 会话
files 非空 没有文件可传,调用无意义
CheckShareType 防止 JS 传入非法 type(如 0、3、999)
folderName 非空 文件夹模式需要知道文件夹名,否则接收端无法创建目录

这里开始换一种解读方式,设计到进程进通讯,有一点复杂。

上面的最后一句是FileShareClient::SendFile。实现如下

int32_t FileShareClient::SendFile(SendTask &task)
{
    LOGD("SendFile start.");
    auto ret = GetRemoteProxy();
    if (ret != FS_SUCCESS) {
        LOGE("GetRemoteProxy failed, ret : %{public}d", ret);
        return ret;
    }
    CHKPTR(fileshareProxy_, FS_NULL_PARAM);
    return fileshareProxy_->SendFile(task);
}

GetRemoteProxy()是负责获取代理,首先讲讲代理的作用,你给代理发送一条指令,代理自动按照指令执行所有操作,比如序列化,打包,通过binder将task发送给服务进程。这里获取代理操作,首先会检查缓存,如果已经有代理了,直接复用对象,如果没有,则会到SystemAbilityManager(open Harmony的能力注册管理)去通过SA id查询服务,查到了会创建死亡监听器挂到远程对象上(当服务端代理崩溃的时候binder驱动会自动触发回调将代理置空),如果没查到服务可能是软总线还没启动,会等200ms重试,一共尝试3次。

如果以上都成功了,就会触发代理的发送程序,代理会将要发送的c++数据序列化成json,打包发送给服务端

int32_t FileShareProxy::SendFile(SendTask &task)
{
    LOGD("SendFile start.");
    MessageParcel data;
    MessageParcel reply;
    MessageOption option;

    sptr<IRemoteObject> remote = Remote();
    CHKPTR(remote, FS_NULL_PARAM);

    nlohmann::json jsonObject;
    SendTaskToJson(task, jsonObject);
    std::string taskStr = jsonObject.dump();
    data.WriteString(taskStr);

    int32_t ret = remote->SendRequest(SEND_FILE, data, reply, option);
    if (ret != FS_SUCCESS) {
        LOGE("sendRequest failed, ret:%{public}d", ret);
        return FS_PROXY_SEND_REQUEST_FAILED;
    }

    int32_t errorcode = reply.ReadInt32();

    return errorcode;
}

这里的remote是binder对象。这里的序列化使用的是nlohmann/json库,手动逐字段填写。这个后面再分析他的技术。

binder发送数据

remote->SendRequest(SEND_FILE, data, reply, option)

binder将序列化好的数据发送给服务端

IRemoteObject (纯虚函数)
    │  virtual int SendRequest(...) = 0;
    │
    ├── IPCObjectProxy   ← 客户端侧实现(你调用的就是这个)
    │     int SendRequest(...) override;
    │
    └── IPCObjectStub    ← 服务端侧实现(接收请求)
          int SendRequest(...) override;

三层调用链

// ━━━━━━━━━━ 第1层:入口(155行)━━━━━━━━━━
int IPCObjectProxy::SendRequest(uint32_t code, MessageParcel &data, 
                                 MessageParcel &reply, MessageOption &option)
{
    // ① 校验命令码合法性
    if (code != DUMP_TRANSACTION && code > MAX_TRANSACTION_ID) {
        return IPC_PROXY_INVALID_CODE_ERR;
    }
    
    // ② 记录开始时间(用于慢调用监控)
    auto beginTime = std::chrono::steady_clock::now();
    
    // ③ ⭐ 真正的发送逻辑
    int err = SendRequestInner(false, code, data, reply, option);
    
    // ④ 慢调用告警(超过阈值打印警告)
    auto endTime = std::chrono::steady_clock::now();
    auto timeInterval = duration_cast<milliseconds>(endTime - beginTime).count();
    if (timeInterval > SEND_REQUEST_TIMEOUT) {
        ZLOGW("IPC cost %lld ms, code:%u", timeInterval, code);
    }
    
    return err;
}

// ━━━━━━━━━━ 第2层:核心调度(199行)━━━━━━━━━━
int IPCObjectProxy::SendRequestInner(bool isLocal, uint32_t code, 
    MessageParcel &data, MessageParcel &reply, MessageOption &option)
{
    // ① 检查代理对象是否已死
    if (IsObjectDead()) {
        return ERR_DEAD_OBJECT;
    }

    // ② 选择 Invoker(调用器)
    IRemoteInvoker *invoker = nullptr;
    if (isLocal) {
        invoker = IPCThreadSkeleton::GetDefaultInvoker();   // 本地调用
    } else {
        invoker = IPCThreadSkeleton::GetRemoteInvoker(proto_); // ⭐ 远程调用(走Binder)
    }
    
    // ③ ⭐⭐⭐ 真正进入 Binder 驱动 ⭐⭐⭐
    int status = invoker->SendRequest(handle_, code, data, reply, option);
    
    // ④ 如果服务端死了,标记代理为死亡
    if (status == ERR_DEAD_OBJECT) {
        SetObjectDied(true);
    }
    
    return status;
}

第三层:invoker->sendrequest()进入内核

invoker->SendRequest(handle_, code, data, reply, option)
    │
    │  这个 invoker 的实际类型是 IPCThreadSkeleton 内部的
    │  具体实现类,最终会调用到:
    │
    ▼
ioctl(binderFd, BINDER_WRITE_READ, ...)   ← 系统调用!
    │
    │  从这里进入 Linux 内核的 binder 驱动
    │  数据从用户空间拷贝到内核空间
    │  再从内核空间唤醒服务端线程读取
    │
    ▼  内核代码(不在你的源码中,在 Linux 内核里)
  binder_ioctl()
    → binder_thread_write()
    → binder_transaction()    ← 把数据从客户端进程拷贝到服务端进程
    → 唤醒服务端等待线程

到这里客户端的流程就走完了。接下来再是服务端。

回顾

回顾一下完整的客户端链路

✅ 已看完的客户端流程
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

JS 层:   mgr.sendFile(deviceId, type, files)
           │
           ▼
NAPI 层:  NapiFileShare::SendFile()          ← JS参数拆包
           │
           ▼
NAPI 层:  NapiFileShare::SendFileInner()     ← 构造SendTask
           │
           ▼
Inner API: ShareManager::SendFile()          ← 参数校验
           │
           ▼
IPC 层:   FileShareClient::SendFile()        ← 获取代理
           │  └─ GetRemoteProxy()            ← 查号+缓存+死亡监听
           ▼
IPC 层:   FileShareProxy::SendFile()         ← 序列化+跨进程
           │  └─ SendTask → JSON → MessageParcel
           ▼
IPC 框架:  IPCObjectProxy::SendRequest()     ← 选择Invoker
           │
           ▼
内核:     ioctl() → binder 驱动              ← 跨进程传输
           │
           ══════════════════════════════════
           │  以下是服务端,还没看
           ▼
❓ SA 服务端: FileShareStub::OnRemoteRequest()
❓ SA 服务端: FileShareService::SendFile()
❓ 核心逻辑:  SendManager::SendFile()        ← 真正的文件传输
❓ 软总线:    SoftBus OpenSession/SendFile

客户端总结

函数 一句话
NAPI SendFile 拆 JS 参数的快递员
NAPI SendFileInner 构造任务单的调度员
Inner API ShareManager::SendFile 校验参数的门卫
IPC Client FileShareClient::SendFile 拨号连线的接线员
IPC Proxy FileShareProxy::SendFile 打包投递的快递员
IPC 框架 IPCObjectProxy::SendRequest 选路发送的物流公司
内核 binder 驱动 修好的公路

客户端的本质就是:翻译参数 → 校验 → 序列化 → 跨进程投递,没有任何实际传输逻辑。

服务端

收到回调

binder发送到服务端后,服务端设置的回调函数触发,OnRemoteRequest,这个是服务端的命令分发器

int32_t FileShareStub::OnRemoteRequest(uint32_t code, MessageParcel& data, 
                                        MessageParcel &reply, MessageOption &option)
{
    // 在 baseFuncs_ 表中查找命令码对应的处理函数
    auto itFunc = baseFuncs_.find(code);
    if (itFunc != baseFuncs_.end()) {
        auto memberFunc = itFunc->second;
        if (memberFunc != nullptr) {
            return (this->*memberFunc)(data, reply);  // ⭐ 调用对应的Stub函数
        }
    }
    // 找不到就走默认处理
    return IPCObjectStub::OnRemoteRequest(code, data, reply, option);
}
FileShareStub::FileShareStub()
{
    baseFuncs_[SEND_FILE]                          = &FileShareStub::SendFileStub;
    baseFuncs_[REGISTER_FILE_SHARE_CALLBACK]       = &FileShareStub::RegisterFileShareCallbackStub;
    baseFuncs_[UNREGISTER_FILE_SHARE_CALLBACK]     = &FileShareStub::UnRegisterFileShareCallbackStub;
    baseFuncs_[CANCEL_SEND_FILE]                   = &FileShareStub::CancelSendFileStub;
    baseFuncs_[SET_SHARE_STATUS]                   = &FileShareStub::SetShareStatusStub;
    // ... 共13个命令
}

反序列化

int32_t FileShareStub::SendFileStub(MessageParcel &data, MessageParcel &reply)
{
    // 1. 从 Parcel 读出 JSON 字符串(Proxy 那边 WriteString 写入的)
    std::string taskStr = data.ReadString();
    
    // 2. 解析 JSON
    nlohmann::json jsonObject = nlohmann::json::parse(taskStr);
    if (jsonObject.is_discarded()) {
        return FS_INVALID_PARAM;  // JSON 解析失败
    }
    
    // 3. JSON → SendTask(与 Proxy 的 SendTaskToJson 完全相反)
    SendTask task;
    JsonToSendTask(jsonObject, task);

    // 4. ⭐ 调用业务层
    auto ret = SendFile(task);  // 虚函数 → FileShareService::SendFile()
    
    // 5. 把结果写回 reply(客户端 Proxy 那边 reply.ReadInt32() 读取的)
    reply.WriteInt32(ret);
    return ret; 
}

分享文件服务层

这里面从IPC获取调用者的token,这个不是由app发送的,这个由linux内核提供,防止app篡改token访问别的用户信息。获取到token后会将沙盒路径转换成物理实际路径

int32_t FileShareService::SendFile(SendTask &task)
{
    Security::AccessToken::HapTokenInfo hapTokenInfo;
    int ret = Security::AccessToken::AccessTokenKit::GetHapTokenInfo(IPCSkeleton::GetCallingTokenID(), hapTokenInfo);
    if (ret != ERR_OK) {
        LOGE("GetHapTokenInfo error code: %{public}d", ret);
        return FS_GET_USER_ID_FAILED;
    }
    for (auto&& file : task.files) {
        ret = AppFileService::SandboxHelper::GetPhysicalPath(file.uri, std::to_string(hapTokenInfo.userID), file.path);
        if (ret != ERR_OK) {
            LOGE("GetPhysicalPath error code: %{public}d", ret);
            return FS_URI_TO_PATH_FAILED;
        }
    }
    return SendManager::GetInstance().SendFile(task);
}

发送管理器

文件传输的真正起点,经过4重检验以后,将任务放入任务队列。这里值得了解的地方我觉得就是获取设备网络地址那一个环节,一个设备在软总线中组网有两个id,一个是deviceID用来标识设备,还有一个networkID用来标识网络地址,两者合起来才是一个完整的对象。至于把任务丢到队列的操作,太老套了,没有什么新东西。

int32_t SendManager::SendFile(SendTask &task)
{
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第1步:参数校验(和客户端类似,双重保险)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    if (task.deviceId.empty() || task.files.empty()) {
        return FS_INVALID_PARAM;
    }
    if (task.type == ShareType::SAVE_AS_FOLDER && task.folderName.empty()) {
        return FS_INVALID_PARAM;
    }

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第2步:校验文件路径是否合法、文件是否真实存在
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    int32_t ret = CheckFilePaths(task.files);
    //  内部做了3件事:
    //  ① realpath() — 解析路径,防止路径穿越攻击(如 "../../etc/passwd")
    //  ② stat()     — 检查文件是否真实存在
    //  ③ S_ISREG()  — 确认是普通文件(不是目录/设备文件/软链接)
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第3步:获取目标设备的网络ID ⭐
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    ret = GetNetWorkId(task);
    //  内部做了:
    //  DeviceManager::GetTrustedDeviceList() → 获取可信设备列表
    //  从中找到目标设备的 networkId 和 deviceName
    //  networkId 是 SoftBus 建立会话用的标识(不是 deviceId!)
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第4步:任务入队 ⭐⭐⭐
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    ret = sendQueue_->Push(task, true);
    //  把任务推入发送队列
    //  后台线程 ProcessThread() 会从队列中 Pop 出任务
    //  然后调用 OpenSessionAndSendFile(task) 真正开始传输
    
    return FS_SUCCESS;
}

 到这里就是最核心的软总线的工作了,之前的文章中分析过,根据任务分类,软总线会创建两个会话通道,一个负责消息发送,就是不如我们下载安装包的时候显示下载了百分之多少,就发那个进度,还有一个是负责发送文件。像文件发送的话按道理根据我的了解会进行分片,有点像tcp的数据流一样,具体的我还没看,但是思路很相似,每个分片因该都会有标识,还会对分片进行md5或者其他的验证,验证文件的完整性,可能也实现了重传机制,这些方面就是我对这个源码最好奇的地方,我也想看看他是怎么设计的。

后台线程

这个线程的任务就是只要发送不停止,就会从队列弹出一个任务,根据任务的类型去进行操作,就是一个调度器

void SendManager::ProcessThread()
{
    while (!isStop_) {
        SendTask task {};
        int ret = sendQueue_->Pop(task);
        if (ret == FS_QUEUE_STOPED) {
            LOGI("send queue stoped.");
            break;
        } else if (ret != FS_SUCCESS) {
            LOGE("send queue pop failed.");
            continue;
        }
        
        ret = OpenSessionAndSendFile(task);
        if (ret != FS_SUCCESS) {
            LOGE("OpenSessionAndSendFile failed.");
            NotifyEventInner(SendState::FAILURE, task.type);
            continue;
        }
    }
}

传递文件前打招呼

在传递文件之前会建立消息通道,传入sessionid(会话通道ID),message.data()(会话消息地址),message.size()(会话消息长度)

int ret = ::SendMessage(sessionId, message.data(), message.size());
//       ↑              │          │              │
//       全局作用域符     │          │              └─ 消息长度(字节数)
//       表示调用SoftBus  │          └─ 消息内容指针
//       的C函数,不是    └─ 会话ID
//       类成员函数
┌─────────────────────────────────────────────────────────────┐
│  SyncSendTask(task)                                         │
│                                                              │
│  ① 获取本机设备信息                                           │
│  ② OpenSession(TYPE_MESSAGE)  → 建立消息通道                 │
│  ③ 等待会话建立成功                                           │
│  ④ SendMessage(JSON)          → 发送任务信息给对端            │
│  ⑤ 等待对端确认接收                                           │
│  ⑥ CloseSession()             → 关闭消息通道                 │
└─────────────────────────────────────────────────────────────┘
int32_t SendManager::SyncSendTask(SendTask task)
{
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第1步:获取本机设备信息
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    DistributedHardware::DmDeviceInfo localDeviceInfo;
    DeviceManager::GetInstance().GetLocalDeviceInfo(PKG_NAME, localDeviceInfo);
    //  ↑ 拿到自己的 networkId、deviceId、deviceName
    //  对端需要知道"是谁发的"

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第2步:建立消息通道(不是文件通道!)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    SessionAttribute sessionAttr = {
        .dataType = TYPE_MESSAGE,              // ⭐ 消息类型,不是文件类型!
        .linkTypeNum = 2,
        .linkType[0] = LINK_TYPE_WIFI_P2P,    // WiFi 直连
        .linkType[1] = LINK_TYPE_WIFI_WLAN_5G, // WiFi 5G
    };
    int sessionId = ::OpenSession(
        SESSION_NAME_CLIENT,    // 自己的会话名
        SESSION_NAME_SERVICE,   // 对端的会话名
        task.networkId.c_str(), // 目标设备网络地址
        GROUP_ID,               // 组ID
        &sessionAttr            // 会话属性
    );

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第3步:等待会话建立成功
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    g_syncResult.clear();
    g_syncCondition.wait_for(lock, timeout, [] { return !g_syncResult.empty(); });
    //  ↑ 等对端接受连接,超时则失败

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第4步:发送任务信息(JSON格式)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    task.networkId  = localDeviceInfo.networkId;   // 替换为自己的网络ID
    task.deviceId   = localDeviceInfo.deviceId;    // 替换为自己的设备ID
    task.deviceName = localDeviceInfo.deviceName;  // 替换为自己的设备名
    //  ↑ 对端收到后就知道"是谁发的",可以回传
    
    nlohmann::json jsonObject;
    SendTaskToJson(task, jsonObject);       // SendTask → JSON
    std::string message = jsonObject.dump(); // JSON → 字符串
    ::SendMessage(sessionId, message.data(), message.size());
    //  ↑ 通过 SoftBus 消息通道发送

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第5步:等待对端确认
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    g_syncResult.clear();
    g_syncCondition.wait_for(lock, timeout, [] { return !g_syncResult.empty(); });
    //  ↑ 等对端回复"我准备好了,开始传文件吧"

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第6步:关闭消息通道
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    ::CloseSession(sessionId);
    
    return g_syncResult == SYNC_SEND_TASK_SUCCESS ? FS_SUCCESS : FS_SEND_FILE_FAILED;
}

文件传输总指挥

整个文件传输的最底层、最核心、最关键的实现

OpenSessionAndSendFile(task)
  │
  ├─ ① SyncSendTask(task)              ← 打招呼,上面分析过了
  │     ├─ OpenSession(TYPE_MESSAGE)    ← 消息通道
  │     ├─ SendMessage(JSON)            ← 发任务信息
  │     ├─ 等对端确认
  │     └─ CloseSession()               ← 关闭消息通道
  │
  ├─ ② 准备文件路径列表
  │
  ├─ ③ OpenSession(TYPE_FILE)           ← 建立文件通道
  │     └─ sessionInfoMap_send_ 保存会话信息
  │
  ├─ ④ 等待文件通道建立(轮询 g_openFlag_send)
  │     └─ OnSessionOpened 回调设置 g_openFlag_send = true
  │
  └─ ⑤ ::SendFile(sessionId, files, null, count)  ← ⭐ 调用软总线C API正式传文件!
        │
        ▼
     SoftBus 底层传输
        │
        ├─ 文件分片
        ├─ WiFi P2P / WLAN 传输
        ├─ 进度回调 OnSendEvent
        └─ 完成回调 OnFileSendFinished

这里的auto &&file:task.files可以了解下,这里也是这个项目的比较好的地方,auto&&是万能引用,既能存左值,也能存右值。另外c++的引用和c的指针有区别,c的指针存放的是地址,引用是别名,其实就是task.files[num]本身。

int32_t SendManager::OpenSessionAndSendFile(SendTask &task)
{
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第1步:先打招呼(SyncSendTask),上面分析过了
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    int ret = SyncSendTask(task);
    //  → OpenSession(TYPE_MESSAGE)  建立消息通道
    //  → SendMessage(JSON)          发送任务信息
    //  → 等对端确认                  对端准备好了
    //  → CloseSession()             关闭消息通道
    if (ret != FS_SUCCESS) {
        return ret;  // 打招呼失败,不传了
    }

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第2步:准备文件路径列表
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    std::vector<const char *> sourceFileList;
    for (auto&& file : task.files) {
        sourceFileList.push_back(file.path.data());
        //  收集所有文件的真实物理路径
        //  如:["/data/photos/photo1.jpg", "/data/photos/photo2.jpg"]
    }

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第3步:建立文件通道(TYPE_FILE)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    SessionAttribute sessionAttr = {
        .dataType = TYPE_FILE,                  // ⭐ 文件类型(不是消息!)
        .linkTypeNum = 2,
        .linkType[0] = LINK_TYPE_WIFI_P2P,     // WiFi 直连
        .linkType[1] = LINK_TYPE_WIFI_WLAN_5G, // WiFi 5G
    };
    int sessionId = ::OpenSession(
        SESSION_NAME_CLIENT,       // 自己的会话名
        SESSION_NAME_SERVICE,      // 对端的会话名
        task.networkId.c_str(),    // 目标设备
        GROUP_ID,
        &sessionAttr
    );

    // 保存会话信息(后续回调要用)
    SessionInfo sessionInfo = {
        .sessionId = sessionId,
        .networkId = task.networkId,
        .sendFlag = true,          // 标记正在发送
        .type = task.type,
        .files = task.files,
    };
    sessionInfoMap_send_[task.deviceId] = sessionInfo;

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第4步:等待文件通道建立(轮询方式)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    g_openFlag_send.store(false, std::memory_order_relaxed);  // 重置标志
    
    while (g_openFlag_send == false && retryTimes < MAX_RETRY) {
        sleep_for(重试间隔);
        retryTimes++;
    }
    //  ↑ 与 SyncSendTask 用 wait_for 不同!
    //    这里用轮询(sleep + 检查标志位)
    //    OnSessionOpened 回调中会设置 g_openFlag_send = true

    if (retryTimes == MAX_RETRY) {
        return FS_OPEN_SESSION_TIME_OUT;  // 超时
    }

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 第5步:⭐⭐⭐ 正式发送文件!
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    ret = ::SendFile(sessionId, sourceFileList.data(), nullptr, sourceFileList.size());
    //                    │            │              │            │
    //                    会话ID       文件路径数组    接收端路径     文件数量
    //                                 (char**数组)   (null=默认)   
    if (ret != FS_SUCCESS) {
        ::CloseSession(sessionId);
        sessionInfoMap_send_[task.deviceId].sendFlag = false;
        return FS_SEND_FILE_FAILED;
    }

    gettimeofday(&send_start_time, nullptr);  // 记录开始时间(算速度用)
    return FS_SUCCESS;
}

这里的sessionattr和sessioninfo采用的是C99结构体语法,和原来有所不同,原来的c初始化结构体或者数组必须要按照顺序全部初始化(C++20也开始正式支持,但不能改变顺序,但如果不是严格遵守规范,编译器扩展已经支持了)也不用在乎顺序,剩余的会自动补0。

g_openFlag_send.store(false, std::memory_order_relaxed);这一句值得深挖一下,g_openFlag_send是一个全局原子变量,是是否开启会话的标识符,常规的重置为false是直接让他等于false就行了(他其实也会调用store,内存序采用最高的seq_cst),但是这里实现是显示指定内存序,因为这里只需要保证所有线程读取到的标识符是一样的就行了,这样做性能最好。

六种内存序(从弱到强)

relaxed        ← 最快,只保证原子性,不保证可见性顺序
release        ← 写操作:本线程之前的写,对其他线程可见
acquire        ← 读操作:之后的读,能看到其他线程的写
acq_rel        ← acquire + release
seq_cst        ← 最慢,全局顺序一致(默认)
                  所有线程看到的操作顺序完全一样

本项目的使用场景

// 835行:重置标志
g_openFlag_send.store(false, std::memory_order_relaxed);
//  ↑ 只需要写入 false,不需要保证其他操作的顺序
//  用 relaxed 足够

// 856行:轮询检查
while (g_openFlag_send.load(std::memory_order_acquire) == false)
//  ↑ 需要确保读到 true 之后,后续的 SendFile 能看到正确的数据
//  用 acquire 保证

// 321行:回调中设置
g_openFlag_send.store(true, std::memory_order_relaxed);
//  ↑ 只需要写入 true
//  用 relaxed 足够

为什么store用relaxed就够了

因为这里 flag 本身就是一个简单的信号:

store(false) → "会话还没开"
store(true)  → "会话开了"

不需要保证 flag 和其他变量之间的顺序关系
只需要保证写入本身是原子的(不会写一半被读走)
relaxed 就够了

如果用默认的seq_cst会怎样

// 用默认 seq_cst
g_openFlag_send = false;
// 内部会插入内存屏障指令
// 在 x86 上是 MFENCE 指令
// 在 ARM 上是 DMB 指令
// → 多花几十个 CPU 周期
// → 但这里完全不需要这个保证

// 用 relaxed
g_openFlag_send.store(false, std::memory_order_relaxed);
// 不插入内存屏障
// 只是一个普通的原子写入
// → 最快

softbus SDK层

做一系列校验,把文件交到通道层处理(/foundation/communication/dsoftbus/sdk/transmission/session/src/client_trans_message_service.c)

int SendFile(int sessionId, const char *sFileList[], const char *dFileList[], uint32_t fileCnt)
//               │               │                               │                │
//               会话ID          发送端文件路径                接收端存储路径     文件数量
//                               (源)                            (目的)
参数 含义 例子
sessionId 会话ID 123
sFileList 发送端文件路径数组 ["/data/photo1.jpg", "/data/photo2.jpg"]
dFileList 接收端存储路径数组 NULL(让对端自己决定存哪)
fileCnt 文件数量 2

流程

SendFile(sessionId, sFileList, dFileList, fileCnt)
  │
  ├─ ① 参数校验:sFileList 不能为空,fileCnt 不能为0
  │
  ├─ ② 权限检查:CheckPermissionState(sessionId)
  │     → 这个会话有没有发送文件的权限?
  │
  ├─ ③ 文件协议检查:CheckFileSchema()
  │     → 文件路径是否符合 schema 规范?
  │     → 设置 schema 回调
  │
  ├─ ④ 查找通道:ClientGetChannelBySessionId()
  │     → sessionId → channelId + 通道类型
  │     → 会话是建立在通道上的,需要找到底层通道
  │
  ├─ ⑤ 业务类型校验:ClientGetChannelBusinessTypeBySessionId()
  │     → 必须是 BUSINESS_TYPE_FILE 才能发文件
  │     → 不能用消息通道发文件!
  │
  └─ ⑥ ⭐ 真正发送:ClientTransChannelSendFile()
        → 交给通道层处理
        → 根据 type 选择 Proxy 通道或 UDP 通道
关键概念:Session 和 Channel 的关系
Session(会话)    ← 对外暴露的抽象,开发者看到的
    │
    │  一对一映射
    ▼
Channel(通道)    ← 内部实现,真正传输数据的管道

sessionId = 123
    │
    └─→ channelId = 456, type = PROXY_CHANNEL
                              │
                              ▼
                        Proxy 通道(WiFi P2P 传输)
内部路由
ClientTransChannelSendFile(channelId, type, sFileList, dFileList, fileCnt)
  │
  ├─ type == PROXY_CHANNEL → ClientTransProxyChannelSendFile()
  │                          → WiFi P2P / WLAN 文件传输
  │
  └─ type == UDP_CHANNEL   → ClientTransUdpChannelSendFile()
                              → UDP 文件传输
与 file_share 调用的对应关系
file_share 层:
  ::SendFile(sessionId, sourceFileList.data(), nullptr, sourceFileList.size())
       │
       ▼
SoftBus SDK 层(这个函数):
  SendFile(sessionId, sFileList, dFileList, fileCnt)
       │
       ├─ 校验权限、通道、业务类型
       └─ ClientTransChannelSendFile(...)
              │
              ▼
SoftBus 通道层:
  ClientTransProxyChannelSendFile(...)
       │
       ▼
WiFi P2P / WLAN 实际传输
路由函数

/foundation/communication/dsoftbus/sdk/transmission/trans_channel/manager/src/client_trans_channel_manager.c

int32_t ClientTransChannelSendFile(int32_t channelId, int32_t channelType, const char *sFileList[],
    const char *dFileList[], uint32_t fileCnt)
{
    int32_t ret = SOFTBUS_OK;
    switch (channelType) {
        case CHANNEL_TYPE_UDP:
            ret = TransUdpChannelSendFile(channelId, sFileList, dFileList, fileCnt);
            break;
        case CHANNEL_TYPE_PROXY:
            ret = TransProxyChannelSendFile(channelId, sFileList, dFileList, fileCnt);
            break;
        default:
            TRANS_LOGE(TRANS_FILE, "unsupport channelType=%{public}d.", channelType);
            return SOFTBUS_TRANS_CHANNEL_TYPE_INVALID;
    }
    return ret;
}

链路

file_share 层:
  ::OpenSession(linkType={P2P, WLAN_5G})
       ↓
SoftBus Lane Manager(链路选择)
  ├── 尝试 P2P → 成功?→ 用 P2P 链路
  └── P2P 失败?→ 尝试 WLAN 5G → 用 WLAN 链路
       ↓
SoftBus Channel Manager(通道选择)
  ├── channelType == PROXY → ClientTransProxyChannelSendFile()
  └── channelType == UDP   → ClientTransUdpChannelSendFile()
       ↓
实际数据流:
  [文件数据] → [Proxy/UDP通道] → [P2P/WLAN链路] → 对端设备

softbus会自动选择

场景1:两台手机面对面传文件
  → WiFi P2P 可用 → 选 P2P → Proxy/UDP 通道走 P2P 链路
  → 最快最稳定

场景2:两台手机在同一 WiFi 下,但距离较远
  → P2P 信号弱或不可用 → 降级到 WLAN 5G
  → 通过路由器中转

场景3:没有 WiFi,只有蓝牙
  → 两者都不可用 → 传输失败
走proxy链路

获取文件在接收端待存放的名字,如果没有,从发送端文件路径获截取文件名,还没有就走异常。如果有接收端传过来的名字,就用接收端的。

int32_t TransProxyChannelSendFile(int32_t channelId, const char *sFileList[], const char *dFileList[], uint32_t fileCnt)
{
    if (sFileList == NULL || fileCnt == 0 || fileCnt > MAX_SEND_FILE_NUM) {
        TRANS_LOGE(TRANS_SDK, "input para failed! fileCnt=%{public}" PRIu32, fileCnt);
        return SOFTBUS_INVALID_PARAM;
    }
    const char **remoteFiles = NULL;
    const char **generatedRemoteFiles = NULL;
    if (dFileList == NULL) {
        generatedRemoteFiles = GenerateRemoteFiles(sFileList, fileCnt);
        if (generatedRemoteFiles == NULL) {
            return SOFTBUS_FILE_ERR;
        }
        remoteFiles = generatedRemoteFiles;
    } else {
        remoteFiles = dFileList;
    }
    int32_t ret = ProxyChannelSendFile(channelId, sFileList, remoteFiles, fileCnt);
    if (generatedRemoteFiles != NULL) {
        SoftBusFree(generatedRemoteFiles);
        generatedRemoteFiles = NULL;
    }
    return ret;
}
开启通道发送

根据通道id创建或持有通道锁,发送文件,同一时间只能有一个线程持有,发送完后解锁,释放锁对象(对象不一定是直接销毁,内部有引用计数,归零才销毁)

int32_t ProxyChannelSendFile(int32_t channelId, const char *sFileList[], const char *dFileList[], uint32_t fileCnt)
{
    TRANS_LOGI(TRANS_FILE, "proxy send file trans start");
    if (fileCnt == 0 || fileCnt > MAX_SEND_FILE_NUM) {
        TRANS_LOGE(TRANS_FILE, "sendfile arg filecnt=%{public}u error", fileCnt);
        return SOFTBUS_INVALID_PARAM;
    }
    if (sFileList == NULL || !IsValidFileString(sFileList, fileCnt, MAX_FILE_PATH_NAME_LEN)) {
        TRANS_LOGE(TRANS_FILE, "sendfile invalid arg sFileList");
        return SOFTBUS_INVALID_PARAM;
    }
    if (dFileList == NULL || !IsValidFileString(dFileList, fileCnt, MAX_FILE_PATH_NAME_LEN)) {
        TRANS_LOGE(TRANS_FILE, "sendfile invalid arg dFileList");
        return SOFTBUS_INVALID_PARAM;
    }

    ProxyFileMutexLock *sessionLock = GetSessionFileLock(channelId);
    TRANS_CHECK_AND_RETURN_RET_LOGE(sessionLock != NULL, SOFTBUS_LOCK_ERR, TRANS_FILE, "get file lock failed");
    if (SoftBusMutexLock(&sessionLock->sendLock) != SOFTBUS_OK) {
        TRANS_LOGE(TRANS_FILE, "proxy send file lock file mutex failed");
        DelSessionFileLock(sessionLock);
        return SOFTBUS_LOCK_ERR;
    }

    int32_t ret = HandleFileSendingProcess(channelId, sFileList, dFileList, fileCnt);
    if (ret != SOFTBUS_OK) {
        TRANS_LOGE(TRANS_FILE, "file senging process failed, ret=%{public}d", ret);
    }
    (void)SoftBusMutexUnlock(&sessionLock->sendLock);
    DelSessionFileLock(sessionLock);
    return ret;
}

接下来再是文件发送调度中心,最底层的功能实现。再次提醒,服务端是进程通信中的接收者,这里指的是软总线。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐