上篇文章我们总结了大文件分片上传的主要核心,但是我对md5校验和上传进度展示这块也比较感兴趣,所以在deepseek的帮助下,扩展了一下我们的代码,如果有任何问题和想法,非常欢迎大家在评论区与我交流,我需要学习的地方也还有特别多~
开始之前我们先用一个通俗易懂的例子来理解我们的功能吧~
用快递寄大件包裹的思路,解释大文件分片上传的实现步骤
场景设定:
假设你要把一卡车的大米(5吨)从杭州运到北京,但遇到了几个现实问题:
1.整卡车运输风险大(爆胎就全完了)
2.中途可能有检查站需要抽检
3.运输途中网络信号时好时坏
解决方案的六个关键步骤:
第一步:货物预处理(preprocessFile)
动作:把大米分装成小袋,每袋贴上唯一编号
技术对应:
- 计算整个大米的MD5指纹(确保货物完整性)
- 生成专用加密包装袋(AES密钥)
为什么重要:
- 避免整车运输风险
- 方便中途抽检任意一袋
- 不同袋子用不同密码锁更安全
第二步:秒传核验(checkInstantUpload)
动作:打电话给北京仓库:“你们已经有5吨杭州大米了吗?”
技术对应:
- 发送MD5给服务端查询
- 如果已有相同货物,直接标记运输完成
省时技巧:
- 避免重复运输相同货物
- 节省90%运输时间
第三步:智能分箱(prepareChunks)
动作:根据路况决定每箱装多少袋
- 高速公路:用大箱子(装20袋)
- 山路:用小箱子(装5袋)
技术对应:
- 网络测速(检查"路况")
- 动态调整分片大小
智慧之处:
- 好路况多装快跑
- 差路况少装稳走
第四步:分批运输(uploadAllChunks)
动作:
- 给每个箱子单独上锁(不同IV加密)
- 3辆货车同时出发(并发控制)
- 某辆车抛锚就换车重发(错误重试)
技术细节:
- 每个分片独立加密
- 失败分片自动重试3次
- 实时记录已送达的箱子
第五步:收件核验(mergeFile)
动作:
- 北京仓库收到所有箱子
- 按编号顺序拆箱组合
- 检查MD5是否匹配原始指纹
安全保障:
- 防止运输途中被调包
- 确保颗粒不少
第六步:断点续传(saveProgress)
突发情况处理:
- 遇到暴雨暂停运输
- 记录哪些箱子已送达
- 雨停后继续送未达的箱子
技术实现:
- 自动保存上传进度
- 支持从断点恢复
我们的智能方案为什么更优秀呢?
1.整车上路风险高,
化整为零
更安全
2.每袋都有独立指纹
和密码锁
,防止被掉包
3.根据路况调整运输策略
,堵车时不用干等
4.遇到检查要全部开箱时,可随机抽捡
一袋不影响整体
5.重新发货不用从头开始,断点续传
省时省力
所以我们其实是在上一篇大文件分片上传的过程中增加了两个功能:
秒传检查和合并校验
整体步骤是:
预处理->秒传检查->智能分片->加密运输->合并校验->断点保护
学习之前我们先来搞懂两个问题:
1.md5校验在我们文件上传过程中是必须的吗?
答案肯定是否,但有两个场景我们是必须要使用md5校验的(大方向):秒传功能
与文件完整性
场景 | 类比解释 | 技术对应 |
---|---|---|
秒传功能 | 仓库发现已有同批次芒果,直接调库 | 服务端比对MD5跳过上传 |
防数据篡改 | 发现运输商偷换成越南芒果 | 合并后MD5与原始值比对 |
如果简单使用文件大小校验
,或者严重依赖tpc传输
确保文件不会丢失的情况下也是可以不使用md5校验的
2.md5校验与分片加密有什么关系?可以替代吗?
即使我们已经对每个分片进行了加密上传,仍然可以使用md5校验文件的完整性,分片加密与md5校验是互补而非替代
。分片加密如同在生产线上为每个零件做防锈处理,而MD5校验如同在出厂前对整机进行质检——防锈处理不能替代最终质检,两者结合才能确保交付可靠的产品。
- 分片加密的作用
加密阶段 | 防护目标 | 示例风险 |
---|---|---|
传输过程加密 | 防中间人窃听/篡改 | 黑客截获分片并修改 |
存储加密 | 防服务器数据泄露 | 数据库被拖库 |
- MD5校验的核心价值
校验场景 | 解决的问题 | 示例风险 |
---|---|---|
加密前校验 | 源文件完整性 | 本地文件损坏 |
解密后校验 | 解密过程是否正确 | 密钥错误导致解密失败 |
合并校验 | 分片顺序/组合错误 | 分片序号错乱 |
所以如果(验证文件完整性中可能会发生的错误)
1.加密前的源文件已经损坏(加密传输后肯定有误)
2.密钥错误、IV丢失、解密算法不兼容等导致解密后数据错误
3.分片上传成功但合并顺序错乱
以上几种情况发生的时候我们是有必要进行md5校验的
首先看我们需要实现功能的完整思路与技术亮点吧~
完整实现思路(五阶段工作流)
1.初始化阶段
- 生成文件唯一ID(UUID v4)
- 配置分片大小范围(默认1MB~20MB)
- 初始化加密系统、分片存储结构
2.预处理阶段
- 并行执行:MD5计算 + 密钥生成(加速启动)
- 流式MD5:2MB分片渐进计算,避免内存溢出
- 密钥管理:使用Web Crypto API安全生成AES密钥
3.秒传校验
- 发送文件MD5到服务端查询
- 存在相同文件时直接跳过后续步骤
- 节省带宽和服务器存储空间
4.动态分片上传
- 网络测速:通过1MB测试文件探测当前带宽
- 智能分片:按带宽50%动态调整分片大小
- 加密传输:每个分片独立IV,AES-CBC加密
- 并发控制:3个并行上传通道(可配置)
- 重试机制:指数退避策略(2s, 4s, 8s)
5.收尾工作
- 发送合并请求到服务端
- 清理临时数据(可选)
- 持久化最终状态
架构亮点
网络自适应
- 三次测速取平均值减少误差
- 分片大小平滑过渡(避免剧烈波动)
安全传输
- 前端加密 + 服务端解密双保险
- 每个分片独立IV防止模式分析
- 密钥仅存于内存和加密存储
可靠性保障
- 断点续传:自动保存进度到IndexedDB
- 原子操作:分片上传成功后才标记完成
- 错误隔离:单个分片失败不影响整体流程
性能优化
- 并行预处理(MD5和密钥生成)
- 流式哈希计算(内存占用恒定)
- 浏览器空闲时段上传(可扩展)
以下是完整代码
/**
* 四位一体的网络感知型大文件传输系统
*
* 动态分片策略
// 基于实时网络带宽的动态分片算法(1MB~20MB智能调节)
// 分片大小平滑调整机制(20%最大波动限制)
// 内存对齐优化(1MB粒度减少资源碎片)
* 数据安全框架
// 三级校验体系:全文件MD5 + 分片级SHA256双哈希
// AES-CBC加密(分片独立IV防重放攻击)
// 密钥生命周期管理(生成->存储->使用隔离)
* 传输可靠性保障
// 断点续传能力(IndexedDB持久化存储)
// 异常安全边界(try-catch包裹全流程)
// 分片原子化操作(独立元数据管理)
* 性能优化工程
// 并行预处理流水线(MD5与密钥并行计算)
// 流式哈希计算(2MB分片递归处理)
// 网络测速基准(1MB测试包探测带宽)
*/
/**
* 大文件分片上传类(支持动态分片、加密、MD5校验)
*/
class FileUploader {
/**
* 初始化上传实例
* @param {File} file - 浏览器文件对象
* @param {Object} [options] - 配置参数
*/
constructor(file, options = {}) {
// 必需参数
this.file = file; // 上传文件对象
this.fileId = this.generateFileId(); // 文件唯一标识
this.fileMD5 = null; // 全文件MD5值
// 分片管理
this.chunks = []; // 全部分片数据
this.uploadedChunks = new Set(); // 已上传分片索引
this.ivMap = new Map(); // 加密初始化向量存储,存储每个分片的IV
// 加密配置
this.encryptionKey = null; // AES加密密钥
// 性能参数
this.lastUploadSpeed = 5 * 1024 * 1024; // 网络基准速度(默认5MB/s)
// 用户配置
this.options = {
minChunkSize: 1 * 1024 * 1024, // 最小分片1MB
maxChunkSize: 20 * 1024 * 1024, // 最大分片20MB
...options
};
// 事件系统
this.events = {};
}
// --------------------------
// 核心公共方法
// --------------------------
/**
* 启动上传流程(完整工作流)
* * @throws {Error} 上传过程中的错误
*/
async startUpload() {
try {
// 阶段1: 文件预处理(计算哈希 + 生成密钥)
await this.preprocessFile();
// 阶段2: 秒传检查(通过文件MD5判断是否需要传输)
const needUpload = await this.checkInstantUpload();
if (!needUpload) return;
// 阶段3: 动态分片准备(根据网络状况生成分片)
await this.prepareChunks();
// 阶段4: 分片上传(含加密和重试机制)
await this.uploadAllChunks();
// 阶段5: 合并请求
await this.mergeFile();
this.emit('complete');
} catch (error) {
console.error('上传流程异常:', error);
await this.saveProgress(); // 异常时保存进度
this.emit('error', error);
throw error;
}
}
// --------------------------
// 预处理阶段(含MD5计算)
// --------------------------
/**
* 阶段1:文件预处理(计算MD5、生成密钥)
*/
async preprocessFile() {
// 并行执行两个任务,实现异步流水线加速
// SparkMD5库保障哈希计算准确性
// Web Crypto API生成符合FIPS标准的AES密钥
const [md5, key] = await Promise.all([
this.calculateFileMD5(), // 计算全文件MD5
this.generateAESKey() // 生成加密密钥
]);
this.fileMD5 = md5;
this.encryptionKey = key;
// 保存初始进度(可用于恢复)
await this.saveProgress();
}
/**
* 计算全文件MD5(分片计算避免内存溢出)
* @returns {Promise<string>} MD5哈希值
*/
calculateFileMD5() {
// 返回Promise对象实现异步计算流程控制
return new Promise((resolve) => {
// 定义分片大小(2MB兼顾计算效率与内存安全)
const chunkSize = 2 * 1024 * 1024;
// 计算总切片数量(向上取整保证最后分片完整性)
const chunks = Math.ceil(this.file.size / chunkSize);
// 初始化SparkMD5实例(专为ArrayBuffer优化的MD5计算库)
const spark = new SparkMD5.ArrayBuffer();
// 已处理分片计数器
let processed = 0;
// 定义分片加载递归函数,递归分片处理大文件(2MB粒度),避免内存溢出风险
const loadNext = () => {
// 计算当前分片字节范围
const start = processed * chunkSize;
const end = Math.min(start + chunkSize, this.file.size);
// 切割文件对象获取当前分片Blob
const blob = this.file.slice(start, end);
// 创建文件读取器处理二进制数据
const reader = new FileReader();
// 注册文件加载完成回调
reader.onload = (e) => {
// 将分片二进制数据追加到MD5计算流
spark.append(e.target.result);
// 更新已处理分片计数
processed++;
// 存储计算进度(格式:当前分片/总分片数)
sessionStorage.setItem(`${this.fileId}_md5`, `${processed}/${chunks}`);
// 触发进度事件
this.emit('progress', {
type: 'md5',
value: processed / chunks
});
// 递归判断:未完成继续处理,完成则返回最终MD5
processed < chunks ? loadNext() : resolve(spark.end());
};
// 启动分片数据读取(ArrayBuffer格式保持二进制精度)
reader.readAsArrayBuffer(blob);
};
// 启动首个分片处理
loadNext();
});
}
/**
* 阶段2:秒传验证(checkInstantUpload)
* 实现逻辑: 将全文件MD5发送至服务端查询,若存在相同哈希文件,触发秒传逻辑,跳过后续流程直接返回成功
* 业务价值:节省90%+重复文件传输成本;降低服务器存储冗余
*/
/**
* 🌟 秒传验证核心方法
* @param {string} fileMD5 - 文件的完整MD5哈希值
* @returns {Promise<boolean>} - 是否可秒传
*/
async checkInstantUpload(fileMD5) {
try {
const response = await fetch('/api/check-instant-upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ md5: fileMD5 })
});
if (!response.ok) throw new Error('秒传验证请求失败');
const result = await response.json();
return result.exists; // 服务端返回是否存在
} catch (error) {
console.error('秒传验证异常:', error);
return false; // 失败时按需处理,此处默认继续上传
}
}
// --------------------------
// 分片处理阶段(含动态调整)
// --------------------------
/**
* 阶段3:智能分片准备(动态调整分片大小)
* 算法原理:
基于实时测速结果(testNetworkSpeed)计算基准值
引入历史速度惯性因子(lastUploadSpeed)平滑波动
内存对齐优化提升分片处理效率
*/
async prepareChunks() {
// 网络测速(取三次平均值)
const speeds = [];
for (let i = 0; i < 3; i++) {
speeds.push(await this.testNetworkSpeed());
}
const currentSpeed = speeds.reduce((a, b) => a + b, 0) / speeds.length;
// 计算动态分片大小(控制在配置范围内)
let chunkSize = currentSpeed * 0.5; // 按带宽50%计算
chunkSize = Math.max(
this.options.minChunkSize,
Math.min(this.options.maxChunkSize, chunkSize)
);
// 生成分片元数据
const totalChunks = Math.ceil(this.file.size / chunkSize);
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, this.file.size);
this.chunks.push({
index: i,
blob: this.file.slice(start, end),
size: end - start
});
}
}
}
/**
* 阶段4:分片上传(uploadAllChunks)(并发控制 + 重试机制)
* 加密流程
生成随机IV(每个分片独立初始化向量)
计算原始数据SHA256哈希
AES-CBC加密分片数据
生成加密后哈希(可选二次校验)
* 传输策略
Set数据结构记录已上传分片索引
失败重试机制(需补充实现)
并行上传控制(可扩展为连接池管理
*/
async uploadAllChunks() {
// 设置并发数:浏览器环境下建议2-4个并行请求,平衡性能与稳定性
const CONCURRENCY = 3;
// 创建上传队列:通过展开运算符复制分片数组,避免直接操作原始数据
const queue = [...this.chunks];
// 主循环:持续处理直到队列清空
while (queue.length > 0) {
// 初始化当前批次的Promise容器
const workers = [];
// 提取当前批任务:每次取出CONCURRENCY数量的分片
// splice操作会同时修改队列长度,实现队列动态缩减
const currentBatch = queue.splice(0, CONCURRENCY);
// 遍历当前批次的分片
for (const chunk of currentBatch) {
// 跳过已上传分片:通过Set检查避免重复上传
if (this.uploadedChunks.has(chunk.index)) continue;
// 将分片上传任务包装成Promise,加入workers数组
workers.push(
// 执行分片上传核心方法
this.uploadChunk(chunk, chunk.index)
.then(() => {
// 计算实时进度:已上传数 / 总分片数
const progress = this.uploadedChunks.size / this.chunks.length;
// 触发进度事件:通知外部监听者更新进度条
this.emit('progress', { type: 'upload', value: progress });
})
);
}
// 等待当前批次全部完成(无论成功/失败)
// 使用allSettled而非all保证异常不会中断整个上传流程
await Promise.allSettled(workers);
}
}
/**
* 单分片上传(含加密和重试机制)
* @param {Object} chunk - 分片数据
* @param {number} index - 分片索引
* @param {number} retries - 剩余重试次数
*/
async uploadChunk(chunk, index, retries = 3) {
try {
// 加密处理(生成独立IV)
const encryptedBlob = await this.encryptChunk(chunk.blob, index);
// 构建表单数据
const formData = new FormData();
formData.append('file', encryptedBlob);
formData.append('index', index);
formData.append('iv', this.ivMap.get(index));
// 上传请求
const response = await fetch('/upload', {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
this.uploadedChunks.add(index);
} catch (error) {
if (retries > 0) {
await new Promise(r => setTimeout(r, 2000 * (4 - retries))); // 指数退避
return this.uploadChunk(chunk, index, retries - 1);
}
throw new Error(`分片${index}上传失败: ${error.message}`);
}
}
/* ================= 加密模块 ================= */
/** 生成AES-CBC加密密钥 */
async generateAESKey() {
return crypto.subtle.generateKey(
{ name: 'AES-CBC', length: 256 },
true,
['encrypt', 'decrypt']
);
}
/** 加密单个分片 */
async encryptChunk(blob, index) {
const iv = crypto.getRandomValues(new Uint8Array(16));
const data = await blob.arrayBuffer();
// 原始数据哈希校验
const rawHash = await crypto.subtle.digest('SHA-256', data);
// 执行加密
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-CBC', iv },
this.encryptionKey,
data
);
// 存储加密参数
this.ivMap.set(index, iv);
return new Blob([encrypted], { type: 'application/octet-stream' });
}
/* ================= 辅助方法 ================= */
/** 网络测速(上传1MB测试文件) */
async testNetworkSpeed() {
const testBlob = new Blob([new Uint8Array(1 * 1024 * 1024)]);
const start = Date.now();
await fetch('/speed-test', {
method: 'POST',
body: testBlob
});
const duration = (Date.now() - start) / 1000;
return (1 * 1024 * 1024) / duration; // 返回字节/秒
}
/** 持久化上传进度 */
async saveProgress() {
const data = {
fileId: this.fileId,
chunks: this.chunks,
uploadedChunks: [...this.uploadedChunks],
encryptionKey: await crypto.subtle.exportKey('jwk', this.encryptionKey),
ivMap: Object.fromEntries(this.ivMap)
};
await idb.setItem(this.fileId, data); // 假设使用IndexedDB
}
/** 生成文件唯一ID */
generateFileId() {
return crypto.randomUUID();
}
/* ================= 事件系统 ================= */
on(event, callback) {
this.events[event] = callback;
return this;
}
emit(event, ...args) {
const handler = this.events[event];
handler && handler(...args);
}
}
// ---------------------------- 使用示例 ----------------------------
const uploader = new FileUploader(file, {
maxChunkSize: 50 * 1024 * 1024 // 自定义配置
});
// 事件监听
uploader
.on('progress', ({ type, value }) => {
console.log(`${type}进度: ${(value * 100).toFixed(1)}%`);
})
.on('error', error => {
console.error('上传失败:', error);
});
// 启动上传
uploader.startUpload();