nodejs的stream

127 阅读6分钟

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方法