Node.js Stream(流)

流的概念

  • 流是一组有序的,有起点和终点的字节数据传输手段
  • 它不关心文件的整体内容,只关注是否从文件中读到了数据,以及读到数据之后的处理
  • 流(stream)在Node.js中是一个抽象接口,被Node中的很多对象所实现。比如HTTP 服务器request和response对象都是流
  • 流可以是可读的、可写的,或是可读写的。所有的流都是 EventEmitter 的实例。

可读流

一、两种模式

可读流会工作在下面两种模式之一

  • flowing模式:可读流 自动(不断的) 从底层读取数据(直到读取完毕),并通过EventEmitter 接口的事件尽快将数据提供给应用
  • paused模式:必须显示调用stream.read() 方法来从流中读取数据片段

所有初始工作模式为 paused 的可读流,可以通过下面三种途径切换到 flowing 模式:

  • 监听 'data' 事件。
  • 调用 stream.resume() 方法。
  • 调用 stream.pipe() 方法将数据发送到 Writable。

可读流可以通过下面途径切换到 paused 模式:

  • 如果不存在管道目标(pipe destination),可以通过调用 stream.pause() 方法实现。
  • 如果存在管道目标,可以通过取消 'data' 事件监听,并调用 stream.unpipe() 方法移除所有管道目标来实现。

下面演示如何从流中读取数据
注:文件1.txt中的内容是1234567890

let fs = require("fs");
let rs = fs.createReadStream('1.txt',{   //这些参数是可选的,不需要精细控制可以不设置
    flags:'r',      //文件的操作是读取操作
    encoding:'utf8',//默认是null,null代表buffer,会按照encoding输出内容
    highWaterMark:3,//单位是字节,表示一次读取多少字节,默认是64k
    autoClose:true,//读完是否自动关闭
    start:0,       //读取的起始位置
    end:9          //读取的结束位置,包括9这个位置的内容
})
//rs.setEncoding('utf8');  //可以设置编码方式

rs.on('open',function(){
    console.log('open');
})

rs.on('data',function(data){
     console.log('data');
})

rs.on('error',function(){
    console.log('error');
})
rs.on('end',function(){
    console.log('end');
})
rs.on('close',function(){
    console.log('close');
})
复制代码

执行结果

open
123
456
789
0
end
close
复制代码

1、fs.createReadStream创建可读流实例时,默认打开文件,触发open事件(并不是每个流都会触发open事件),但此时并不会将文件中的内容输出(因为处于‘暂停模式’,没有事件消费),而是将数据存储到内部的缓冲器buffer,buffer的大小取决于highWaterMark参数,读取大小达到highWaterMark指定的阈值时,流会暂停从底层资源读取数据,直到当前缓冲器的数据被消费

2、这里的rs可以理解为流的消费者,当消费者监听了'data'事件时,就开始消费数据,可读流会从paused切换到flowing“流动模式”,不断的向消费者提供数据,直到没有数据

3、从打印结果可以看出,可读流每次读取highWaterMark个数据,交给消费者,所以先打印123,再打印456 ... ...

4、当读完文件,也就是数据被完全消费后,触发end事件

5、最后流或者底层资源文件关闭后,这里就是1.txt这个文件关闭后,触发close事件

6、error事件通常会在底层系统内部出错从而不能产生数据,或当流的实现试图传递错误数据时发生。

7、fs.createReadStream第二个参数是可选的,可不填,或只设置部分,比如编码,不需要精细控制可以不设置

模式切换
rs.on('data',function(data){ // 暂停模式 -> 流动模式
    console.log(data);
    rs.pause(); // 暂停方法 表示暂停读取,暂停data事件触发
});
setTimeout(function () {
    rs.resume(); //恢复data事件触发,变为流动模式
},1000)
//结果 open  123  456
复制代码

1、上例当监听data事件时,可读流处于flowing模式,调用了pause()方法,会暂停data事件的触发,切换到paused模式
2、resume()可以恢复data事件触发,再切换到flowing模式
3、上例中,setTimeout中切换流到flowing模式后,data事件触发,但又遇到pause(),所以暂停了输出,只打印到6

注意: 如果可读流切换到 flowing 模式,且没有消费者处理流中的数据,这些数据将会丢失。 比如, 调用了可读流的resume() 方法却没有监听 'data' 事件,或是取消了 'data' 事件监听,就有可能出现这种情况。

二、readable事件

let fs = require('fs');
let rs = fs.createReadStream('1.txt',{
    highWaterMark:2
});

rs.on('readable',function(){
    console.log('begin');
    let result = rs.read(2);
    console.log('result '+result);
});
复制代码

'readable' 事件将在流中有数据可供读取时触发
上面已经说过,当我们创建可读流时,就会先把缓存区填满(highWaterMark为指定的单次缓存区大小),等待消费
如果缓存区被清空(消费)后,会触发readable事件
到达流数据尾部时,readable事件也会触发,触发顺序在end事件之前

rs.read(size)
  • 该方法从内部缓冲区中收取并返回一些数据,如果没有可读数据,返回null
  • size是可选的,指定要读取size个字节,如果没有指定,内部缓冲区所包含的所有数据将返回
  • 如果size字节不可读,返回null,如果此时流没有结束(除非流已经结束),会将所有保留在内部缓冲区的数据将被返回。比如:文件中有1个可读字节,但是指定size为2,这时调用read(2)会返回null,如果流没有结束,那么会再次触发readable事件,将已经读到内部缓冲区中的那一个字节也返回
  • rs.read()方法只应该在暂停模式下的可读流上运行,在流动模式下,read会自动调用,直到内部缓冲区数据完全耗尽

所以,上例中,如果文件1.txt中内容是 a,输出结果

begin
result null
begin
result a
复制代码

说明:highWaterMark是2,但文件只有a,所以只有1个字节在缓存区,而size指定了2,2个字节被认为是不可读的,返回null;再次触发readable,将缓存区内容全部返回

如果内容是ab

begin
result ab
begin
result null
复制代码

说明:highWaterMark是2,所以一开始缓存中有2个字节,size指定了2,所以将ab全部读取,缓存清空——>继续缓存,发现到文件末尾,于是触发readable返回null

如果内容是abc,输出

begin
result ab
begin
result null
begin
result c
复制代码

说明:一开始缓存了2个,被消费掉,继续缓存c,并触发readable,再次read(2),此时没有2个字节的数据,被认为是不可读的,返回null,并且再次触发readable将缓存中剩余数据读取返回

如果内容是abcd,输出

begin
result ab
begin
result cd
begin
result null
复制代码

说明:先读完2个字节,即ab输出,缓存区被清空,所以会再次触发readable事件,再read(2)读出cd,继续自动缓存,发现到了文件末尾,又会触发readable,返回null

在某些情况下,为 'readable' 事件添加回调将会导致一些数据被读取到内部缓存中

这句话我的理解是,当消费数据大小 < 缓存区大小,可读流会自动添加highWaterMark个数据到缓存,那么新添加的数据和之前缓存区中未被消费的数据加一起,有可能超过了highWaterMark大小,即缓存区大小增加了

下面将highWaterMark改为3,read(1)再来看看怎么执行的

let rs = fs.createReadStream('1.txt',{
    highWaterMark:3
});
rs.on('readable',function(){
    console.log('begin');
    let result = rs.read(1);
    console.log('result '+result);
});
复制代码

当1.txt内容是 a,输出

begin
result a
begin
result null
复制代码

说明:缓存中只有a,也只读了一个(read(1)),消费后,缓存区清空,再去读取时,已经到了文件末尾,返回null

当1.txt内容是 ab,输出

begin
result a
begin
result b
复制代码

说明:缓存中有ab,当读完a后,继续缓存,发现到了文件末尾,触发readable,而此时缓存中还有b,因此将b返回

当读取个数size > 缓存区个数,会去更改缓存区的大小highWaterMark(规则为找满足>=size的最小的2的几次方)
let rs = fs.createReadStream('1.txt',{
    highWaterMark:3
});

rs.on('readable',function(){
    console.log('begin');
    let result = rs.read(4);
    console.log('result '+result);
});
复制代码

当1.txt中内容是abcdefgh,输出

begin
result null
begin
result abcd
复制代码

说明:读取的size(4)>缓存,认为是不可读的,size返回null;这时会重新计算highWaterMark大小,离4最近的是2的2次方,为4,所以highWaterMark此时等于4,返回了abcd;继续缓存efgh

但如果1.txt内容是abcdefg,输出

begin
result null
begin
result abcd
begin
result efg
复制代码

同上,但当返回abcd继续自动缓存4个时,发现读到文件末尾,将缓存数据返回,所以efg也输出

可写流

可写流是对数据写入'目的地'的一种抽象。

可写流基本用法

let fs = require('fs');
let ws = fs.createWriteStream('./1.txt',{
    flags:'w',
    mode:0o666,
    autoClose:true,
    highWaterMark:3, // 默认是16k ,而createReadStream是64k
    encoding:'utf8',//默认是utf8
    start:0
});
for(let i = 0;i<4;i++){
    let flag =  ws.write(i+'');
    console.log(flag)
}
ws.end("ok");// 标记文件末尾

ws.on('open',function(){
    console.log('open')
});

ws.on('error',function(err){
    console.log(err);
});

ws.on('finish',function(err){
    console.log('finish');
});

ws.on('close',function(){
    console.log('close')
});
复制代码

打印结果

true
true
false
false
open
finish
close
复制代码

写入文件1.txt的结果 0123ok

1、fs.createWriteStream创建可写流,同样默认会打开文件

2、可写流通过反复调用 ws.write(chunk) 方法将数据放到内部缓冲器
写入的数据chunk必须是字符串或者buffer
write虽然是个异步方法,但有返回值,这个返回值flag的含义,不是文件是否写入,而是表示能否继续写入
即缓冲器总大小 < highWaterMark时,可以继续写入,flag为true;
一旦内部缓冲器大小达到或超过highWaterMark,flag返回false;
注意,即使flag为flase,写入的内容也不会丢失

3、上例中指定的highWaterMark是3,调用write时一次写入了一个字节,当调用第三次write方法时,缓冲器中的数据大小达到3这个阈值,开始返回flase,所以先打印了两次true,后打印了两次false

4、ws.end("ok"); end方法用来标记文件末尾,表示接下来没有数据要写入可写流;
可以传入可选的 chunk 和 encoding 参数,在关闭流之前再写入一段数据;
如果传入了可选的 callback 函数,它将作为 'finish' 事件的回调函数。所以'ok'会被写入文件末尾。
注意,ws.write()方法必须在ws.end()方法之前调用

5、在调用了 ws.end() 方法,且缓冲区数据都已经传给底层系统(这里是文件1.txt)之后, 'finish' 事件将被触发。

6、'close' 事件将在流或其底层资源(比如一个文件)关闭后触发。'close'事件触发后,该流将不会再触发任何事件。不是所有 可写流/可读流 都会触发 'close' 事件。

drain事件

如果调用 stream.write(chunk) 方法返回 false,'drain' 事件会在适合恢复写入数据到流的时候触发。

drain触发条件

  • 缓冲器满了,即write返回false
  • 缓冲器的数据都写入到流,即数据都被消费掉后,才会触发

将上例中for循环改为如下

let i = 8;
function write(){
    let flag = true;
    while(i>0&&flag){
        flag = ws.write(--i+'','utf8',()=>{});
        console.log(flag)
    }

    if(i <= 0){
        ws.end("ok");
    }
 }
 write();
 // drain只有当缓存区充满后 ,并且被消费后触发
 ws.on('drain',function(){
   console.log('drain');
   write();
 });
复制代码

打印

true
true
false
open
drain
true
true
false
drain
true
true
finish
close
复制代码

文件1.txt写入 76543210ok

上例当write返回为false,即缓冲器满了时,停止while循环,等待;当缓冲器数据都写入1.txt之后,会触发drain事件,这时继续write,直到写到0,停止写入,调用end,在文件末尾写入ok,关闭文件

管道流 & pipe事件

管道提供了一个输出流到输入流的机制。通常我们用于从一个流中获取数据并将数据传递到另外一个流中

如下,将1.txt的内容,按照读一点,写一点的方式 写入2.txt

let fs = require('fs');
let rs = fs.createReadStream('1.txt',{
    highWaterMark:4
});
let ws = fs.createWriteStream('2.txt',{
    highWaterMark:3
});
rs.pipe(ws);   //可读流上调用pipe()方法,pipe方法就是读一点写一点
复制代码

这段代码工作原理类似于下面这段代码

rs.on('data',function(chunk){ // chunk 读到的内容
    let flag = ws.write(chunk);
    if(!flag){  //如果缓冲器满了,写不下了,就停止读
        rs.pause();
    }
});
ws.on('drain',function(){ //当缓存都写到文件了,恢复读
    console.log('写一点');
    rs.resume();
});
复制代码

参考资料 1、nodejs.cn/api/stream.…
2、www.runoob.com/nodejs/node…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值