谈一谈大文件上传——前台分片和后台合并

欢迎光临我的博客拓跋的前端客栈,这个是原文地址。如果您发现我文章中存在错误,请尽情向我吐槽,大家一起学习一起进步φ(>ω<*)

最近做了一个需求,需要上传镜像的tar包,小的3、5G,大的可能会达到20多G,而要求在浏览器中上传,因此普通的上传方式肯定无法满足需求,必须要使用到分片上传,前台分片后台就需要合并,一系列做完以后踩了很多坑,在这边总结记录一下。

前台分片上传


所谓上传,实际上就是一个把文件通过客户端传给服务端的过程,也就是通过前端传给后台的过程。在这个过程中,如果要传输的内容太过庞大,在传输过程中就容易遇到各种各样的问题。为了把出现问题的概率控制在最低,我们往往需要把大文件分成一份一份的小文件来依次上传,这个过程就是我们所说分片上传。

我前台使用的是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表示切片的数目,具体切分方法也很简单,看代码就好了。

具体在使用webuploader的过程中,只需要在创建webuploader实例的过程中如下配置一下:

    const uploader = WebUploader.create({
        // 文件接收服务端。
        server: './ftp/images',

        // 选择文件的按钮。可选。
        // 内部根据当前运行是创建,可能是input元素,也可能是flash.
        pick: '#picker',

        chunked: true,//开启分片上传
        chunkSize: 50*1024*1024,//每片大小50M
        chunkRetry: 1,//失败后重试次数
        threads: 1,//上传并发数目
        fileNumLimit: 1,//验证文件总数量,超出则不允许加入队列

    });

后台合并


对于分片上传上来的文件,后台肯定要再次合并起来,重新生成跟原文件一模一样的文件。后台合并的方法有很多,以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()进行合并。

这种方法简单易理解,但有个最大的缺点,就是你读取的文件有多大,合并的过程占用的内存就有多大,因为我们相当于把这个大文件的全部内容都一次性载入到内存中了,这是非常低效的。同时,Node默认的缓冲区大小的上限是2GB,一旦我们上传的大文件超出2GB,那使用这种方法就会失败。虽然可以通过修改缓冲区大小上限的方法来规避这个问题,但是鉴于这种合并方式极吃内存,我不建议您这么做。

那么,有更好的方式吗?那是当然,下面介绍一种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);

    }

为什么说流更好呢?流到底是什么呢?

流是数据的集合 —— 就像数组或字符串一样。区别在于流中的数据可能不会立刻就全部可用,并且你无需一次性的把这些数据全部放入内存。这使得流在操作大量数据或是数据从外部来源逐段发送过来的时候变得非常有用。

换句话说,当你使用buffer方式来处理一个2GB的文件,占用的内存可能是2GB以上,而当你使用流来处理这个文件,可能只会占用几十个M。这就是我们为什么选择流的原因所在。

在Node.js中,有4种基本类型的流,分别是可读流,可写流,双向流以及变换流。

  • 可读流是对一个可以读取数据的源的抽象。fs.createReadStream 方法是一个可读流的例子。
  • 可写流是对一个可以写入数据的目标的抽象。fs.createWriteStream 方法是一个可写流的例子。
  • 双向流既是可读的,又是可写的。TCP socket 就属于这种。
  • 变换流是一种特殊的双向流,它会基于写入的数据生成可供读取的数据。

所有的流都是EventEmitter的实例。它们发出可用于读取或写入数据的事件。然而,我们可以利用pipe方法以一种更简单的方式使用流中的数据。

在上面那段代码中,我们首先通过fs.createWriteStream()创建了一个可写流,用来存放最终合并的文件。然后使用fs.createReadStream()分别读取各个分片后的文件,再通过pipe()方式将读取的数据像倒水一样“倒”到可写流中,到监控到一杯水倒完后,马上接着倒下一杯,直到全部倒完为止。此时,全部文件合并完毕。

追加文件方式合并

追加文件方式合并指的是使用fs.appendFile()的方式来进行合并。fs.appendFile()的作用是异步地追加数据到一个文件,如果文件不存在则创建文件。data可以是一个字符串或buffer。例:

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

使用这种方法也可以将文件合并,虽然我没有在项目中实验,但通过在网上查的资料来看,性能强过buffer合并方式,但不及流合并方式。

小结


这篇文章虽然是谈前台分片和后台合并,但是由于前台分片主要是使用webUploader实现的,因此侧重点都在后台合并上。

后台合并主要有3种方式:buffer合并、流合并、追加文件方式合并。三种方式各有各的特点,但是在大文件合并上,我推荐使用流方式合并,流合并占内存最少,效率最高,是处理大文件的最佳选择。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容