nodejs中存在4种流操作。
- 可读流 Readbale
- 可写流 Writable
- Duplex 双工流(具备可读流和可写流的能力,但读和写是相互独立的。可读流的数据不能作为可写流的数据源)
- Transform 转换流(具备可读流和可写流的能力,但可读流可以作为可写流的数据源。一般用于文件格式转换或数据压缩,比如gzip压缩)。 其中Duplex,Transform都是对Readbale和Writable的抽象。
// Duplex 双工流的使用
const { Duplex } = require('stream');
class DuplexDemo extend Duplex {
constructor(dataSource) {
super();
this.dataSource = dataSource;
}
// 可读流必须要求的_read方法,可读流内部会调用_read往缓冲区写数据
_read() {
// 当数据写完后需要再写入null,作为数据结束的表示
const data = this.dataSource.shift() || null;
// 调用this.push往缓冲区写数据
this.push(data);
}
// 可写流必须要求的_write方法,可写流内部调用_write往缓冲区写入数据
_write(chunk, encode, callBack) {
process.stdout.write(chunk);
Promise.resolve.then(callBack);
}
}
const dataSource = ['这','是', '数', '据', '源'];
let duplexDemo = new DuplexDemo(dataSource);
/**
* 监听data事件,启用的可读流的流动模式
*/
duplexDemo.on('data', (chunk) => {
console.log(chunk.toString());
})
/**
* 若监听readable事件,则启动了可读流的暂停模式。
* 需要再readable事件的回调函数中调用可读流的read方法才能,再次触发可读流往缓冲区写入数据
*/
duplexDemo.on('readable', () => {
let data = null;
// 监听readable事件后只有调用read方法后,可读流才能继续往缓冲区中写入数据
while ((data = duplexDemo.read()) !== null) {
console.log(data.toString());
}
})
/*
* 调用可写流的write方法每次会返回一个布尔值,表示写入的数据是否操作缓冲区的阈值。
* 未达到返回true否则返回false。
* 返回false,则可写流无法再往缓冲区写数据。
* 可写流需要监听drain事件,drain事件触发表示缓冲区中数据的尺寸已经小于阈值。
* 此时可以再次调用可写流的write方法往缓冲区中写入数据
**/
let atHighWaterMark = duplexDemo.write('这是写入的数据', () => {
console.log('写入结束')
})
// Transform 转换流的使用,对同一个数据进行操作
const { Transform } = require('stream');
class TransformDemo extends Transform {
constructor() {
super();
}
_transform(chunk, encode, callback) {
this.push(chunk.toString().toUpperCase());
callback();
}
}
const transformDemo = new TransformDemo();
// 下面的可读流和可写流都是对同一个数据进行操作
// 调用可写流的write方法
transformDemo.write('a'); // 写如'a'
// 监听可读流的data事件
transformDemo.on('data',(chunk) => {
console.log(chunk.toString()) // 打印出"A"
})
可读流中流动模式和暂停模式可以互相切换
可读流中分为2种模式流动模式和暂停模式。
1、流动模式:可读流自动读取数据,通过EventEmitter接口的事件尽快将数据提供给应用。
> 2、暂停模式:必须显式调用可读流的read()方法来从流中读取数据片段。
暂停模式切换到流动模式:
1、监听“data”事件
2、调用 stream.resume()方法
3、调用 stream.pipe()方法将数据发送到可写流
流动模式切换到暂停模式:
1、如果不存在管道目标,调用stream.pause()方法
2、如果存在管道目标,调用 stream.unpipe()并取消'data'事件监听
一般手动的调用write和监听data方法需要注意可写流的缓冲区是否已经达到背压,需要监听dran事件,进行可读流的流动和暂停模式切换。所以一般我们会采用pipe方法(pipe方法内部会帮我们实现好这些)。自己实现的实示例码如下:
const fs = require('fs');
const rs = fs.createReadstream('read.text', {
heighWaterMark: 16, // 指定可读流缓冲区的heighWaterMark为16个字节,默认64kb
})
const ws = fs.creatWritestream('write.text', {
heighWaterMark: 4, // 指定可写流缓冲区的heighWaterMark为4个字节,默认16kb
})
let isFull = true;
// 监听可写流的data事件,触发可读流的流动模式
rs.on('data', (chunk) => {
// ws.write方法返回false,代表可写流中缓冲区数据已经达到背压
flag = ws.write(chunk, () => {
console.log('写入完成');
})
if (!flag) {
// 可写流缓冲区数据已经达到背压,无法消化更多的数据了
// 调用可读流pause方法,切换到暂停模式,可写流暂时不要往可读流缓冲区中写入数据
rs.pause();
}
})
// drain事件触发,代表缓冲区的数据已经消费了,已经腾出足够的空间
// 调用可读流的resume方法,将可读流的暂停模式切换到流动模式
ws.on('drain', () => {
rs.resume();
})
nodejs中的流操作提供了很多事件
- Readbale 可读流的data,readable, end, pause, error,end事件
- Writable 可写流drain,pipe,close,finish,事件。
因为nodejs的流继承自EventEmiter类,所以流具备监听和触发事件的能力。
可读流的实现原理
可读流的特点:可读流具有事件监听和触发的能力,能分片获取数据
- 1:可读流继承自EventEmiter类所以具有事件监听和触发的能力
- 2:可读流内部调用的是fs模块相关的api实现的,fs.open打开文件获取fd(文件描述符),fs.read读取文件,fs.close关闭文件。
class fsReadStream extends EventEmiter{
constructor(path, options) {
this.path = path;
this.mode = option.mode || 438;
this.heighWaterMark = option.heighWaterMark || 64 * 1024;
this.start = option.start || 0;
this.end = option.start;
this.stop = false;
this.open();
// 获取监听可读流的实现类型
this.on('newListener', (type) => {
console.log(type)
})
}
open () {
fs.open(this.path, this.flags, this.mode, (err, fd) => {
if (err) {
this.emit('error', err)
}
this.fd = fd;
this.emit('open', fd);
})
}
read() {
// 申请heighWaterMark大小的buffer空间
let buffer = Buffer.mlloc(heighWaterMark);
let readoffset = 0;
/*
* fd(文件描述符),
* buffer缓冲区,读取的文件会先写入buffer缓冲区
* offset从buffer的第0个位置开始写入
* heighWaterMark背压
* readoffset从文件readoffset位置开始读取
* read方法读取完成后的回调函数
*/
fs.read(fd, buffer, offset, heighWaterMark, readoffset, (err, readBytes) => {
// 暂停模式,不在继续读取
if (this.stop) return;
if (typeof this.fd !== 'number') {
this.once('open', this.read)
}
// 读取到数据长度不为0
if (readBytes) {
readoffset += readBytes;
this.emit('data', buffer);
this.read();
} else { // 读取的数据长度为0,表面当前文件已经读取完毕
// 先触发end事件再触发close事件
// 这也就是为什么监听的close事件一直在end事件之后触发
this.emit('end');
this.emit('close');
}
})
// 开启暂停模式
pause() {
this.stop = true;
}
// 流动模式继续读取数据触发data事件
resume() {
this.stop = false;
this.read();
}
}
// pipe方法的实现
pipe(ws) {
this.on('data', (chunk) => {
const flag = ws.write(chunk);
if (!flag) {
this.pause();
}
})
ws.on('drain', () => {
this.resume();
})
}
close () {
fs.close(this.fd, () => {
this.emit('close')
})
}
}
const rs = fsReadStream('demo.text');
const ws = fs.createWritestream('demo1.text')
rs.on('open', (fd) => {
console.log(fd)
})
rs.on('err', (err) => {
console.log(err)
})
rs.on('data', (chunk) => {
console.log(chunk);
})
rs.on('end', () => {
console.log('end')
})
rs.pipe(ws);
具有的open,end,data,error事件的基础就是可读流继承自EventEmiter
- 调用fs.open实现文件的打开和触发open事件
- 调用fs.read实现分片读取数据和触发data事件
- 文件读取完后调用emit('end')触发end事件
- 调用emit('close')触发close事件
nodej擅长处理IO密集型操作
- 1:网络io操作
- 2:文件io操作 在nodejs内部中处理这两种类型的io,由对应的模块如fs,http,net模块进行处理。这些模块内部都是由对应的流操作接口实现。
tcp服务使用的net模块就是一个Duplex 双工流,同时具备可读流data事件的能力和可写流write方法