node 中的可读 可写流

在 node 中流是最核心的一部分, 也是相对较难的一部分, node 中大量的 api 都与 流相关,通过一些学习, 对流的概念稍稍的了解了一点, 本文将梳理一下自己的理解,不对之处还望指正。
stream

流是一组有序的,有起点和终点的字节数据传输手段,
它不关心文件的整体内容,只关注是否从文件中读到了数据,以及读到数据之后的处理。
node 中的很多对象都实现了流。 比如 HTTP 服务器中的 request 和 response 对象都是流, node 的 api
process.stdin process.stdout 等

node 中 有四种基本的流类型
  1. Readable 可读流,用来读取数据
  2. Writeable 可写流, 用来写入数据
  3. Duplex 可读可写的流(双工流)
  4. Transform 在读写过程中可以修改和变换数据的 Duplex 流

除了以上四种基本的流类型之外, 在 node 环境中我们还可以自定义流

本文将阐述 可读流 与 可写流 的一些基础概念:
  1. 可读流 (createReadStream)

    创建一个可读流

       fs.createReadStream(path, [options]);
    
       path: 读取文件的路径
       options:
           flags 打开文件要做的操作, 默认为'r'
           encoding 默认为 null
           start 开始读取的索引位置
           end 结束读取的索引位置(包括结束位置)
           highWaterMark 读取缓存区默认的大小64kb
    • 可读流分为两种模式 flowing 和 paused

      1. 在 flowing 模式下, 可读流自动从系统底层读取数据,并通过 EventEmitter 接口的事件尽快将数据提供给到应用
      2. 在 paused 模式下, 必须显示的调用 stream.read() 方法来从流中读取数据片段
      3. 所有初始工作模式为 paused 的 可读流,可以通过下面三种途径切换到 flowing 模式:

        1. 监听 ‘data’ 事件
        2. 调用 stream.resume();
        3. 调用 stream.pipe() 方法将数据发送到 Writeable 中
      4. 可读流可以通过下面两种途径切换到 paused 模式

        1. 如果不存在管道目标 ,可以通过调用 stream.pause() 方法实现。
        2. 如果存在管道目标,可以通过取消 ‘data’ 事件监听,并调用 stream.unpipe() 方法移除所有管道目标来实现。
    • 可读流的事件: ‘open’, ‘data’, ‘close’, ‘error’, ‘end’, ‘readable’
      代码体现

          let fs = require('fs');
          let path = require('path');
      
          let fs = require('fs');
          let path = require('path');
          let rs = fs.createReadStream(path.join(__dirname, '1.txt'), {
              flags: 'r',
              encoding: 'utf-8',   // 设置读取到数据的编码格式, 默认 null 为 buffer
              highWaterMark: 3,    // 最高水位线 默认是 64 * 1024b
              start: 0,
              // end: 2
          });
      
          rs.on('open', function (fd) {
              // fd 文件描述符
              console.log('文件打开了', fd);
          } );
      
          // rs 默认情况下为 paused 模式 , 通过调用 data 事件变为 flowing 模式, 会疯狂的出发 data 事件读取数据, 直到读取完毕
          // 流动模式会疯狂的触发 data 事件 , 知道读取完毕
          rs.on('data', function (data) {
              console.log('data', data);
              // 调用 rs.pause 表示暂停读取,暂停 data 事件的触发
              rs.pause();
          });
          setInterval(() => {
              rs.resume();
          }, 1000);
          rs.on('end', function () {
              console.log('===end===');
          } );
          rs.on('close', function () {
              console.log('===文件关闭===');
          });
          rs.on('error', function (err) {
              console.log(err);
          });
      
          // or 
          fs.createReadStream(path.join(__dirname, '1.txt')).pipe(process.stdout);
    • 可读流中还有一个 readable 的方法

      • readable 方法特点
        1. 当我们创建了一个流时, 会先读取 highWaterMark 的数据到当前流的缓存区中, 等待着你去消费
        2. 如果缓存区中的数据被清空, 会再次触发 readable 事件
        3. readable 事件每次触发时, 都会对比一下你消费后剩下的数据是否小于 highWaterMark,小于时 readable 事件中会再次添加 highWaterMark 这么多的数据到 缓存区中, 等待这你去消费
        let fs = require('fs');
        let path = require('path');
        let rs = fs.createReadStream(path.join(__dirname, '1.txt'), {
            flags: 'r',
            encoding: 'utf-8',   // 设置读取到数据的编码格式, 默认 null 为 buffer
            highWaterMark: 3,    // 最高水位线 默认是 64 * 1024b
            start: 0,
        });
    
        rs.on('readable', function () {
            // rs.read 中不给参数的话, 会默认读取 highWaterMark 个数据
            // 如果读取的数据超过缓存区的数据时,会更改缓存区的大小再去读取
            let result =  rs.read(5);
            console.log(result);
        });

    Readable 和 Writable 流都会将数据存储到内部的缓冲区中
    缓存区的大小取决于传递给流构造函数的 highWaterMark 选项。 对于普通的流, highWaterMark 选项指定了总共的字节数。 对于工作在对象模式的流, highWaterMark 指定了对象的总数
    当可读流的实例调用 stream.push(chunk) 方法时,数据被放到缓存区中. 如果流的消费者没有调用 stream.read() 方法, 这些数据会始终存在于内部队列中, 直到被消费。
    当内部缓存区的大小达到 highWaterMark 制定的阈值时, 流会暂停从底层读取资源数据, 直到当前缓存区的数据被消费(也就是说 流会在内部停止调用 readable._read() 来填充可读缓存区)。

  2. 可写流 (createWriteStream)
    创建一个可写流, 可写流写入的数据必须是 字符串 或者 buffer

        let ws = fs.createWriteStream(path, [options]);
        path: 写入的文件的路径, 如果文件不存在, 会自动创建一个文件
        options: {
            flags: 'w',         // 打开文件要做的操作, 默认为 'w'
            mode: 0o666,        // 设置文件模式,默认 0o666
            autoClose: true,    // 是否自动关闭
            highWaterMark: 3,   // 写入缓存区的大小 默认 16kb
            encoding: 'utf-8',  // 写入的编码格式
            start: 0            // 开始写入文件的位置
        }

    可写流通过反复调用 writeStream.write(chunk) 方法将数据放到缓存区中, 当内部可写缓存区的总大小 小于 highWaterMark 制定的阈值时, 调用 writeStream.write(chunk) 将返回 true。 一旦内部缓存区的大小达到或超过 highWaterMark, 调用 writeStream.write(chunk) 将返回 false。

        let fs = require('fs');
        let path = require('path');
        let i = 9;
    
        let ws = fs.createWriteStream(path.join(__dirname, '1.txt'), {
            flags: 'w',
            mode: 0o666,     // 设置文件模式,默认 0o666
            autoClose: true,
            highWaterMark: 3,
            encoding: 'utf-8',
            start: 0
        });
    
        function write () {
            let flag = true;
            while ( i > 0 && flag ) {
    
                // 返回的标识符 表示的并不是是否写入,而是能否继续写,返回 false 时数据也不会丢失,会把内容放到缓存区中, 当 flag 变为 true 时, 会先将缓存区的内容写入到文件内容中去, 当缓存区的内容消费完时会出发 drain 事件
                flag = ws.write(--i+'','utf8',() => { console.log('ok') });
                console.log(flag);
            }
        }
        write();
    
        // 抽干, 当缓存区满了之后又被清空后才会触发 drain 事件
        //当一个流不处在 drain 的状态, 对 write() 的调用会缓存数据块, 并且返回 false。 一旦当前所有缓存的数据块都排空了, 那么 'drain' 事件就会被触发建议, 一旦 write() 返回 false, 在 'drain' 事件触发前, 不能写入任何数据块
        ws.on('drain', function () {
            console.log('抽干');
            write();
        });
        // ws.end('文件关闭'); // 当调用 end 方法后 , 文件写入结束,就不能在继续调 write 方法, 调用 end 方法后将不会触发 drain 事件
  3. stream pipe

        // readStream.pipe(writeStream);
        let from = fs.createWriteStream(path.join(__dirname,'1.txt'))
        let to = fs.createWriteStream(path.join(__dirname,'2.txt'));
        from.pipe(to);

    pipe 方法的原理

        let fs = require('fs');
        let path = require('path');
        let ws = fs.createWriteStream(path.join(__dirname,'2.txt'));
        let rs = fs.createReadStream(path.join(__dirname,'1.txt'));
        rs.on('data', function (data) {
            var flag = ws.write(data);
            if(!flag) rs.pause();
        });
        ws.on('drain', function () {
            rs.resume();
        });
        rs.on('end', function () {
            ws.end();
        });

stream API 的关键目标, 尤其对于 stream.pipe() 方法, 就是限制缓存区的数据大小, 以达到可接受的程度。 这样, 对于读写速度不匹配的源头和目标, 就不会超出可用的内存大小。

这里写图片描述
在网上看到一张图描述的挺到位的(盗图一张),借鉴来总结一下:

当读取一个文件写入到另一个文件时的过程:
首先会创建一个可读流, 我们有两种选择,
1. 当监听 readable 事件时, 此时 readable._readableState.flowing 属性默认为 null, 当监听 readable 事件时,会自动读区 highWaterMark 这么多的数据到缓存区中等待被消费,这时流属于暂停模式, 当 手动调用
流的 read 方法后,流转变为流动模式,会读取缓存区的内容消费, 当缓存区的内容读取 highWaterMark 这么多数据之后(缓存区被清空后),会再次触发 readable 事件, 直到源文件中所有的数据被读区完毕
2. 当监听 data 事件时,此时 readable._readableState.flowing 属性为 true, 可读流会直接读取 highWaterMark 这么多的数据在 data 的回调函数中供消费,流的模式转变为为 flowing ,这时会不停的触发的 data 事件,直到源文件中的数据全部读取完毕, 这个过程中可以调用 stream.pause 暂停流的读取。
以上两种读区模式的区别, readable 可以对流读取的数据进行精准的控制, 而当我们不需要精确的操作流时,可以选择 data 的流动方式, 效率更高 。
上面的两种方式不管以哪种读取的数据,我们都可以创建一个可写流对读取到的数据进行写入的消费,
调用 write 方法会像文件中写入数据, 但是因为写入的速度较慢, 如果当前正在写入, 而同时又调用了 write 方法, node 会将你写入的内容暂存到缓存区中, 等到文件写入完毕会从缓存区中取出数据, 继续写入。
write 方法执行会返回一个布尔类型的值, 表示目前是否还可以继续调用 write 方法写入内容(到内存中), 当缓存区的大小大于 highWaterMark 时,这是会返回 false , 应当停止数据的写入,以避免消耗过多内存。 当缓存区满后,文件写入一直在进行,一会儿就把缓存区的内容全部写入,缓存区处于清空状态, 这时会触发 drain 事件, 我们可以继续写入文件
注意:如果缓存区从未满过,drain 事件永远也不会触发。

可读流/可写流 剖析代码 点我

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值