node.js中的Stream

原链接

https://segmentfault.com/a/1190000000519006?share_user=1030000004603185

转过来自己看

nodejs的fs模块并没有提供一个copy的方法,但我们可以很容易的实现一个,比如:

var source = fs.readFileSync('/path/to/source', {encoding: 'utf8'});
fs.writeFileSync('/path/to/dest', source);

这种方式是把文件内容全部读入内存,然后再写入文件,对于小型的文本文件,这没有多大问题,比如grunt-file-copy就是这样实现的。但是对于体积较大的二进制文件,比如音频、视频文件,动辄几个GB大小,如果使用这种方法,很容易使内存“爆仓”。理想的方法应该是读一部分,写一部分,不管文件有多大,只要时间允许,总会处理完成,这里就需要用到流的概念。

如上面高大上的图片所示,我们把文件比作装水的桶,而水就是文件里的内容,我们用一根管子(pipe)连接两个桶使得水从一个桶流入另一个桶,这样就慢慢的实现了大文件的复制过程。

Stream在nodejs中是EventEmitter的实现,并且有多种实现形式,例如:

  • http responses request
  • fs read write streams
  • zlib streams
  • tcp sockets
  • child process stdout and stderr

上面的文件复制可以简单实现一下:

var fs = require('fs');
var readStream = fs.createReadStream('/path/to/source');
var writeStream = fs.createWriteStream('/path/to/dest');

readStream.on('data', function(chunk) { // 当有数据流出时,写入数据
    writeStream.write(chunk);
});

readStream.on('end', function() { // 当没有数据时,关闭数据流
    writeStream.end();
});

上面的写法有一些问题,如果写入的速度跟不上读取的速度,有可能导致数据丢失。正常的情况应该是,写完一段,再读取下一段,如果没有写完的话,就让读取流先暂停,等写完再继续,于是代码可以修改为:

var fs = require('fs');
var readStream = fs.createReadStream('/path/to/source');
var writeStream = fs.createWriteStream('/path/to/dest');

readStream.on('data', function(chunk) { // 当有数据流出时,写入数据
    if (writeStream.write(chunk) === false) { // 如果没有写完,暂停读取流
        readStream.pause();
    }
});

writeStream.on('drain', function() { // 写完后,继续读取
    readStream.resume();
});

readStream.on('end', function() { // 当没有数据时,关闭数据流
    writeStream.end();
});

或者使用更直接的pipe

// pipe自动调用了data,end等事件
fs.createReadStream('/path/to/source').pipe(fs.createWriteStream('/path/to/dest'));

下面是一个更加完整的复制文件的过程

var fs = require('fs'),
    path = require('path'),
    out = process.stdout;

var filePath = '/Users/chen/Movies/Game.of.Thrones.S04E07.1080p.HDTV.x264-BATV.mkv';

var readStream = fs.createReadStream(filePath);
var writeStream = fs.createWriteStream('file.mkv');

var stat = fs.statSync(filePath);

var totalSize = stat.size;
var passedLength = 0;
var lastSize = 0;
var startTime = Date.now();

readStream.on('data', function(chunk) {

    passedLength += chunk.length;

    if (writeStream.write(chunk) === false) {
        readStream.pause();
    }
});

readStream.on('end', function() {
    writeStream.end();
});

writeStream.on('drain', function() {
    readStream.resume();
});

setTimeout(function show() {
    var percent = Math.ceil((passedLength / totalSize) * 100);
    var size = Math.ceil(passedLength / 1000000);
    var diff = size - lastSize;
    lastSize = size;
    out.clearLine();
    out.cursorTo(0);
    out.write('已完成' + size + 'MB, ' + percent + '%, 速度:' + diff * 2 + 'MB/s');
    if (passedLength < totalSize) {
        setTimeout(show, 500);
    } else {
        var endTime = Date.now();
        console.log();
        console.log('共用时:' + (endTime - startTime) / 1000 + '秒。');
    }
}, 500);

可以把上面的代码保存为copy.js试验一下

我们添加了一个递归的setTimeout(或者直接使用setInterval)来做一个旁观者,每500ms观察一次完成进度,并把已完成的大小、百分比和复制速度一并写到控制台上,当复制完成时,计算总的耗费时间,效果如图:

我们复制了一集1080p的权利的游戏第四季第7集,大概3.78G大小,由于使用了SSD,可以看到速度还是非常不错的,哈哈哈~
复制完成后,显示总花费时间

结合nodejs的readline, process.argv等模块,我们可以添加覆盖提示、强制覆盖、动态指定文件路径等完整的复制方法,有兴趣的可以实现一下,实现完成,可以

ln -s /path/to/copy.js /usr/local/bin/mycopy

这样就可以使用自己写的mycopy命令替代系统的cp命令

你可能感兴趣的文章

18 条评论
stormslowly  · 2014年06月03日

bash的cp命令花了多久时间?

  回复

kazaff  · 2014年06月03日

不太明白为什么读取速度大于写入速度时会出现数据丢失,这里面到底发生了什么,有待挖掘啊~

  回复

Ran_Aizen  · 2014年08月31日

因为缓冲空间有限..

  回复

kazaff  · 2014年09月01日

如果是写入速度大于读取速度,导致缓冲区存满溢出,引起的数据丢失我可以理解,但反过来的话,我觉得有点匪夷所思啊~

  回复

可不可以理解是还没写完就读完了,数据部分读不到,说成丢失。可以吗?

 
— 流逝的0号 · 7月11日
Ran_Aizen  · 2014年09月01日

其实他说的就是这个意思,他所说的读和写应该是针对磁盘文件而言的:读是指从文件读数据到内存,写是指将内存数据写到文件。

  回复

维尼Bernie  · 2014年12月17日

是不是意味着readStream.pipe( writeStream ) 是异步的

  回复

hechangmin  · 2014年12月26日

本来就是异步的为什么要暂停读取的操作呢?

我在本机不暂停,明显速度提升很多倍,也没遇到任何错误。

  回复

wangck1998  · 2015年07月27日

这篇文章中,有这样一句话:

好吧,每一片数据只会冒出来一次,没人消费的话它就没了;OK 你想到了用 pause-stream「暂停」一下这个流,等下再消费?完了,数据会暂存在内存中,慢慢堆积,然后,你懂了。

如果一直pause而不使用resume,内存会吃不消吗?

  回复

桔子先生  · 2015年08月03日

用createReadStream读取的文件可以直接上传么 为什么我通过这个读取的压缩文件上传以后会变大

  回复

superwf  · 2015年11月11日

我看到了game of throne

  回复

xbgxwh  · 2016年04月22日

灵魂画师。。。。

  回复

Daniel_chan  · 2016年06月16日

这种方法在拷贝图片会不会导致乱码?

  回复

Seven_Cai  · 2016年08月10日

不会

  回复

叶叶yeah  · 2016年11月02日

复制的文件被放在了哪里呢??

  回复

赵家兴  · 2016年12月02日

因为它读的速度和写的速度不一样

  回复

JasonKidd  · 7月13日

感觉不太对,因为.write操作是异步的,并不是同步

  回复

.write是同步的,是将数据写入到内部缓存buffer里面这个操作是同步的 缓存buffer的数据会写入到文件里面.如果read太快,缓存buffer的数据来不及写入到文件里面,那么缓存buffer的数据会越来越多直至爆仓数据,再写入数据的话数据就丢失了

— Amande灬无悔 · 8月11日

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值