前言
node.js天生异步和事件驱动,比较适合处理I/O相关的任务,所以在处理I/O相关的操作时,可以用stream流。
流的概念
流是一种传输手段,是有顺序的,有起点和终点,它只是一个实现了一些方法的 EventEmitter 。在unix中有一个概念:‘管道’,流在实现的过程中也可被看成是一个管道一样,进行拼接数据,将一个进程的stdout输出看成下一个进程的输入stdin。.pipe()方法是流的精髓,用于数据两端的数据桥接。可以通过 require(‘stream’) 加载 Stream 基类。就会包括以下四种流的类型。
四种基本流的类型:
1、stream.Readable 可读流(需要实现_read方法,关注点在于对数据流读取的细节)
2、stream.Writable 可写流(需要实现_write方法,关注点在于对数据流写入的细节)
3、stream.Duplex 可读/写流(需要实现以上两接口,关注点为以上两接口的细节)
4、stream.Transform 继承自Duplex(需要实现_transform方法,关注点在于对数据块的处理)
Readable 可读流
有两种模式:flowing mode流动模式和 paused mode暂停模式,流动模式下,数据会自动从来源流出,跟不老泉似的,直到来源的数据耗尽。暂停模式下,你得通过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属性保存了发生错误时的堆栈信息。
var fs=require('fs');
var path=require('path');
var txt = path.resolve('./text.txt');//text.txt里面的内容12345678901234567890
//指定开始位置,结束位置读取文件
// 创建一个可读流
var readable=fs.createReadStream(txt,{
encoding:'utf8',//不传默认为buffer,显示为字符串
highWaterMark:6,//缓冲区大小
start:6,//开始索引值
end:12//结束索引值
});
readable.on('open',function(fd){
console.log('打开文件成功,句柄:'+fd); // 打开文件成功,句柄:3
});
readable.on('data',function(data){
console.log(data)
readable.pause();//暂停读取和发射data事件
setTimeout(function(){
readable.resume();//恢复读取并触发data事件
},2000);
});
readable.on('end',function(){
console.log('读取结束'); //读取结束
});
readable.on('close',()=>{
console.log('读取关闭'); //读取关闭
});
readable.on('error',function(err){
console.log('读取异常,'+err); //当出现异常时触发
})
Writable streams可写流
Writable:可写流的例子包括了:
- HTTP requests, on the client 客户端请求
- HTTP responses, on the server 服务器响应
- fs write streams 文件
- zlib streams 压缩
- crypto streams 加密
- TCP sockets TCP服务器
- child process stdin 子进程标准输入
- process.stdout, process.stderr 标准输出,错误输出
注意:每次写入文件的时候并不是直接写入文件的,而是先写入缓冲区,待缓冲区写满后再写入到文件里面。缓存区的大小就是highWaterMark,默认值是16K。
let fs = require('fs');
let ws = fs.createWriteStream('./txt.txt',{
flags:'w',
mode:0o666,
start:3,
highWaterMark:3//默认是16K
});
let flag = ws.write('1');
console.log(flag);//true
flag =ws.write('2');
console.log(flag);//true
flag =ws.write('3');
console.log(flag);//false
flag =ws.write('4');
console.log(flag);//false
- 如果缓存区已满 ,返回false,如果缓存区未满,返回true
- 如果能接着写,返回true,如果不能接着写,返回false
- 按理说如果返回了false,就不能再往里面写了,但是如果你真写了,如果也不会丢失,会缓存在内存里。等缓存区清空之后再从内存里读出来
自定义可写流
为了实现可写流,我们需要使用流模块中的Writable构造函数。
创建一个writable流:
- chunk代表写进去的数据
- encoding字符编码
- callback是一个回调函数
var stream = require('stream');
var util = require('util');
util.inherits(Writer, stream.Writable);
let stock = [];
function Writer(opt) {
stream.Writable.call(this, opt);
}
Writer.prototype._write = function(chunk, encoding, callback) {
setTimeout(()=>{
stock.push(chunk.toString('utf8'));
console.log("增加: " + chunk);
callback();
},500)
};
var w = new Writer();
for (var i=1; i<=5; i++){
w.write("项目:" + i, 'utf8');
}
w.end("结束写入",function(){
console.log(stock);
});
Duplex streams可读写的流(双工流)
Duplex 流是同时实现了 Readable 和 Writable 接口的流
双工流的可读性和可写性操作完全独立于彼此,这仅仅是将两个特性组合成一个对象
Duplex 流的实例包括了:
- TCP sockets
- zlib streams
- crypto streams
const Duplex = require('stream').Duplex;
const inoutStream = new Duplex({
write(chunk, encoding, callback) {
console.log(chunk)
// console.log(chunk.toString());
callback();
},
read(size) {
this.push((++this.index)+'个');
if (this.index > 3) {
this.push(null);
}
}
});
// console.log(process.stdout)
inoutStream.index = 0;
process.stdin.pipe(inoutStream).pipe(process.stdout);
Transform streams转换流
转换流(Transform streams) 是一种 Duplex 流。它的输出与输入是通过某种方式关联的。和所有 Duplex 流一样,变换流同时实现了 Readable 和 Writable 接口
转换流的输出是从输入中计算出来的
对于转换流,我们不必实现read或write的方法,我们只需要实现一个transform方法,将两者结合起来。它有write方法的意思,我们也可以用它来push数据
变换流的实例包括:
- zlib streams
- crypto streams
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);
对象流:
默认情况下,流处理的数据是Buffer/String类型的值。有一个objectMode
标志,我们可以设置它让流可以接受任何JavaScript对象。
const {Transform} = require('stream');
let fs = require('fs');
let rs = fs.createReadStream('./users.json');
rs.setEncoding('utf8');
let toJson = Transform({
readableObjectMode: true,
transform(chunk, encoding, callback) {
this.push(JSON.parse(chunk));
callback();
}
});
let jsonOut = Transform({
writableObjectMode: true,
transform(chunk, encoding, callback) {
console.log(chunk);
callback();
}
});
rs.pipe(toJson).pipe(jsonOut);
pipe管道+链式流
前者的输出是后者的输入,pipe是一种最简单直接的方法连接两个stream,内部实现了数据传递的整个过程,在开发的时候不需要关注内部数据的流动。
- 这个方法从可读流拉取所有数据, 并将数据写入到提供的目标中
- 自动管理流量,将数据的滞留量限制到一个可接受的水平,以使得不同速度的来源和目标不会淹没可用内存
- 默认情况下,当源数据流触发 end的时候调用end(),所以写入数据的目标不可再写。传 { end:false }作为options,可以保持目标流打开状态
下面这段代码中,服务器每收到一次请求,就会先把data.txt读入到内存中,然后再从内存取出返回给客户端。尴尬的是,如果data.txt非常的大,而每次请求都需要先把它全部存到内存,再全部取出,不仅会消耗服务器的内存,也可能造成用户等待时间过长。
var http = require('http');
var fs = require('fs')
var server = http.createServer(function (req, res) {
fs.readFile(__dirname + '/data.txt', function (err, dat
res.end(data);
});
});
server.listen(8000);
HTTP请求中的request对象和response对象都是流对象,于是我们可以换一种方法:
var http = require('http');
var fs = require('fs');
var server = http.createServer(function (req, res) {
let stream = fs.createReadStream(__dirname + '/data.txt');//创造可读流
stream.pipe(res);//将可读流写入response
});
server.listen(8000);
会在脚本的运行同级目录下压缩成一个text.gz压缩包
var fs=require('fs');
var zlib=require('zlib');
var path=require('path');
var file1=path.resolve('./text.txt');
var file3=path.resolve('./text.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');
// 解压 input.txt.gz 文件为 input.txt
fs.createReadStream('./text.gz')
.pipe(zlib.createGunzip())
.pipe(fs.createWriteStream('text.txt'));
console.log("文件解压完成。");
pipe的几种应用场景
简单stream进行的pipe使用
var path = require('path');
var fs = require("fs");
var file1=path.resolve('./test.txt');
var a = process.stdin.pipe(process.stdout)
var b = process.stdin.pipe(fs.createWriteStream(file1))
var c = fs.createReadStream(file1).pipe(process.stdin)
console.log(a,b,c)
// 输出
// Socket {connecting: false, _hadError: false, _handle: Pipe, _parent: null, _host: null, …}
// WriteStream {_writableState: WritableState, writable: true, domain: null, _events: Object, _eventsCount: 5, …}
// Socket {connecting: false, _hadError: false, _handle: Pipe, _parent: null, _host: null, …}
剩下的几种方法参考此链接:https://blog.csdn.net/vieri_32/article/details/48376547