Stream 是Node.js中最重要的组件和模式之一,在构建较复杂的系统时,通常将其拆解为功能独立的若干部分。这些部分的接口遵循一定的规范,通过某种方式相连,以共同完成较复杂的任务。nodejs的核心模块,基本上都是stream的的实例,比如process.stdout、http.clientRequest
什么是流?
- 流是一组有序的,有起点和终点的字节数据传输手段
- 它不关心文件的整体内容,只关注是否从文件中读到了数据,以及读到数据之后的处理
- 流是一个抽象接口,被 Node 中的很多对象所实现。比如HTTP 服务器request和response对象都是流。
简单的理解,流就是将大块的东西,分小块依次处理。就像你需要从水龙头上接一杯水,那么当你拧开水龙头,水管就会一点点的源源不断的流出来给你。
那么流这种方式在程序当中又有什么优势呢?先看如下代码:
let fs = require('fs');
fs.readFile('./1.txt', 'utf8', function(err, data){
// 1.txt 已经读取完成
console.log(data);
fs.writeFile('/2.txt', data); // 将内容写入2.txt中
});
以上两个方法是实现的功能是将1.txt文件读取到内存当中,再将它写入到2.txt文件中。但是如果文件过大就会出现问题了,内存容易爆掉。那么这里比较合适的方式应该是读写交替进行,也就是使用流的方式读写文件,这样不管文件有多大,都不会一下子耗尽内存,可以安全的执行完。
如下:
let fs = require('fs');
let readStream = fs.createReadStream('./1.txt');
let writeStream = fs.createWriteStream('./2.txt');
readStream.on('data', function(chunk) { // 当有数据流出时,写入数据,chunk的类型为Buffer
writeStream.write(chunk);
});
readStream.on('end', function() { // 当没有数据时,关闭数据流
writeStream.end();
});
流的四种类型
在nodejs中,有四种stream类型:
- Readable - 可读的流,用来读取数据 (例如 fs.createReadStream()).
- Writable - 可写的流,用来写数据 (例如 fs.createWriteStream()).
- Duplex - 可读写的流(双工流),可读+可写 (例如 net.Socket).
- Transform - 转换流,在读写过程中可以修改和变换数据的 Duplex 流 (比如 zlib.createDeflate()(数据压缩/解压)).
1、可读流(Readable streams)
nodejs中常见的可读流有:fs.createReadStream()、http.IncomingRequest、process.stdin
可读流createReadStream用法如下:
// 创建可读流
let rs = fs.createReadStream(path,[options]);
// 设置编码格式
rs.setEncoding('utf8');
// 监听open事件,打开文件时触发
rs.on('open', function () {
console.log(err);
});
//流切换到流动模式,数据会被尽可能快的读出
rs.on("data",function(data){
console.log(data); //读取到的数据
});
// 该事件会在读完数据后被触发
rs.on("end",function(data){
console.log("数据已经读取完毕");
});
//如果读取文件出错了,会触发error事件
rs.on("error",function(err){
console.log("something is wrong during processing");
})
//文件关闭触发
rs.on('close', function () {
console.log('文件关闭');
});
1、path读取文件的路径
2、options
- flags打开文件要做的操作,默认为'r'
- encoding默认为null
- start开始读取的索引位置
- end结束读取的索引位置(包括结束位置)
- highWaterMark读取缓存区默认的大小64kb
如果指定utf8编码highWaterMark要大于3个字节
2、可写流(Writable streams)
可写流createReadStream
实现了stream.Readable接口的对象,将对象数据读取为流数据,当监听data事件后,开始发射数据
let fs = require("fs");
// 创建一个可以写入的流,写入到文件 1.txt 中
let ws= fs.createWriteStream('1.txt');
let data = '写入流数据';
// 使用 utf8 编码写入数据
ws.write(data,'UTF8');
// 表明接下来没有数据要被写入 Writable 通过传入可选的 chunk 和 encoding 参数,可以在关闭流之前再写入一段数据 如果传入了可选的 callback 函数,它将作为 'finish' 事件的回调函数
ws.end("最后写入的数据","utf8",function(){
console.log(" 我是'finish' 事件的回调函数")
});
// 在调用了 stream.end() 方法,且缓冲区数据都已经传给底层系统之后, 'finish' 事件将被触发。
ws.on('finish', function() {
console.log("写入完成。");
});
// 写入时发生错误触发
ws.on('error', function(err){
console.log(err.stack);
});
// 创建可写流
let ws = fs.createWriteStream(path,[options]);
1、path读取文件的路径
2、options
- flags打开文件要做的操作,默认为'w'
- encoding默认为utf8
- highWaterMark写入缓存区的默认大小16kb
管道流pipe用法
将数据的滞留量限制到一个可接受的水平,以使得不同速度的来源和目标不会淹没可用内存。
linux经典的管道的概念,前者的输出是后者的输入
pipe是一种最简单直接的方法连接两个stream,内部实现了数据传递的整个过程,在开发的时候不需要关注内部数据的流动
用法:
var from = fs.createReadStream('./1.txt');
var to = fs.createWriteStream('./2.txt');
from.pipe(to); // 就是从1.txt中读一点就往2.txt中写一点
3、双工流(Duplex streams)
Duplex实际上就是继承了Readable和Writable。
有了双工流,我们可以在同一个对象上同时实现可读和可写,就好像同时继承这两个接口。 重要的是双工流的可读性和可写性操作完全独立于彼此。这仅仅是将两个特性组合成一个对象
const {Duplex} = require('stream');
const inoutStream = new Duplex({
write(chunk, encoding, callback) {
console.log(chunk.toString());
callback();
},
read(size) {
this.push((++this.index)+'');
if (this.index > 3) {
this.push(null);
}
}
});
inoutStream.index = 0;
process.stdin.pipe(inoutStream).pipe(process.stdout);
最常见的Duplex stream应该就是net.Socket
实例了。
4、转换流(Transform streams)
转换流的输出是从输入中计算出来的,Transform stream是Duplex stream的特例。也就是说,Transform stream也同时可读可写,它可以用来修改或转换数据。然它跟Duplex stream的区别在于,Transform stream的输出与输入是存在相关性的。你可以认为转换流就是一个函数,这个函数的输入是一个可写流,输出是一个可读流。
对于转换流,我们不必实现read或write的方法,我们只需要实现一个transform方法,将两者结合起来。它有write方法的意思,我们也可以用它来push数据。
例如:希望将输入的内容转化成大写在输出出来
const {Transform} = require('stream');
const upperCase = new Transform({
transform(chunk, encoding, callback) {
this.push(chunk.toString().toUpperCase()); // 将输入的内容放入到可读流中
callback();
}
});
// 希望将输入的内容转化成大写在输出出来
process.stdin.pipe(upperCase).pipe(process.stdout);
常见的Transform stream包括zlib、crypto,这里有个简单例子:文件的gzip压缩。
let fs = require('fs');
let zlib = require('zlib');
let gzip = zlib.createGzip();
// 将1.txt文件的内容,打包压缩成compress.txt.gz
let inFile = fs.createReadStream('./file/1.txt');
let outGz = fs.createWriteStream('./file/compress.txt.gz');
inFile .pipe(gzip).pipe(outGz);