什么是流
流是一个在node中与流数据工作的抽象接口,stream模块提供了一个基本的API,使得比较容易创建一个实现了流的接口的对象。node.js中提供了很多的流对象,例如,一个发向http服务器的请求,process.stdout都是流的实例。
流可以是可读的,可写或者两者兼备。所有的流都是EventEmitter的实例。
流的类型
在node中有4种基本的流类型:
1. Writable(可写流):可以把数据写进流里
2. Readable (可读流): 在该流上可以读取数据
3. Duplex (双工流): 在该流上,既可以写数据,又可以读取数据
4. Transform (转换流): 当数据在被写或被读的时候,可以修改数据的格式(例如压缩)
对象模式
node创建的所有流都是对字符串或者是Buffer对象操作。然而,也有与js的其他类型值工作的流实现。这些流被视作为在对象模式里操作。
当流在被创建的时候,使用了objectMode选项,流的实例就被转换为对象模式。试着去转换一个流为对象模式,这并不是安全的。
Buffering(数据缓冲中...)
所有的writable和readable流将会把数据存储在内部的buffer中,这数据可以分别使用writable.writableBuffer或者readable.redableBuffer取得。
一定数量的数据缓冲能力取决于传递进流构造器中的highWaterMark选项。对于正常的流来说,highWaterMark选项明确了字节的数据量。对于在对象模式中操作的流,highWaterMark明确了对象的数量。
当实例调用stream.push(chunk)数据缓冲在readable流中,如果流的消费者没有调用stream.read(),数据将会待在内部队列直到被消费。
一旦内部读取的buffer的数据量达到了highWaterMark标明的阀值,流将会暂时的停止从源头读取数据,直到缓冲的数据能被消费的时候会再次读取
当 writable.write(chunk)方法被重复调用的时候,数据被缓冲在writable流里。当内部写的缓冲数据的总数量低于highWaterMark标明的阀值,调用writable.write()将会返回true,一旦内部缓冲的数据量达到或者超过highWaterMark时,将会返回false。
stream.pipe()是为了限制数据的缓冲在一个可接受的水平,使得源头和目的地不同的缓冲速度将不会那冲垮空闲的内存。
因为Duplex和Transform流都是可读且可写的,每个内部都维护了两个独立的内部缓冲用于读和写,使得每一边在维护一个合适且高效的数据流时都能够独立的进行操作。
可读流
可读流是一种数据被消费的源头的抽象,所有的可读流实例实现了stream.Readable定义的接口。
实例如下,首先创建了一个可读流,传入要读的文件位置还有一些读取设置,从哪里开始读,设置缓冲过程中读取的最大阀值highWaterMark,打开文件时,数据被读取流读取到缓冲中,当定义一个data事件处理器时,此时缓冲中的数据被消费,当流中无数据时或者数据读取完毕时会触发end事件,之后关闭文件。
let fs = require('fs');
let path = require('path');
let rs = fs.createReadStream(path.join(__dirname, '1.txt'), {
flags: 'r', // 读取操作
encoding: 'utf8', // 默认为null,null代表的返回buffer
autoClose: true, // 读取完自动关闭
highWaterMark: 3, // 默认是64k, 64*1024b
start: 3, // 从第3个字节开始读取
end: 8 // 包括索引8
});
// 也可以手动设置编码或者在options声明
// rs.setEncoding('utf8');
rs.on('open', function() {
console.log('文件打开了');
});
// 当流或其底层资源被关闭时触发
rs.on('close', function() {
console.log('文件关闭了');
});
rs.on('error', function(err) {
console.log(err);
});
// 当流将数据块传送给消费者后触发
rs.on('data', function(data) {
console.log('data', data);
// 停止触发data事件,暂停读取
rs.pause();
setTimeout(function() {
// 恢复读取,暂停模式(需显示调用stream.read())切换为流动模式(数据自动从底层系统读取,并通过EventEmitter接口的事件尽可能快的被提供给应用程序)
rs.resume();
}, 1000);
});
// 表明流有新动态,要么有新的数据,要么到达流的尽头,若同时使用data事件,当调用rs.read方法才会触发data事件
// rs.on('readable', function() {
// console.log('readable data', rs.read());
// });
// 流中无数据时或者读取数据完毕触发
rs.on('end', function() {
console.log('end');
});
// 文件打开了
// data 456
// data 789
// end
// 文件关闭了
两种读取模式
可读流有两种读取的模式:暂停模式和流动模式。这些模式与对象流模式不同,一个可读流可以是对象流,也可以不是也不管它是处在流动模式下还是暂停模式下。
1. 在流动模式下,数据被系统自动读取并且使用EventEmitter接口能够尽可能快的提供给应用消费
2. 在暂停模式中,stream.read()必须被显示调用,才能够从流中读取数据块。
所有的可读流开始处于一个暂停模式但是可以切换为流动模式通过以下一种方式:
1. 添加一个data事件处理器
2. 调用stream.resume()方法
3. 调用stream.pipe(),使得数据是可写的
所有的可读流可以切换为暂停模式,通过以下一种方式:
1. 如果没有管道的目的地,通过调用stream.pause()
2. 如果有管道的目的地,通过移除所有管道的目的地。大多数管道的目的地能够通过调用stream.unpipe()移除
最重要的是要明白readable直到有消费或者忽略数据被提供,才会产生数据。如果消费机制被带走了或者被停止了,那么readable会停止产生数据。
出于背后兼容性的原因,移除data事件处理器并不会自动的暂停流。统一,如果有管道的目的地,调用stream.pause()也不会保证流会暂停。
如果readable切换为流动模式,并且没有消费者去消费处理数据,数据将会丢失。
添加readable的事件处理器可以自动的使得流停止流动,数据可以通过readable.read()去消费。如果readable的事件处理器被移除,流可以再次流动如果有个data事件处理器的话
可写流
可写流是数据被写入的目的地的抽象,所有的可写流实例实现了stream.Writable定义的接口
以下是我写的一个例子,首先创建了一个可写流对象,传入一个参数为需要写入的文件位置,一个参数是配置参数,highWaterMark是在写入的时候指定写入数据的阈值,若内部缓冲小于阈值时,会返回true,若返回false,应该停止写入数据,此时缓冲区已经达到阈值,满了,若所有缓冲的数据块被消费完了,清空了会触发一个drain事件。
let fs = require('fs');
let path = require('path');
let ws = fs.createWriteStream(path.join(__dirname, '2.txt'), {
flags: 'w',
encoding: 'utf8',
mode: 0o666,
autoClose: true,
start: 0,
highWaterMark: 3 // 最高水平线,指定了字节总数
});
// 内部的缓冲小于创建的配置的highWaterMark,返回true
let flag = ws.write('0', 'utf8', ()=>{});
console.log(flag); // true
flag = ws.write('1', 'utf8', ()=>{});
console.log(flag); // true
// 若返回false,则应该停止向流写入数据
flag = ws.write('2', 'utf8', ()=>{});
console.log(flag); // 返回false,写完3时,缓冲区已达到highWaterMark:3时,表明缓存区满了
// 所有缓冲的数据块都被排空了,当达到highWaterMark时,表明缓存区满了,满了后被清空了才会触发drain
ws.on('drain', function() {
console.log('drain');
flag = ws.write('3', 'utf8', ()=>{});
console.log(flag); // true
});
// 可写流和可读流都会在内部的缓冲器中存储数据