大文件上傳原理及實現方案

一、什麼是大文件 一般,我們傳送大文件是指傳送大於100M的文件,而普通文件是指小於100M,常見的是20M、30M和50M,兩者主要的區別在於文件大小上,還有傳送速度上。

一般普通“郵件附件”只能發20M、30M,50M的文件,而幾百M的照片、文件、設計圖等大文件傳送起來就不是那麼容易了。

二、大文件跟普通文件上傳時的區別 普通文件上傳只需要注意兩點

1.指定上傳的接口地址。 2.將請求頭的Content-Type設置成:multipart/form-data,將文件對象以二進制流的形式傳給後端

大文件上傳時會遇到的問題

1.前後端上傳請求超時限制,一次性傳輸大小限制。 2.網絡抖動等,失敗後需要重新上傳。 3.http1.1版本, TCP連接默認是open的,所有請求都通過同一個連接進行數據傳輸,如果前面的請求被阻塞了,後面的請求也得不到響應,也叫HTTP/1.1 中的隊頭阻塞問題,除非建立多個連接,但是多個連接會浪費資源。 4.無進度條,用戶體驗極差。

三、大文件上傳的原理及思路 前端

獲取文件的二進制內容,然後對其內容拆分成指定大小的切片文件,最後將每個切片上傳到服務端即可。

流程:獲取文件 ➡️ 分片 ➡️ 上傳

需要優化的點

•中斷後無需重新上傳(斷點續傳) •上傳過的文件無需上傳(秒傳) •顯示上傳進度

後端

根據切片文件的唯一標識在後端將多個相同文件的切片還原成一個文件

流程:獲取分片文件 ➡️ 還原分片 ➡️ 返回拼接好的文件信息

需要優化的點

•刪除碎片文件 還原切片時需要注意的問題

•在後端需要將多個相同文件的切片還原成一個文件,如果不能識別一個切片是屬於哪一個文件的,當同時發生多個請求時,追加的文件內容會出錯。 •切片上傳接口是異步的,無法保證服務器接收到的切片是按照請求順序拼接的。 解決辦法

1)如何識別多個切片是來自於同一個文件的?

這個可以在發送請求時,爲每個切片傳遞一個相同文件的identifier參數。

2)如何將多個切片還原成一個文件?

什麼時候開始拼接:確認所有切片都已上傳完後開始進行拼接,這個可以通過客戶端在切片全部上傳後調用後端定義的mkfile接口來通知服務端進行拼接,或者前端傳遞切片的總數totalChunks, 服務端判斷接收的切片數量如果等於totalChunks的值就開始進行拼接,無須前端通知後端進行拼接。

怎麼按順序拼接:可以在每個切片上標記一個位置索引值,找到同一個context下的所有切片,根據chunkNumber確認每個切片的順序,這個按順序拼接切片,還原成文件

上面有幾個重要的參數: identifier ,chunkNumber,totalChunks

identifier :我們需要獲取爲一個文件的唯一標識,可以通過下面兩種方式獲取

  1. 根據文件名、文件長度等基本信息進行拼接,爲了避免多個用戶上傳相同的文件,可以再額外拼接用戶信息如uid等保證唯一性

  2. 根據文件的二進制內容計算文件的hash,這樣只要文件內容不一樣,則標識也會不一樣,缺點在於計算量比較大.

chunkNumber:當前切片的索引

totalChunks:總的切片數

 

四、大文件上傳的實現方案 前端分片代碼

// 獲取identifier,同一個文件會返回相同的值 function createIdentifiert(file) { return file.name + file.size }

let file = document.querySelector("[name=file]").files[0]; const LENGTH = 1024 * 1024 * 1;//1MB let chunks = slice(file, LENGTH);

// 獲取對於同一個文件,獲取其identifier let identifier = createIdentifier(file);

let tasks = []; chunks.forEach((chunk, index) => { let fd = new FormData(); //傳遞file對象 fd.append("file",chunk); // 傳遞identifier fd.append("identifier", identifier); // 傳遞切片索引值 fd.append("chunkNumber", index + 1); // 傳遞切片總數 fd.append(“totalChunks”, chunks.length);
tasks.push(post("/mkblk.php", fd)); });

// 所有切片上傳完畢後,調用mkfile接口 Promise.all(tasks).then(res => { let fd = new FormData(); fd.append("identifier", identifier); fd.append("totalChunks",chunks.length); post("/mkfile.php", fd).then(res => { console.log(res); }) });

後端還原分片代碼

// mkblk.php接口 $identifier = $_POST['identifier']; $path = './upload/' . $identifier; if(!is_dir($path)){ mkdir($path); } // 把同一個文件的切片放在相同的目錄下 $filename = $path . '/' . $_POST['chunkNumber’]; // 清除保存的切片 $res = move_uploaded_file($_FILES['file']['tmp_name'], $filename);

//接下來是mkfile.php接口的實現,這個接口會在所有切片上傳後調用用來合併文件

// mkfile.php接口 $identifier = $_POST['identifier']; $totalChunks= (int)$_POST['totalChunks'];

//合併後的文件名 $filename = './upload/' . $identifier . '/file.jpg’; // 開始合併文件 for($i = 1; $i <= $totalChunks; ++$i){ $file = './upload/'.$ identifier. '/' .$i; // 讀取單個切塊 // 獲取文件內容 $content = file_get_contents($file); if(!file_exists($filename)){ //創建一個用於讀寫的空文件 $fd = fopen($filename, "w+"); }else{ //追加到一個文件,寫操作向文件末尾追加數據。如果文件不存在,則創建文件。 $fd = fopen($filename, "a"); } fwrite($fd, $content);// 將切塊合併到一個文件上 }

以上代碼還需要繼續優化的點:斷點續傳、秒傳、上傳進度和暫停

1、斷點續傳

爲什麼需要斷點續傳?

即使將大文件拆分成切片上傳,我們仍需等待所有切片上傳完畢,在等待過程中,可能發生一系列導致部分切片上傳失敗的情形,如網絡故障、頁面關閉等。由於切片未全部上傳,因此無法通知服務端合成文件。這種情況下可以通過斷點續傳來進行處理。

斷點續傳指的是:可以從已經上傳部分開始繼續上傳未完成的部分,而沒有必要從頭開始上傳,節省上傳時間。 怎麼實現斷點續傳?

由於整個上傳過程是按切片維度進行的,且mkfile接口是在所有切片上傳完成後由客戶端主動調用的,因此斷點續傳的實現也十分簡單:

在切片上傳成功後,保存已上傳的切片信息

當下次傳輸相同文件時,遍歷切片列表,只選擇未上傳的切片進行上傳

所有切片上傳完畢後,再調用mkfile接口通知服務端進行文件合併

因此問題就落在瞭如何保存已上傳切片的信息了,保存一般有兩種策略

1.可以通過locaStorage等方式保存在前端瀏覽器中,這種方式不依賴於服務端,實現起來也比較方便,缺點在於如果用戶清除了本地文件,會導致上傳記錄丟失

2.服務端本身知道哪些切片已經上傳,因此可以由服務端額外提供一個根據文件context查詢已上傳切片的接口,在上傳文件前調用該文件的歷史上傳記錄 前端斷點續傳代碼

// 獲取已上傳切片記錄 function getUploadSliceRecord(context){ let record = localStorage.getItem(context) if(!record){ return [] }else { return JSON.parse(record) } }

// 保存已上傳切片 function saveUploadSliceRecord(context, sliceIndex){ let list = getUploadSliceRecord(context) list.push(sliceIndex) localStorage.setItem(context, JSON.stringify(list)) }

let context = createContext(file);

// 獲取上傳記錄 let record = getUploadSliceRecord(context); let tasks = []; chunks.forEach((chunk, index) => { // 已上傳的切片則不再重新上傳 if(record.includes(index)){ return }

let fd = new FormData();
fd.append("file", chunk);
fd.append("context", context);
fd.append("chunk", index + 1);

let task = post("/mkblk.php", fd).then(res=>{
    // 上傳成功後保存已上傳切片記錄
    saveUploadSliceRecord(context, index)
    record.push(index)
})
tasks.push(task);

}); ...

後端斷點續傳代碼

服務端實現斷點續傳的邏輯基本相似,只要在getUploadSliceRecord內部調用服務端的查詢接口獲取已上傳切片的記錄即可,因此這裏不再展開。

後端代碼優化:清除切片的時機

此外斷點續傳還需要考慮切片過期的情況

如果調用了mkfile接口,則磁盤上的切片內容就可以清除掉了,如果客戶端一直不調用mkfile的接口,放任這些切片一直保存在磁盤顯然是不可靠的,一般情況下,切片上傳都有一段時間的有效期,超過該有效期,就會被清除掉。基於上述原因,斷點續傳也必須同步切片過期的實現邏輯。 2、秒傳

什麼是秒傳?

已經上傳過的文件,並且在後端已經拼接完成,如果再次上傳的話後端不做處理,直接返回拼接好的文件的信息即可,這裏主要後端實現,由於篇幅關係,這裏不做過多描述。 3、上傳進度和暫停

通過xhr.upload中的progress方法可以實現監控每一個切片上傳進度。

上傳暫停的實現也比較簡單,通過xhr.abort可以取消當前未完成上傳切片的上傳,實現上傳暫停的效果,恢復上傳就跟斷點續傳類似,先獲取已上傳的切片列表,然後重新發送未上傳的切片。

由於篇幅關係,上傳進度和暫停的功能這裏就先不實現了。

五、目前成熟的大文件上傳方案 目前社區已經存在一些成熟的大文件上傳解決方案,也許並不需要我們手動去實現一個簡陋的大文件上傳庫,但是瞭解其原理還是十分有必要的。

推薦的前端vue組件:vue-simple-uploader,支持vue2,vue3

vue-simple-uploader是基於simple-Uploader.js封裝的大文件上傳組件,具有以下優點:

  1. 支持單文件、多文件、文件夾上傳;支持拖拽文件、文件夾上傳
  2. 可暫停、繼續上傳
  3. 錯誤處理
  4. 支持“秒傳”,通過文件判斷服務端是否已存在從而實現“秒傳”
  5. 分塊上傳
  6. 支持進度、預估剩餘時間、出錯自動重試、重傳等操作

vue-simple-uploader 內部的實現也很簡單,有興趣的同學可以去看一下源碼

六、總結 本文首先介紹了什麼是大文件,以及大文件跟普通文件在上傳時的區別,最後通過分析大文件上傳的原理和思路給出簡單的實現方案,並且推薦了一個成熟的vue大文件上傳組件:vue-simple-uploader,希望對大家有所幫助。

作者:京東物流 於俊嬌

來源:京東雲開發者社區 自猿其說 Tech 轉載請註明來源

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章