Files
SimpleRemoter/docs/FileTransferV2_Plan.md
2026-04-19 22:55:21 +02:00

18 KiB
Raw Permalink Blame History

文件传输 V2 协议方案

支持 C2C客户端到客户端传输 + 断点续传

实施进度

阶段 内容 状态 完成日期
Phase 1 V2 结构体 + 接口定义 完成 2026-02-23
Phase 2 V2 基础收发(不含 C2C 完成 2026-02-23
Phase 3 断点续传 完成 2026-02-23
Phase 4 C2C 完成 2026-02-23
Phase 5 SHA-256 文件校验 完成 2026-02-26
Phase 6 能力协商 + 服务端开关 + 缓存整合 完成 2026-02-27

一、命令设计

// common/commands.h

// 老命令(保持不变)
COMMAND_SEND_FILE       = 68,   // V1 文件传输

// 新命令V2 独立)
COMMAND_SEND_FILE_V2      = 85,   // V2 文件传输(支持 C2C + 断点续传)
COMMAND_FILE_RESUME       = 86,   // V2 断点续传控制
COMMAND_CLIPBOARD_V2      = 87,   // V2 剪贴板请求C2C
COMMAND_FILE_QUERY_RESUME = 88,   // V2 断点续传查询
COMMAND_C2C_PREPARE       = 89,   // C2C 准备接收通知
COMMAND_C2C_TEXT          = 90,   // C2C 文本传输
COMMAND_FILE_COMPLETE_V2  = 91,   // V2 文件完成校验SHA-256
COMMAND_C2C_PREPARE_RESP  = 92,   // C2C 准备响应(返回目标目录)

// 客户端能力位
#define CLIENT_CAP_V2   0x0001    // 支持 V2 文件传输

// 功能引入日期
#define FILE_TRANSFER_V2_DATE  "Feb 27 2026"

二、结构体设计

// common/file_upload.h

#pragma pack(push, 1)

// ==================== V1 协议(不改动)====================
struct FileChunkPacket {
    uint8_t     cmd;            // = 68
    uint32_t    fileIndex;
    uint32_t    totalNum;
    uint64_t    fileSize;
    uint64_t    offset;
    uint64_t    dataLength;
    uint64_t    nameLength;
};  // 41 bytes


// ==================== V2 协议(新增,独立)====================
struct FileChunkPacketV2 {
    uint8_t     cmd;            // = 85
    uint64_t    transferID;     // 传输会话ID
    uint64_t    srcClientID;    // 源客户端 (0=主控端)
    uint64_t    dstClientID;    // 目标客户端 (0=主控端)
    uint32_t    fileIndex;      // 文件编号
    uint32_t    totalFiles;     // 总文件数
    uint64_t    fileSize;       // 文件大小
    uint64_t    offset;         // 偏移
    uint64_t    dataLength;     // 数据长度
    uint64_t    nameLength;     // 文件名长度
    uint16_t    flags;          // 标志位
    uint16_t    checksum;       // CRC16
    uint8_t     reserved[8];    // 预留
};  // 81 bytes

enum FileFlagsV2 : uint16_t {
    FFV2_NONE         = 0x0000,
    FFV2_LAST_CHUNK   = 0x0001,
    FFV2_RESUME_REQ   = 0x0002,
    FFV2_RESUME_RESP  = 0x0004,
    FFV2_CANCEL       = 0x0008,
    FFV2_DIRECTORY    = 0x0010,
    FFV2_COMPRESSED   = 0x0020,
    FFV2_ERROR        = 0x0040,
};

struct FileResumePacketV2 {
    uint8_t     cmd;            // = 86
    uint64_t    transferID;
    uint64_t    srcClientID;
    uint64_t    dstClientID;
    uint32_t    fileIndex;
    uint64_t    fileSize;
    uint64_t    receivedBytes;
    uint16_t    flags;
    uint16_t    rangeCount;
    // FileRangeV2 ranges[rangeCount];
};  // 51 bytes + ranges

struct FileRangeV2 {
    uint64_t    offset;
    uint64_t    length;
};  // 16 bytes

struct ClipboardRequestV2 {
    uint8_t     cmd;            // = 87
    uint64_t    srcClientID;
    uint64_t    dstClientID;
    uint64_t    transferID;
    char        hash[64];
    char        hmac[16];
};  // 105 bytes

struct C2CPreparePacket {
    uint8_t     cmd;            // = 89
    uint64_t    transferID;
    uint64_t    srcClientID;    // 发送方客户端ID
};  // 17 bytes

struct C2CPrepareRespPacket {
    uint8_t     cmd;            // = 92
    uint64_t    transferID;
    uint64_t    srcClientID;    // 原始发送方客户端ID
    uint16_t    pathLength;
    // char path[pathLength];   // UTF-8 目标目录
};  // 19 bytes + path

struct FileCompletePacketV2 {
    uint8_t     cmd;            // = 91
    uint64_t    transferID;
    uint64_t    srcClientID;
    uint64_t    dstClientID;
    uint32_t    fileIndex;
    uint64_t    fileSize;
    uint8_t     sha256[32];     // SHA-256 哈希
};  // 69 bytes

#pragma pack(pop)

三、接口设计

// common/file_upload.h

// ==================== V1 接口(保持不变)====================
int FileBatchTransferWorker(...);
int RecvFileChunk(...);

// ==================== V2 接口(新增)====================
struct TransferOptionsV2 {
    uint64_t    transferID;     // 0=自动生成
    uint64_t    srcClientID;
    uint64_t    dstClientID;
    bool        enableResume;   // 启用断点续传
};

int FileBatchTransferWorkerV2(
    const std::vector<std::string>& files,
    const std::string& targetDir,
    void* user,
    OnTransformV2 f,
    OnFinish finish,
    const std::string& hash,
    const std::string& hmac,
    const TransferOptionsV2& options
);

int RecvFileChunkV2(
    char* buf, size_t len,
    void* user,
    OnFinish f,
    const std::string& hash,
    const std::string& hmac,
    uint64_t myClientID
);

// 断点续传
uint64_t GenerateTransferID();
bool SaveResumeState(uint64_t transferID, ...);
bool LoadResumeState(uint64_t transferID, ...);
void CleanupResumeState(uint64_t transferID);
std::vector<uint64_t> GetPendingTransfers();

// 文件完整性校验
bool HandleFileCompleteV2(
    const char* buf, size_t len,
    uint64_t myClientID
);

四、持久化设计

4.1 状态文件位置

所有状态文件统一存放在:

  • Windows: %TEMP%\FileTransfer\
  • Linux: /tmp/FileTransfer/

4.2 状态文件类型

后缀 用途 文件名格式
.resume 断点续传状态 {transferID}.resume
.recv C2C 接收方目标目录 {transferID}.recv
.send C2C 发送方文件路径 {transferID}.send

4.3 自动清理

  • 状态文件超过 7 天自动删除 (RESUME_EXPIRE_DAYS = 7)
  • 清理在 GetPendingTransfers() 时触发
  • 三种后缀文件均会被清理

4.4 .resume 文件结构

struct ResumeFileHeader {
    uint32_t    magic;              // = 0x52455355 "RESU"
    uint64_t    transferID;
    uint64_t    srcClientID;
    uint64_t    dstClientID;
    uint32_t    totalFiles;
    char        targetDir[260];
};

struct ResumeFileEntry {
    uint64_t    fileSize;
    uint64_t    receivedBytes;
    uint16_t    rangeCount;
    char        fileName[260];
    // FileRangeV2 ranges[rangeCount];
};

4.5 .recv / .send 文件结构

简单文本文件,存储 UTF-8 编码的目录路径或文件路径列表(每行一个)。


五、服务端路由

// 2015RemoteDlg.cpp

case COMMAND_SEND_FILE: {
    // V1 逻辑(保持不变)
}

case COMMAND_SEND_FILE_V2: {
    // V2 逻辑(独立)
    FileChunkPacketV2* pkt = (FileChunkPacketV2*)szBuffer;
    if (pkt->dstClientID == 0) {
        HandleLocalReceiveV2(...);
    } else {
        // C2C 转发
        ForwardToClientV2(...);
    }
}

case COMMAND_FILE_RESUME: {
    // V2 断点续传
}

六、版本检测

6.1 能力位协商

// common/commands.h
#define CLIENT_CAP_V2   0x0001      // 支持 V2 文件传输

// LOGIN_INFOR 构造函数
sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, CLIENT_CAP_V2);
// 结果示例: "Feb 27 2026-0001"

6.2 服务端检测

// 双重检测:能力位 + 版本日期
bool SupportsFileTransferV2(context* ctx) {
    // 1. 检查服务端开关
    if (!g_2015RemoteDlg || !g_2015RemoteDlg->m_bEnableFileV2) return false;

    // 2. 检查能力位
    if (ctx->SupportsFileV2()) return true;

    // 3. 兼容旧版:检查版本日期
    CString version = ctx->GetClientData(ONLINELIST_VERSION);
    return IsDateGreaterOrEqual(version, FILE_TRANSFER_V2_DATE);
}

// C2C 要求双方都支持 V2
if (!SupportsFileTransferV2(src) || !SupportsFileTransferV2(dst)) {
    // 提示版本不支持
}

6.3 服务端开关

菜单位置 默认值 配置键
参数 → 文件传输V2 关闭 settings/EnableFileV2
  • 未勾选时使用 V1 协议
  • 勾选后根据客户端能力选择 V1/V2
  • 配置通过 THIS_CFG 持久化

七、行为矩阵

场景 源版本 目标版本 协议 结果
本地→远程 - V1 V1 正常
本地→远程 - V2 V2 正常 + 断点续传
远程→本地 V1 - V1 正常
远程→本地 V2 - V2 正常 + 断点续传
C2C V1 V1 - 不支持
C2C V1 V2 - 不支持(提示升级)
C2C V2 V1 - 不支持(提示升级)
C2C V2 V2 V2 正常 + 断点续传

八、改动文件清单

文件 改动类型 说明
common/commands.h 修改 新增命令字、能力位
common/file_upload.h 修改 新增 V2 结构体和接口
SimplePlugins/file_upload.cpp 修改 V2 实现、缓存整合、自动清理
client/ScreenManager.cpp 修改 新增 case 分支
client/KernelManager.cpp 修改 处理 V2 命令
server/2015Remote/context.h 修改 新增能力位列、SupportsFileV2()
server/2015Remote/2015RemoteDlg.h 修改 新增 m_bEnableFileV2、函数声明
server/2015Remote/2015RemoteDlg.cpp 修改 case 分支、菜单处理、能力解析
server/2015Remote/resource.h 修改 新增菜单ID
server/2015Remote/2015Remote.rc 修改 新增菜单项
server/2015Remote/CDlgFileSend.cpp 修改 C2C 校验包转发
server/2015Remote/ScreenSpyDlg.cpp 修改 剪贴板同步

九、实施记录

Phase 1: V2 结构体 + 接口定义

状态: 已完成

完成时间: 2026-02-23

改动文件:

  • common/commands.h

    • 新增 FEATURE_FILE_V2 = "Mar 1 2026"
    • 新增 COMMAND_SEND_FILE_V2 = 85
    • 新增 COMMAND_FILE_RESUME = 86
    • 新增 COMMAND_CLIPBOARD_V2 = 87
  • common/file_upload.h

    • 新增 FileFlagsV2 枚举
    • 新增 FileChunkPacketV2 结构体 (81 bytes)
    • 新增 FileRangeV2 结构体 (16 bytes)
    • 新增 FileResumePacketV2 结构体 (51 bytes + ranges)
    • 新增 ClipboardRequestV2 结构体 (105 bytes)
    • 新增 FileErrorV2 错误码枚举
    • 新增 TransferOptionsV2 结构体
    • 新增 FileBatchTransferWorkerV2() 接口声明
    • 新增 RecvFileChunkV2() 接口声明
    • 新增断点续传相关接口声明

Phase 2: V2 基础收发

状态: 已完成

完成时间: 2026-02-23

改动文件:

  • SimplePlugins/file_upload.cpp - 实现 FileBatchTransferWorkerV2()
  • SimplePlugins/file_upload.cpp - 实现 RecvFileChunkV2()
  • SimplePlugins/file_upload.cpp - 实现 GenerateTransferID()
  • client/ScreenManager.cpp - 新增 COMMAND_SEND_FILE_V2 case
  • server/2015Remote/2015RemoteDlg.cpp - 新增 COMMAND_SEND_FILE_V2 case

Phase 3: 断点续传

状态: 已完成

完成时间: 2026-02-23

改动文件:

  • SimplePlugins/file_upload.cpp - .resume 文件头结构体定义
  • SimplePlugins/file_upload.cpp - 实现 GetResumeDir() / GetResumeFilePath()
  • SimplePlugins/file_upload.cpp - 实现 SaveResumeState() 保存传输状态
  • SimplePlugins/file_upload.cpp - 实现 LoadResumeState() 恢复传输状态
  • SimplePlugins/file_upload.cpp - 实现 CleanupResumeState() 清理状态文件
  • SimplePlugins/file_upload.cpp - 实现 GetPendingTransfers() 枚举未完成传输
  • SimplePlugins/file_upload.cpp - RecvFileChunkV2() 中定期自动保存状态
  • client/ScreenManager.cpp - OnReconnect() 中检测并恢复未完成传输
  • client/ScreenManager.cpp - 处理 COMMAND_FILE_RESUME 续传控制
  • server/2015Remote/2015RemoteDlg.cpp - 处理/转发 COMMAND_FILE_RESUME

实现细节:

  • .resume 文件存储位置: %TEMP%\FileTransfer\{transferID}.resume
  • 自动保存触发条件: 每 5 秒或每 1MB 新数据
  • 重连时自动恢复本地状态,等待数据继续传输
  • 所有文件完成后自动清理 .resume 文件

Phase 4: C2C

状态: 已完成

完成时间: 2026-02-23

改动文件:

  • server/2015Remote/2015RemoteDlg.cpp - 键盘钩子新增静态变量 remoteCtrlCTime
  • server/2015Remote/2015RemoteDlg.cpp - Ctrl+C 时区分本地/远程,记录时间
  • server/2015Remote/2015RemoteDlg.cpp - 键盘钩子分支[3]远程A→远程B
  • server/2015Remote/2015RemoteDlg.cpp - 发送 COMMAND_CLIPBOARD_V2 触发 C2C
  • client/ScreenManager.cpp - 处理 COMMAND_CLIPBOARD_V2,获取剪贴板并发送到目标
  • SimplePlugins/file_upload.cpp - 新增 C2C 文件跟踪 (g_c2cReceivedFiles)
  • SimplePlugins/file_upload.cpp - 新增 SetFilesToClipboard() 函数
  • SimplePlugins/file_upload.cpp - RecvFileChunkV2() 完成后设置剪贴板

实现细节:

  • 远程A 按 Ctrl+C → 记录 operateWndremoteCtrlCTime
  • 切换到远程B 按 Ctrl+V → 检测 operateWnd != dlg 且时间有效
  • 服务端发送 COMMAND_CLIPBOARD_V2 到源客户端A
  • 客户端A 获取剪贴板文件,使用 V2 协议发送到客户端B
  • 客户端B 接收文件,传输完成后自动设置到剪贴板 (CF_HDROP)

Phase 5: SHA-256 文件校验

状态: 已完成

完成时间: 2026-02-26

改动文件:

  • common/commands.h - 新增 COMMAND_FILE_COMPLETE_V2 = 91
  • common/file_upload.h - 新增 FileCompletePacketV2 结构体 (69 bytes)
  • SimplePlugins/file_upload.cpp - 实现 SHA256Context 类 (Windows bcrypt API)
  • SimplePlugins/file_upload.cpp - FileRecvStateV2 新增 sha256Ctx 流式计算
  • SimplePlugins/file_upload.cpp - 实现 HandleFileCompleteV2() 校验函数
  • SimplePlugins/file_upload.cpp - FileBatchTransferWorkerV2() 发送完成后发送校验包
  • client/KernelManager.cpp - 处理 COMMAND_FILE_COMPLETE_V2
  • client/ScreenManager.cpp - 处理 COMMAND_FILE_COMPLETE_V2
  • server/2015Remote/2015RemoteDlg.cpp - 处理/转发 COMMAND_FILE_COMPLETE_V2
  • server/2015Remote/CDlgFileSend.cpp - 处理/转发 C2C 校验包

实现细节:

  • 使用 Windows bcrypt API (BCRYPT_SHA256_ALGORITHM) 计算 SHA-256
  • 接收端在写入数据时同步更新 SHA-256流式计算无需重读文件
  • 每个文件传输完成后,发送端发送 FILE_COMPLETE_V2 校验包
  • 接收端收到校验包后对比本地计算的哈希值
  • C2C 场景下服务端负责转发校验包到目标客户端

支持的传输方向:

方向 状态
客户端 → 服务端 已测试
服务端 → 客户端 已测试
客户端A → 客户端B (C2C) 已测试

Phase 5.1: SHA-256 校验 Bug 修复

状态: 已完成

完成时间: 2026-02-26

修复的问题:

问题 原因 修复
C2C 校验包未转发 CDlgFileSend 未检查 dstClientID 检查并转发到目标客户端
断点续传校验失败 .resume 恢复时 sha256Valid 仍为 true 恢复时设为 false强制从文件重算
已完成状态被复用 内存匹配未排除已完成的状态 增加 receivedBytes < fileSize 条件
不同目录文件被误匹配 C2C 续传按文件名匹配,忽略目标目录 匹配时包含目标目录

改动文件:

  • server/2015Remote/CDlgFileSend.cpp - C2C 校验包转发
  • SimplePlugins/file_upload.cpp - 断点续传时设 sha256Valid=false
  • SimplePlugins/file_upload.cpp - 内存状态匹配跳过已完成
  • SimplePlugins/file_upload.cpp - C2C 续传匹配包含目标目录

设计决策:

  • C2C 断点续传匹配时包含目标目录,避免跨目录误匹配
  • 断点续传时从文件重新计算 SHA-256无法恢复流式上下文
  • 校验失败自动删除损坏文件

Phase 6: 能力协商 + 服务端开关 + 缓存整合

状态: 已完成

完成时间: 2026-02-27

改动内容:

  1. 能力位协商

    • LOGIN_INFOR 构造函数在版本字符串后附加能力位(格式:Feb 27 2026-0001
    • context.h 新增 ONLINELIST_CAPABILITIES 列、SupportsFileV2() 方法
    • AddList() 解析能力位并存储到列表项
  2. 服务端开关

    • 参数菜单新增 "文件传输V2" 选项(ID_PARAM_FILE_V2
    • m_bEnableFileV2 类成员控制 V2 开关
    • 配置通过 THIS_CFG 持久化到 settings/EnableFileV2
    • SupportsFileTransferV2() 统一判断函数
  3. 缓存目录整合

    • 原目录:
      • %TEMP%\FileTransfer\ (.resume)
      • %LOCALAPPDATA%\ServerD11\c2c_recv_targets\ (.target)
      • %LOCALAPPDATA%\ServerD11\c2c_targets\ (.target)
    • 整合后:%TEMP%\FileTransfer\ 统一存放 .resume.recv.send
    • 移除 GetC2CTargetDir() 函数
    • CleanupExpiredStateFiles() 处理三种文件类型
  4. 自动清理

    • RESUME_EXPIRE_DAYS = 7
    • GetPendingTransfers() 时触发清理
    • 检查 ftLastWriteTime 判断过期

改动文件:

  • common/commands.h - 新增 CLIENT_CAP_V2
  • server/2015Remote/context.h - 新增 ONLINELIST_CAPABILITIESSupportsFileV2()
  • server/2015Remote/2015RemoteDlg.h - 新增 m_bEnableFileV2、声明 SupportsFileTransferV2()
  • server/2015Remote/2015RemoteDlg.cpp - 实现菜单处理、能力解析、判断函数
  • server/2015Remote/resource.h - 新增 ID_PARAM_FILE_V2
  • server/2015Remote/2015Remote.rc - 新增菜单项
  • SimplePlugins/file_upload.cpp - 整合缓存目录、自动清理