Node.js中流Stream的介绍

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;
复制代码

转载于:https://juejin.im/post/5b437cb95188257ce871218c

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值