1.流Stream的概念
- 流是一组有序的,有起点和终点的字节数据传输手段
- 它不关心文件的整体内容,只关注是否从文件中读到了数据,以及读到数据之后的处理
- 流是一个抽象接口,被 Node 中的很多对象所实现。比如HTTP 服务器request和response对象都是流。
2.Node.js中流Stream的分类
- Readable - 可读的流 (例如 fs.createReadStream()),读取数据.
- Writable - 可写的流 (例如 fs.createWriteStream()),写入数据.
- Duplex - 可读写的流 (例如 net.Socket),读取+写入数据.
- Transform - 在读写过程中可以修改和变换数据的 Duplex 流 (例如 zlib.createDeflate()),在读写过程中,可对数据进行修改.
const Stream = require('stream') //stream模块
//四种流
let Readable = Stream.Readable //可读的流
let Writable = Stream.Writable //可写的流
let Duplex = Stream.Duplex //可读写的流
let Transform = Stream.Transform //在读写过程中可以修改和变换数据的 Duplex 流
复制代码
Node.js中关于流的操作被封装到了Stream模块中,这个模块也被多个核心模块所引用。例如在fs.createReadStream()和fs.createWriteStream()的源码实现里,都调用了Stream模块提供的抽象接口来实现对流数据的操作。
3.为什么要使用流
如果读取一个文件,使用fs.readFileSync同步读取,程序会被阻塞,然后所有数据被写到内存中。使用fs.readFile读取,程序不会阻塞,但是所有数据依旧会一次性全被写到内存,然后再让消费者去读取。如果文件很大,内存使用便会成为问题。 这种情况下流就比较有优势。流相比一次性写到内存中,它会先写到到一个缓冲区,然后再由消费者去读取,不用将整个文件写进内存,节省了内存空间。
- 例如:
const http = require('http');
const fs = require('fs');
const path=require('path');
http.createServer((req, res) => {
fs.readFile(path.join(__dirname,'1.txt'), (err, data) => {
res.end(data);
});
}).listen(3000);
复制代码
问题在于1.txt文件需要全部读完放到内存中,才能返回给用户。当1.txt 有10M的大小时并且并发为100时,服务器内存会消耗10M*100的大小,当并发量更高时内存会吃不消。
如果用流的话,流可以将文件一点一点读到内存中,再一点一点返回给用户,读一部分,写一部分。可通过 HTTP 协议的 Transfer-Encoding: chunked 分段传输,使服务器内存的开销和客户端用户体验得到优化,一举两得。
4.流Stream的用法
4.1 Readable-可读流
常见的可读流:
- HTTP responses, on the client
- HTTP requests, on the server
- fs read streams
- TCP sockets //双工流,即可读可写的流
- process.stdin //标准输入
所有的 Readable Stream 都实现了 stream.Readable 类定义的接口。
使用方式
let fs = require('fs');
// 一般情况下我们不会使用后面的参数
let rs = fs.createReadStream('./1.txt'{
highWaterMark: 3, // buffer来缓存这些数据
flags:'r',//这里是读默认为r,下面可写流为w ,
// 设置文件的执行模式:
// r:读文件,
// r+读取并写入,
// rs同步读取文件并忽略缓存,
// w写入文件,不存在则创建,存在则清空,
// wx排它写入文件,
// w+读取并写入文件,不存在则创建,存在则清空,
// wx+和w+类似,排他方式打开,
// a追加写入,
// ax与a类似,排他方式写入,
// a+读取并追加写入,不存在则创建,
// ax+作用与a+类似,但是以排他方式打开文件
autoClose:true, // 默认读取完毕后自动关闭
start:0,//开始读取文件中的内容位置,默认是从0开始
end:9,// 流是闭合区间,用于指定定具体要读取文件多长的数据
encoding:'utf8'//编码格式设置
});
// 默认创建一个流 是非流动模式,默认不会读取数据
// 我们需要接收数据 我们要监听data事件,数据会总动的流出来
rs.on('error',function (err) {
console.log(err)
});
rs.on('open',function () {
console.log('打开文件');
});
rs.on('data',function (data) {
console.log(data);
rs.pause(); // 触发暂停,此时流动模式为暂停模式
});
setTimeout(()=>{
rs.resume();//重新设置为流动模式,开始读取数据
},5000)
rs.on('end',function () {
console.log('读取完成');
});
rs.on('close',function () {
console.log('关闭')
});
复制代码
代码实现
ReadStream是基于EventEmitter类的对象,因此要继承EventEmitter。
ReadStream是通过每次读取highWartMark大小的数据,是一段一段的读取的,当数据的bytesRead小于highWartMark说明数据已经读完,并触发end事件。
暂停与启动方法的原理则是内部有一个flowing变量,它表示了当前流是否是流动模式,若为流动模式才继续读取。
let EventEmitter = require('events');
let fs = require('fs');
class ReadStream extends EventEmitter {
constructor(path, options = {}) {
super();
this.path = path;
this.autoClose = options.autoClose || true;
this.flags = options.flags || 'r';
this.encoding = options.encoding || null;
this.start = options.start || 0;
this.end = options.end || null;
this.highWaterMark = options.highWaterMark || 64 * 1024;//读取文件默认每次读64k
this.pos = this.start;
this.flowing = null; // 流动状态
this.buffer = Buffer.alloc(this.highWaterMark);//默认创建长度为highWaterMark的buffer换成
//注意!这里是异步执行,可能文件还没打开就触发了this.read(),
//所以下面this.read()方法里面订阅this.once('open',()=>this.read());
this.open();
// newListener订阅事件时触发的事件
this.on('newListener', (type) => {
if(type === 'data'){ // 当订阅了事件类型为data时候开始读取文件
this.flowing = true;
this.read();// 读取文件
}
});
}
read(){
// 这时候文件还没有打开呢,等待着文件打开后再去读取
if(typeof this.fd !== 'number'){
// 等待着文件打开,再次调用read方法
return this.once('open',()=>this.read());
}
// 判断文件还能读多少,如果文件读到最后可能小于highWaterMark(64k)
let howMuchToRead = this.end ? Math.min(this.highWaterMark,this.end - this.pos+1) :this.highWaterMark
fs.read(this.fd, this.buffer,0,howMuchToRead,this.pos,(err,bytesRead)=>{
if (bytesRead>0){ // 读到内容了
this.pos += bytesRead;
// 保留有用的
let r = this.buffer.slice(0, bytesRead);
r = this.encoding ? r.toString(this.encoding) : r;
// 第一次读取
this.emit('data', r);
if (this.flowing) {
this.read();//继续读取
}
}else{
//读取完毕
this.end = true;
this.emit('end');
this.destroy();
}
});
}
pipe(dest){
this.on('data',(data)=>{
let flag = dest.write(data);
if(!flag){
this.pause();
}
});
dest.on('drain',()=>{
this.resume();
});
this.on('end',()=>{
this.destroy();
});
}
destroy() { // 判断文件是否打开 (将文件关闭掉)
if (typeof this.fd === 'number') {
fs.close(this.fd, () => {
this.emit('close');
});
return;
}
this.emit('close');
}
open() { // 打开文件
fs.open(this.path, this.flags, (err, fd) => {
if (err) {
this.emit('error', err);
if (this.autoClose) {
this.destroy(); // 销毁,关闭文件(触发close事件)
} return;
}
this.fd = fd;
this.emit('open'); // 触发文件开启事件
});
}
//暂停
pause(){
this.flowing = false;
}
//继续读
resume(){
this.flowing = true;
this.read(); // 继续读取
}
}
module.exports = ReadStream;
复制代码
4.1 Writable-可读流
使用方式
let writeStream = fs.createWriteStream('./2.txt', {
highWaterMark: 2,
start: 0,
encoding: 'utf8',
flags: 'w'
});
//当缓存区中的数据已经全部清空(完部写入完)会触发drain事件。
writeStream.on('drain',()=>{
console.log('drain');
});
writeStream.write('123456789');
复制代码
代码实现
WriteStream也是基于EventEmitter类的对象,因此要继承EventEmitter。
WriteStream处理过程是,开始是直接写入数据,之后的数据保存到缓存区中,当缓存区的数据量超过highWaterMark时,将needDrain标记为“流干”,每次流干就会触发订阅的drain事件。
let fs = require('fs');
let EventEmitter = require('events');
class WriteStream extends EventEmitter{
constructor(path,options ={}){
super();
this.path = path;
this.flags = options.flags || 'w';
this.mode = options.mode || 0o666;
this.highWaterMark = options.highWaterMark || 16*1024;//写入默认是每次写入16k
this.start = options.start || 0;
this.autoClose = options.autoClose|| true;
this.encoding = options.encoding || 'utf8';
// 是否需要触发drain事件
this.needDrain = false;
// 是否正在写入
this.writing = false;
// 缓存 正在写入就放到缓存中
this.buffer = [];
// 算一个当前缓存的个数
this.len = 0;
// 写入的时候也有位置关系
this.pos = this.start;
this.open();
}
/**
*写入方法(核心方法)
*
* @param {Buffer} chunk 待写入的数据段
* @param {string} [encoding=this.encoding] 编码
* @param {function} callback 回调
* @returns 缓存区未填满返回true否则返回false
* @memberof WriteStream
*/
write(chunk, encoding = this.encoding,callback){
//如果chunk不是buffer则将chunk通过Buffer.from方法转化成Buffer对象
chunk = Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk);
this.len += chunk.length;// 每次调用write就统计一下长度
this.needDrain = this.highWaterMark <= this.len;
if(this.writing){
this.buffer.push({chunk,encoding,callback});
}else{
this.writing = true; // 下一步从缓存写入
this._write(chunk,encoding,()=>this.clearBuffer()); // 当文件首次写入后 清空缓存区的内容
}
return !this.needDrain; // write 的返回值为boolean类型
}
/**
*写入的私有方法(核心方法)
*
* @param {Buffer} chunk 待写入的数据段
* @param {string} encoding 编码
* @param {function} callback 回调清理缓存区方法clearBuffer
* @returns
* @memberof WriteStream
*/
_write(chunk,encoding,callback){
if (typeof this.fd !== 'number') {
return this.once('open', () => this._write(chunk, encoding, callback));
}
// fd是文件描述符 chunk是数据 0 写入的位置和 长度 , this.pos偏移量
fs.write(this.fd, chunk,0,chunk.length,this.pos,(err,bytesWritten)=>{
this.pos += bytesWritten;
this.len -= bytesWritten; // 写入的长度会减少
callback();
});
}
/**
* 清理缓存区
*/
clearBuffer(){
let data = this.buffer.shift();//取出缓存中第一个chunk
if(data){//若有数据,继续递归执行
this._write(data.chunk, data.encoding, () => this.clearBuffer());
}else{//若缓存区里已经没有数据了
this.writing = false;//修改状态,已经写完了
if (this.needDrain) {
this.needDrain = false;//触发drain后改变的状态
this.emit('drain');// 触发一次drain
}
}
}
destroy(){
if(typeof this.fd === 'number'){
fs.close(this.fd,()=>{
this.emit('close');
});
return
}
this.emit('close');
}
open(){
fs.open(this.path,this.flags,this.mode,(err,fd)=>{
if(err){
this.emit('error');
this.destroy();
return
}
this.fd = fd;
this.emit('open');
});
}
}
module.exports = WriteStream;
复制代码