大文件上傳如何做斷點續傳?

一、是什麼

不管怎樣簡單的需求,在量級達到一定層次時,都會變得異常複雜

文件上傳簡單,文件變大就複雜

上傳大文件時,以下幾個變量會影響我們的用戶體驗

  • 服務器處理數據的能力
  • 請求超時
  • 網絡波動

上傳時間會變長,高頻次文件上傳失敗,失敗後又需要重新上傳等等

爲了解決上述問題,我們需要對大文件上傳單獨處理

這裏涉及到分片上傳及斷點續傳兩個概念

分片上傳

分片上傳,就是將所要上傳的文件,按照一定的大小,將整個文件分隔成多個數據塊(Part)來進行分片上傳

如下圖

上傳完之後再由服務端對所有上傳的文件進行彙總整合成原始的文件

大致流程如下:

  1. 將需要上傳的文件按照一定的分割規則,分割成相同大小的數據塊;
  2. 初始化一個分片上傳任務,返回本次分片上傳唯一標識;
  3. 按照一定的策略(串行或並行)發送各個分片數據塊;
  4. 發送完成後,服務端根據判斷數據上傳是否完整,如果完整,則進行數據塊合成得到原始文件

斷點續傳

斷點續傳指的是在下載或上傳時,將下載或上傳任務人爲的劃分爲幾個部分

每一個部分採用一個線程進行上傳或下載,如果碰到網絡故障,可以從已經上傳或下載的部分開始繼續上傳下載未完成的部分,而沒有必要從頭開始上傳下載。用戶可以節省時間,提高速度

一般實現方式有兩種:

  • 服務器端返回,告知從哪開始
  • 瀏覽器端自行處理

上傳過程中將文件在服務器寫爲臨時文件,等全部寫完了(文件上傳完),將此臨時文件重命名爲正式文件即可

如果中途上傳中斷過,下次上傳的時候根據當前臨時文件大小,作爲在客戶端讀取文件的偏移量,從此位置繼續讀取文件數據塊,上傳到服務器從此偏移量繼續寫入文件即可

二、實現思路

整體思路比較簡單,拿到文件,保存文件唯一性標識,切割文件,分段上傳,每次上傳一段,根據唯一性標識判斷文件上傳進度,直到文件的全部片段上傳完畢

下面的內容都是僞代碼

讀取文件內容:

const input = document.querySelector('input');
input.addEventListener('change', function() {
    var file = this.files[0];
});

可以使用md5實現文件的唯一性

const md5code = md5(file);

然後開始對文件進行分割

var reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.addEventListener("load", function(e) {
    //每10M切割一段,這裏只做一個切割演示,實際切割需要循環切割,
    var slice = e.target.result.slice(0, 10*1024*1024);
});

h5上傳一個(一片)

const formdata = new FormData();
formdata.append('0', slice);
//這裏是有一個坑的,部分設備無法獲取文件名稱,和文件類型,這個在最後給出解決方案
formdata.append('filename', file.filename);
var xhr = new XMLHttpRequest();
xhr.addEventListener('load', function() {
    //xhr.responseText
});
xhr.open('POST', '');
xhr.send(formdata);
xhr.addEventListener('progress', updateProgress);
xhr.upload.addEventListener('progress', updateProgress);

function updateProgress(event) {
    if (event.lengthComputable) {
        //進度條
    }
}

這裏給出常見的圖片和視頻的文件類型判斷

function checkFileType(type, file, back) {
/**
* type png jpg mp4 ...
* file input.change=> this.files[0]
* back callback(boolean)
*/
    var args = arguments;
    if (args.length != 3) {
        back(0);
    }
    var type = args[0]; // type = '(png|jpg)' , 'png'
    var file = args[1];
    var back = typeof args[2] == 'function' ? args[2] : function() {};
    if (file.type == '') {
        // 如果系統無法獲取文件類型,則讀取二進制流,對二進制進行解析文件類型
        var imgType = [
            'ff d8 ff', //jpg
            '89 50 4e', //png

            '0 0 0 14 66 74 79 70 69 73 6F 6D', //mp4
            '0 0 0 18 66 74 79 70 33 67 70 35', //mp4
            '0 0 0 0 66 74 79 70 33 67 70 35', //mp4
            '0 0 0 0 66 74 79 70 4D 53 4E 56', //mp4
            '0 0 0 0 66 74 79 70 69 73 6F 6D', //mp4

            '0 0 0 18 66 74 79 70 6D 70 34 32', //m4v
            '0 0 0 0 66 74 79 70 6D 70 34 32', //m4v

            '0 0 0 14 66 74 79 70 71 74 20 20', //mov
            '0 0 0 0 66 74 79 70 71 74 20 20', //mov
            '0 0 0 0 6D 6F 6F 76', //mov

            '4F 67 67 53 0 02', //ogg
            '1A 45 DF A3', //ogg

            '52 49 46 46 x x x x 41 56 49 20', //avi (RIFF fileSize fileType LIST)(52 49 46 46,DC 6C 57 09,41 56 49 20,4C 49 53 54)
        ];
        var typeName = [
            'jpg',
            'png',
            'mp4',
            'mp4',
            'mp4',
            'mp4',
            'mp4',
            'm4v',
            'm4v',
            'mov',
            'mov',
            'mov',
            'ogg',
            'ogg',
            'avi',
        ];
        var sliceSize = /png|jpg|jpeg/.test(type) ? 3 : 12;
        var reader = new FileReader();
        reader.readAsArrayBuffer(file);
        reader.addEventListener("load", function(e) {
            var slice = e.target.result.slice(0, sliceSize);
            reader = null;
            if (slice && slice.byteLength == sliceSize) {
                var view = new Uint8Array(slice);
                var arr = [];
                view.forEach(function(v) {
                    arr.push(v.toString(16));
                });
                view = null;
                var idx = arr.join(' ').indexOf(imgType);
                if (idx > -1) {
                    back(typeName[idx]);
                } else {
                    arr = arr.map(function(v) {
                        if (i > 3 && i < 8) {
                            return 'x';
                        }
                        return v;
                    });
                    var idx = arr.join(' ').indexOf(imgType);
                    if (idx > -1) {
                        back(typeName[idx]);
                    } else {
                        back(false);
                    }

                }
            } else {
                back(false);
            }

        });
    } else {
        var type = file.name.match(/\.(\w+)$/)[1];
        back(type);
    }
}

調用方法如下

checkFileType('(mov|mp4|avi)',file,function(fileType){
    // fileType = mp4,
    // 如果file的類型不在枚舉之列,則返回false
});

上面上傳文件的一步,可以改成:

formdata.append('filename', md5code+'.'+fileType);

有了切割上傳後,也就有了文件唯一標識信息,斷點續傳變成了後臺的一個小小的邏輯判斷

後端主要做的內容爲:根據前端傳給後臺的md5值,到服務器磁盤查找是否有之前未完成的文件合併信息(也就是未完成的半成品文件切片),取到之後根據上傳切片的數量,返回數據告訴前端開始從第幾節上傳

如果想要暫停切片的上傳,可以使用XMLHttpRequestabort方法

三、使用場景

  • 大文件加速上傳:當文件大小超過預期大小時,使用分片上傳可實現並行上傳多個 Part, 以加快上傳速度
  • 網絡環境較差:建議使用分片上傳。當出現上傳失敗的時候,僅需重傳失敗的Part
  • 流式上傳:可以在需要上傳的文件大小還不確定的情況下開始上傳。這種場景在視頻監控等行業應用中比較常見

小結

當前的僞代碼,只是提供一個簡單的思路,想要把事情做到極致,我們還需要考慮到更多場景,比如

  • 切片上傳失敗怎麼辦
  • 上傳過程中刷新頁面怎麼辦
  • 如何進行並行上傳
  • 切片什麼時候按數量切,什麼時候按大小切
  • 如何結合 Web Work 處理大文件上傳
  • 如何實現秒傳

人生又何嘗不是如此,極致的人生體驗有無限可能,越是後面才發現越是精彩 _

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