談一談大文件上傳——前臺分片和后臺合并

歡迎光臨我的博客拓跋的前端客棧,這個是原文地址卿捎。如果您發(fā)現(xiàn)我文章中存在錯誤,請盡情向我吐槽径密,大家一起學(xué)習(xí)一起進步φ(>ω<*)

最近做了一個需求午阵,需要上傳鏡像的tar包,小的3享扔、5G底桂,大的可能會達到20多G,而要求在瀏覽器中上傳惧眠,因此普通的上傳方式肯定無法滿足需求籽懦,必須要使用到分片上傳,前臺分片后臺就需要合并氛魁,一系列做完以后踩了很多坑暮顺,在這邊總結(jié)記錄一下厅篓。

前臺分片上傳


所謂上傳,實際上就是一個把文件通過客戶端傳給服務(wù)端的過程捶码,也就是通過前端傳給后臺的過程羽氮。在這個過程中,如果要傳輸?shù)膬?nèi)容太過龐大惫恼,在傳輸過程中就容易遇到各種各樣的問題档押。為了把出現(xiàn)問題的概率控制在最低,我們往往需要把大文件分成一份一份的小文件來依次上傳祈纯,這個過程就是我們所說分片上傳令宿。

我前臺使用的是webuploader上傳插件,參考webuploader的切片方法:

    function CuteFile( file, chunkSize ) {
        var pending = [],
            blob = file.source,
            total = blob.size,
            chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1,
            start = 0,
            index = 0,
            len, api;

        api = {
            file: file,

            has: function() {
                return !!pending.length;
            },

            shift: function() {
                return pending.shift();
            },

            unshift: function( block ) {
                pending.unshift( block );
            }
        };

        while ( index < chunks ) {
            len = Math.min( chunkSize, total - start );

            pending.push({
                file: file,
                start: start,
                end: chunkSize ? (start + len) : total,
                total: total,
                chunks: chunks,
                chunk: index++,
                cuted: api
            });
            start += len;
        }

        file.blocks = pending.concat();
        file.remaning = pending.length;

        return api;
    }

其中盆繁,chunkSize表示每片大小掀淘,chunks表示切片的數(shù)目,具體切分方法也很簡單油昂,看代碼就好了。

具體在使用webuploader的過程中倾贰,只需要在創(chuàng)建webuploader實例的過程中如下配置一下:

    const uploader = WebUploader.create({
        // 文件接收服務(wù)端冕碟。
        server: './ftp/images',

        // 選擇文件的按鈕〈艺悖可選安寺。
        // 內(nèi)部根據(jù)當(dāng)前運行是創(chuàng)建,可能是input元素首尼,也可能是flash.
        pick: '#picker',

        chunked: true,//開啟分片上傳
        chunkSize: 50*1024*1024,//每片大小50M
        chunkRetry: 1,//失敗后重試次數(shù)
        threads: 1,//上傳并發(fā)數(shù)目
        fileNumLimit: 1,//驗證文件總數(shù)量易猫,超出則不允許加入隊列

    });

后臺合并


對于分片上傳上來的文件遣臼,后臺肯定要再次合并起來,重新生成跟原文件一模一樣的文件。后臺合并的方法有很多吠勘,以Node.js為例,可以使用以下方式:

buffer合并

按照慣例铜幽,先貼代碼:

    // 合并分片
    function mergeChunks(fileName, chunks, callback) {
        console.log('chunks:' + chunks);
        let chunkPaths = chunks.map(function (name) {
            return path.join(process.env.IMAGESDIR, name)
        });

        // 采用Buffer方式合并
        const readStream = function (chunkArray, cb) {
            let buffers = [];
            chunkPaths.forEach(function (path) {
                let buffer = fs.readFileSync(path);
                buffers.push(buffer);
            });

            let concatBuffer = Buffer.concat(buffers);
            let concatFilePath = path.join(process.env.IMAGESDIR, fileName);
            fs.writeFileSync(concatFilePath, concatBuffer);

            chunkPaths.forEach(function (path) {
                fs.unlinkSync(path)
            })
            cb();
        };


        readStream(chunkPaths, callback);

    }

buffer方式合并是一種常見的文件合并方式驴剔,方法是將各個分片文件分別用fs.readFile()方式讀取,然后通過Buffer.concat()進行合并跋核。

這種方法簡單易理解岖瑰,但有個最大的缺點,就是你讀取的文件有多大砂代,合并的過程占用的內(nèi)存就有多大蹋订,因為我們相當(dāng)于把這個大文件的全部內(nèi)容都一次性載入到內(nèi)存中了,這是非常低效的刻伊。同時露戒,Node默認(rèn)的緩沖區(qū)大小的上限是2GB椒功,一旦我們上傳的大文件超出2GB,那使用這種方法就會失敗玫锋。雖然可以通過修改緩沖區(qū)大小上限的方法來規(guī)避這個問題蛾茉,但是鑒于這種合并方式極吃內(nèi)存,我不建議您這么做撩鹿。

那么谦炬,有更好的方式嗎?那是當(dāng)然节沦,下面介紹一種stream合并方式键思。

stream合并

顯然,stream(流甫贯,下面都用‘流’來表示stream)就是這種更好的方式吼鳞。

按照慣例,先貼代碼:

    // 合并分片
    function mergeChunks(fileName, chunks, callback) {
        console.log('chunks:' + chunks);
        let chunkPaths = chunks.map(function (name) {
            return path.join(process.env.IMAGESDIR, name)
        });

        // 采用Stream方式合并
        let targetStream = fs.createWriteStream(path.join(process.env.IMAGESDIR, fileName));
        const readStream = function (chunkArray, cb) {
            let path = chunkArray.shift();
            let originStream = fs.createReadStream(path);
            originStream.pipe(targetStream, {end: false});
            originStream.on("end", function () {
                // 刪除文件
                fs.unlinkSync(path);
                if (chunkArray.length > 0) {
                    readStream(chunkArray, callback)
                } else {
                    cb()
                }
            });
        };

        readStream(chunkPaths, callback);

    }

為什么說流更好呢叫搁?流到底是什么呢赔桌?

流是數(shù)據(jù)的集合 —— 就像數(shù)組或字符串一樣。區(qū)別在于流中的數(shù)據(jù)可能不會立刻就全部可用渴逻,并且你無需一次性的把這些數(shù)據(jù)全部放入內(nèi)存疾党。這使得流在操作大量數(shù)據(jù)或是數(shù)據(jù)從外部來源逐段發(fā)送過來的時候變得非常有用。

換句話說惨奕,當(dāng)你使用buffer方式來處理一個2GB的文件雪位,占用的內(nèi)存可能是2GB以上,而當(dāng)你使用流來處理這個文件梨撞,可能只會占用幾十個M雹洗。這就是我們?yōu)槭裁催x擇流的原因所在。

在Node.js中卧波,有4種基本類型的流时肿,分別是可讀流,可寫流幽勒,雙向流以及變換流嗜侮。

  • 可讀流是對一個可以讀取數(shù)據(jù)的源的抽象。fs.createReadStream 方法是一個可讀流的例子啥容。
  • 可寫流是對一個可以寫入數(shù)據(jù)的目標(biāo)的抽象锈颗。fs.createWriteStream 方法是一個可寫流的例子。
  • 雙向流既是可讀的咪惠,又是可寫的击吱。TCP socket 就屬于這種。
  • 變換流是一種特殊的雙向流遥昧,它會基于寫入的數(shù)據(jù)生成可供讀取的數(shù)據(jù)覆醇。

所有的流都是EventEmitter的實例朵纷。它們發(fā)出可用于讀取或?qū)懭霐?shù)據(jù)的事件。然而永脓,我們可以利用pipe方法以一種更簡單的方式使用流中的數(shù)據(jù)袍辞。

在上面那段代碼中,我們首先通過fs.createWriteStream()創(chuàng)建了一個可寫流常摧,用來存放最終合并的文件搅吁。然后使用fs.createReadStream()分別讀取各個分片后的文件,再通過pipe()方式將讀取的數(shù)據(jù)像倒水一樣“倒”到可寫流中落午,到監(jiān)控到一杯水倒完后谎懦,馬上接著倒下一杯,直到全部倒完為止溃斋。此時界拦,全部文件合并完畢。

追加文件方式合并

追加文件方式合并指的是使用fs.appendFile()的方式來進行合并梗劫。fs.appendFile()的作用是異步地追加數(shù)據(jù)到一個文件享甸,如果文件不存在則創(chuàng)建文件。data可以是一個字符串或buffer梳侨。例:

    fs.appendFile('message.txt', 'data to append', (err) => {
      if (err) throw err;
      console.log('The "data to append" was appended to file!');
    });

使用這種方法也可以將文件合并枪萄,雖然我沒有在項目中實驗,但通過在網(wǎng)上查的資料來看猫妙,性能強過buffer合并方式,但不及流合并方式聚凹。

小結(jié)


這篇文章雖然是談前臺分片和后臺合并割坠,但是由于前臺分片主要是使用webUploader實現(xiàn)的,因此側(cè)重點都在后臺合并上妒牙。

后臺合并主要有3種方式:buffer合并彼哼、流合并、追加文件方式合并湘今。三種方式各有各的特點敢朱,但是在大文件合并上,我推薦使用流方式合并摩瞎,流合并占內(nèi)存最少拴签,效率最高,是處理大文件的最佳選擇旗们。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蚓哩,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子上渴,更是在濱河造成了極大的恐慌岸梨,老刑警劉巖喜颁,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異曹阔,居然都是意外死亡半开,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門赃份,熙熙樓的掌柜王于貴愁眉苦臉地迎上來寂拆,“玉大人,你說我怎么就攤上這事芥炭±炜猓” “怎么了?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵园蝠,是天一觀的道長渺蒿。 經(jīng)常有香客問我,道長彪薛,這世上最難降的妖魔是什么茂装? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮善延,結(jié)果婚禮上少态,老公的妹妹穿的比我還像新娘。我一直安慰自己易遣,他們只是感情好彼妻,可當(dāng)我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著豆茫,像睡著了一般侨歉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上揩魂,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天幽邓,我揣著相機與錄音,去河邊找鬼火脉。 笑死牵舵,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的倦挂。 我是一名探鬼主播畸颅,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼妒峦!你這毒婦竟也來了重斑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤肯骇,失蹤者是張志新(化名)和其女友劉穎窥浪,沒想到半個月后祖很,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡漾脂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年假颇,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片骨稿。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡笨鸡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出坦冠,到底是詐尸還是另有隱情形耗,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布辙浑,位于F島的核電站激涤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏判呕。R本人自食惡果不足惜倦踢,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望侠草。 院中可真熱鬧辱挥,春花似錦、人聲如沸边涕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽功蜓。三九已至哼蛆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間霞赫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工肥矢, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留端衰,地道東北人。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓甘改,卻偏偏與公主長得像旅东,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子十艾,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內(nèi)容