上篇文章我們總結了大文件分片上傳的主要核心,但是我對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, // 最小分片1MBmaxChunkSize: 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(), // 計算全文件MD5this.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);// 切割文件對象獲取當前分片Blobconst 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});// 遞歸判斷:未完成繼續處理,完成則返回最終MD5processed < 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();