Stream 是一个抽象接口,Node 中有很多对象实现了这个接口。例如,对http 服务器发起请求的request 对象就是一个 Stream,还有stdout(标准输出)。
Node.js,Stream 有四种流类型:
-
Readable - 可读操作。 Writable - 可写操作。 Duplex - 可读可写操作. Transform - 操作被写入数据,然后读出结果。
所有的 Stream 对象都是 EventEmitter 的实例。常用的事件有:
-
data - 当有数据可读时触发。 end - 没有更多的数据可读时触发。 error - 在接收和写入过程中发生错误时触发。 finish - 所有数据已被写入到底层系统时触发。
Readable 读取流
Readable流提供了一种将外部来源(比如文件、套接字等)的数据读入到应用程序的机制。
可读的流有两种模式:流动模式和暂停模式。流动模式下,数据会自动从来源流出,跟不老泉似的,直到来源的数据耗尽。暂停模式下,你得通过stream.read()主动去要数据,你要了它才从来源读,你不要它就在那儿耗着等你。
可读流在创建时都是暂停模式。暂停模式和流动模式可以互相转换。
要从暂停模式切换到流动模式,有下面三种办法:
- 给“data”事件关联了一个处理器
- 显式调用resume()
- 调用pipe()将可读流桥接到一个可写流上
要从流动模式切换到暂停模式,有两种途径:
- 如果这个可读的流没有桥接可写流组成管道,直接调用pause()
- 如果这个可读的流与若干可写流组成了管道,需要移除与“data”事件关联的所有处理器,并且调用unpipe()方法断开所有管道。
需要注意的是,出于向后兼容的原因,移除“data”事件的处理器,可读流并不会自动从流动模式转换到暂停模式;还有,对于已组成管道的可读流,调用pause也不能保证这个流会转换到暂停模式。
Readable流的一些常见实例如下:
- 客户端的HTTP响应
- 服务端的HTTP请求
- fs读取流
- zlib流
- crypto(加密)流
- TCP套接字
- 子进程的stdout和stderr
- process.stdin
Readable流提供了以下事件:
- readable:在数据块可以从流中读取的时候发出。它对应的处理器没有参数,可以在处理器里调用read([size])方法读取数据。
- data:有数据可读时发出。它对应的处理器有一个参数,代表数据。如果你只想快快地读取一个流的数据,给data关联一个处理器是最方便的办法。处理器的参数是Buffer对象,如果你调用了Readable的setEncoding(encoding)方法,处理器的参数就是String对象。
- end:当数据被读完时发出。对应的处理器没有参数。
- close:当底层的资源,如文件,已关闭时发出。不是所有的Readable流都会发出这个事件。对应的处理器没有参数。
- error:当在接收数据中出现错误时发出。对应的处理器参数是Error的实例,它的message属性描述了错误原因,stack属性保存了发生错误时的堆栈信息。
Readable还提供了一些函数,我们可以用它们读取或操作流:
- read([size]):如果你给read方法传递了一个大小作为参数,那它会返回指定数量的数据,如果数据不足,就会返回null。如果你不给read方法传参,它会返回内部缓冲区里的所有数据,如果没有数据,会返回null,此时有可能说明遇到了文件末尾。read返回的数据可能是Buffer对象,也可能是String对象。
- setEncoding(encoding):给流设置一个编码格式,用于解码读到的数据。调用此方法后,read([size])方法返回String对象。
- pause():暂停可读流,不再发出data事件
- resume():恢复可读流,继续发出data事件
- pipe(destination,[options]):把这个可读流的输出传递给destination指定的Writable流,两个流组成一个管道。options是一个JS对象,这个对象有一个布尔类型的end属性,默认值为true,当end为true时,Readable结束时自动结束Writable。注意,我们可以把一个Readable与若干Writable连在一起,组成多个管道,每一个Writable都能得到同样的数据。这个方法返回destination,如果destination本身又是Readable流,就可以级联调用pipe(比如我们在使用gzip压缩、解压缩时就会这样,马上会讲到)。
- unpipe([destination]):端口与指定destination的管道。不传递destination时,断开与这个可读流连在一起的所有管道。
- isPaused() :判断是否已经是
好吧,大概就这些了,我们来举一个简单的使用Readable的例子。以fs模块为例吧。
fs.ReadStream实现了stream.Readable,另外还提供了一个“open”事件,你可以给这个事件关联处理器,处理器的参数是文件描述符(一个整型数)。
fs.createReadStream(path[, options])用来打开一个可读的文件流,它返回一个fs.ReadStream对象。path参数指定文件的路径,可选的options是一个JS对象,可以指定一些选项,类似下面这样:
options 参数说明
{
flags: 'r', //指定用什么模式打开文件,’w’代表写,’r’代表读,类似的还有’r+’、’w+’、’a’等
encoding: null, //默认就是“utf8”,你还可以为它指定”ascii”或”base64”
fd: null, //当你指定了这个属性时,createReadableStream会根据传入的fd创建一个流,忽略path。
mode: 0o666,
autoClose: true, //当发生错误或文件读取结束时会自动关闭文件描述符。
highWaterMark :64,//要知道,不同的设置为一个可读流highwatermark默认值(16 KB),此方法返回的流具有相同的参数的默认值64 KB。
start:0,
end:0 //选项可以包括开始和结束值,从文件中读取一个字节范围,而不是整个文件。开始和结束都是包容性的,并开始在0。该编码可以是任何一个接受的缓冲区。
}
实例一:
var fs=require('fs');
var path=require('path');
var file=path.resolve('/test1/one.txt');
//指定开始位置,结束位置读取文件
var readable=fs.createReadStream(file,{
highWaterMark:6,
start:6,
end:12
});
readable.on('open',function(fd){
console.log('打开文件成功,句柄:'+fd); // 打开文件成功,句柄:3
});
readable.on('data',function(chunk){
console.info(chunk.toString('ascii')); //789012
});
readable.on('end',function(){
console.log('读取结束'); //读取结束
});
readable.on('close',()=>{
console.log('读取关闭'); //读取关闭
});
readable.on('error',function(err){
console.log('读取异常,'+err); //当出现异常时触发
});
Writable 写入流
Writable流提供了一个接口,用来把数据写入到目的设备(或内存)中。Writable流的一些常见实例:
- 客户端的HTTP请求
- 服务器的HTTP响应
- fs写入流
- zlib流
- crypto(加密)流
- TCP套结字
- 子进程的stdin
- process.stdout和process.stderr
Writable流的write(chunk[,encoding] [,callback])方法可以把数据写入流中。其中,chunk是待写入的数据,是Buffer或String对象。这个参数是必须的,其它参数都是可选的。如果chunk是String对象,encoding可以用来指定字符串的编码格式,write会根据编码格式将chunk解码成字节流再来写入。callback是数据完全刷新到流中时会执行的回调函数。write方法返回布尔值,当数据被完全处理后返回true(不一定是完全写入设备哦)。
Writable流的end([chunk] [,encoding] [,callback])方法可以用来结束一个可写流。它的三个参数都是可选的。chunk和encoding的含义与write方法类似。callback是一个可选的回调,当你提供它时,它会被关联到Writable的finish事件上,这样当finish事件发射时它就会被调用。
Writable还有setDefaultEncoding等方法,具体可以参考在线文档。
现在我们来看看Writable公开的事件:
- finish: 在end()被调用、所有数据都已被写入底层设备后发射。对应的处理器函数没有参数。
- drain: 在write()方法,写入文件缓存清空时触发
- pipe: 当你在Readable流上调用pipe()方法时,Writable流会发射这个事件,对应的处理器函数有一个参数,类型是Readable,指向与它连接的那个Readable流。
- unpipe: 当你在Readable流上调用unpipe()方法时,Writable流会发射这个事件,对应的处理器函数有一个参数,类型是Readable,指向与刚与它断开连接的那个Readable流。
- error: 出错时发射,对应的处理器函数的参数是Error对象。
fs.createWriteStream(path[, options])
返回 WriteStream 实例对象,默认值不同,参数使用与ReadStream相似
options 常用参数
{
flags: 'w',
defaultEncoding: 'utf8',
fd: null,
mode: 0o666,
start:0,//指定写入的开始位置
}
var fs=require('fs');
var path=require('path');
var file=path.resolve('/test1/write.txt');
var writeable=fs.createWriteStream(file,{
highWaterMark:6,
defaultEncoding:'utf8', //设置写入时默认的编码
flags:'w' //如果不存在则创建
});
//当前写入完成时触发
//调用end()方法时触发
writeable.on('finish',function(){
console.log('写入完成');
process.exit(0); //程序退出
});
//当前出现异常时触发
writeable.on('error',function(err){
console.log('写入异常,'+err);
});
writeable.write('中文abcdefghijk');
//writeable.write('中文字符','ascii'); //显示指定写入编码,如果文件中有内容,同时也转码成ascii
//标记结束
writeable.end();
Duplex 流
sock.Socket是Duplex流,既实现了Readable又实现了Writable,所以,sock.pipe(sock)是正确的调用。
常见的Duplex流有:
- TCP socket
- zlib
- crypto
Duplex是Readable和Writable的合体。
Transform 流
Transform扩展了Duplex流,它会修改你使用Writable接口写入的数据,当你用Readable接口来读时,数据已经发生了变化。
比较常见的Transform流有:
- zlib
- crypto
压缩文件
//实例1,文件压缩
var fs=require('fs');
var zlib=require('zlib');
var path=require('path');
var file1=path.resolve('/test1/one.txt');
var file3=path.resolve('/test1/one.txt.gz');
//压缩文件
var readable1=fs.createReadStream(file1);
var writeable=fs.createWriteStream(file3);
readable1
.pipe(zlib.createGzip())
.pipe(writeable);
console.log('压缩文件完成');
解压文件
//解压文件实例
var fs=require('fs');
var zlib=require('zlib');
var path=require('path');
var file1=path.resolve('/test1/one.txt.gz');
//解压文件
var readable=fs.createReadStream(file1);
readable
.pipe(zlib.createGunzip())
.pipe(fs.createWriteStream('one.gz.txt'));
readable.on('end',function(){
console.log('解压文件完毕');
});
console.log('代码执行完毕');