文件互传---发送文件完整流程
前言
应用端调用的是底层封装好的接口,这里分析就是从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;
}
接下来再是文件发送调度中心,最底层的功能实现。再次提醒,服务端是进程通信中的接收者,这里指的是软总线。
更多推荐


所有评论(0)