18 KiB
18 KiB
文件传输 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_V2caseserver/2015Remote/2015RemoteDlg.cpp- 新增COMMAND_SEND_FILE_V2case
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- 键盘钩子新增静态变量remoteCtrlCTimeserver/2015Remote/2015RemoteDlg.cpp- Ctrl+C 时区分本地/远程,记录时间server/2015Remote/2015RemoteDlg.cpp- 键盘钩子分支[3]:远程A→远程Bserver/2015Remote/2015RemoteDlg.cpp- 发送COMMAND_CLIPBOARD_V2触发 C2Cclient/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 → 记录
operateWnd和remoteCtrlCTime - 切换到远程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 = 91common/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_V2client/ScreenManager.cpp- 处理COMMAND_FILE_COMPLETE_V2server/2015Remote/2015RemoteDlg.cpp- 处理/转发COMMAND_FILE_COMPLETE_V2server/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=falseSimplePlugins/file_upload.cpp- 内存状态匹配跳过已完成SimplePlugins/file_upload.cpp- C2C 续传匹配包含目标目录
设计决策:
- C2C 断点续传匹配时包含目标目录,避免跨目录误匹配
- 断点续传时从文件重新计算 SHA-256(无法恢复流式上下文)
- 校验失败自动删除损坏文件
Phase 6: 能力协商 + 服务端开关 + 缓存整合 ✅
状态: 已完成
完成时间: 2026-02-27
改动内容:
-
能力位协商
LOGIN_INFOR构造函数在版本字符串后附加能力位(格式:Feb 27 2026-0001)context.h新增ONLINELIST_CAPABILITIES列、SupportsFileV2()方法AddList()解析能力位并存储到列表项
-
服务端开关
- 参数菜单新增 "文件传输V2" 选项(
ID_PARAM_FILE_V2) m_bEnableFileV2类成员控制 V2 开关- 配置通过
THIS_CFG持久化到settings/EnableFileV2 SupportsFileTransferV2()统一判断函数
- 参数菜单新增 "文件传输V2" 选项(
-
缓存目录整合
- 原目录:
%TEMP%\FileTransfer\(.resume)%LOCALAPPDATA%\ServerD11\c2c_recv_targets\(.target)%LOCALAPPDATA%\ServerD11\c2c_targets\(.target)
- 整合后:
%TEMP%\FileTransfer\统一存放.resume、.recv、.send - 移除
GetC2CTargetDir()函数 CleanupExpiredStateFiles()处理三种文件类型
- 原目录:
-
自动清理
RESUME_EXPIRE_DAYS = 7GetPendingTransfers()时触发清理- 检查
ftLastWriteTime判断过期
改动文件:
common/commands.h- 新增CLIENT_CAP_V2server/2015Remote/context.h- 新增ONLINELIST_CAPABILITIES、SupportsFileV2()server/2015Remote/2015RemoteDlg.h- 新增m_bEnableFileV2、声明SupportsFileTransferV2()server/2015Remote/2015RemoteDlg.cpp- 实现菜单处理、能力解析、判断函数server/2015Remote/resource.h- 新增ID_PARAM_FILE_V2server/2015Remote/2015Remote.rc- 新增菜单项SimplePlugins/file_upload.cpp- 整合缓存目录、自动清理