NodeJS 难点(网络,文件)的 核心 stream 三:readable ?
什么是可读流
可读流 常见 读取磁盘文件、读取网络请求内容等,看一下前面介绍什么是流用的例子:
const rs = fs.createReadStream(filePath);
我们常见的控制台 process.stdin 也是一个可读流:
process.stdin.pipe(process.stdout);
自定义菜单
除了系统提供给我们的 fs.CreateReadStream
我们还经常使用 gulp 或者 vinyl-fs 提供的 src 方法
gulp.src(['*.js', 'dist/**/*.scss'])
如果我们想自己以某种特定的方式 做菜单,那么改如何开始呢?
简单两步即可
- 继承 stream 模块的 Readable 类
- 重写 **_read** 方法,调用 this.push 将菜放入 菜单表
Readable 类已经把可读流要做的大部分工作完成,我们只需要继承它,
然后把生产数据的方式写在 _read 方法里就可以实现一个自定义的可读流。
如果我们想实现一个每 100 毫秒生产一个随机数的流(没什么用处)
const Readable = require('stream').Readable;
class RandomNumberStream extends Readable {
constructor(max) {
super()
}
_read() {
const ctx = this;
setTimeout(() => {
const randomNumber = parseInt(Math.random() * 10000);
// 只能 push 字符串或 Buffer,为了方便显示打一个回车
ctx.push(`${randomNumber}\n`);
}, 100);
}
}
module.exports = RandomNumberStream;
类继承部分代码很简单,主要看一下 _read 方法的实现,有几个值得注意的地方
- Readable 类中默认有 _read 方法的实现,不过什么都没有做,我们做的是覆盖重写
- _read 方法有一个参数 size,用来向 read 方法指定应该读取多少数据返回,不过只是一个参考数据,很多实现忽略此参数,我们这里也忽略了,后面会详细提到
- 通过 this.push 向缓冲区推送数据,缓冲区概念后面会提到,暂时理解为挤到了水管中可消费了
- push 的内容只能是字符串或者 Buffer,不能是数字
- push 方法有第二个参数 encoding,用于第一个参数是字符串时指定 encoding
执行一下看看效果
const RandomNumberStream = require('./RandomNumberStream');
const rns = new RandomNumberStream();
rns.pipe(process.stdout);
这样可以看到数字源源不断的显示到了控制台上,我们实现了一个产生随机数的可读流,还有几个小问题待解决
如何停下来
我们每隔 100 毫秒向缓冲区推送一个数字,那么就像读取一个本地文件总有读完的时候,如何停下来标识数据读取完毕?
向缓冲区 push 一个 null 就可以。我们修改一下代码,允许消费者定义需要多少个随机数字:
const Readable = require('stream').Readable;
class RandomNumberStream extends Readable {
constructor(max) {
super()
this.max = max;
}
_read() {
const ctx = this;
setTimeout(() => {
if (ctx.max) {
const randomNumber = parseInt(Math.random() * 10000);
// 只能 push 字符串或 Buffer,为了方便显示打一个回车
ctx.push(`${randomNumber}\n`);
ctx.max -= 1;
} else {
ctx.push(null);
}
}, 100);
}
}
module.exports = RandomNumberStream;
我们使用了一个 max 的标识,允许消费者指定需要的字符数,在实例化的时候指定即可
const RandomNumberStream = require('./RandomNumberStream');
const rns = new RandomNumberStream(5);
rns.pipe(process.stdout);
这样可以看到控制台只打印了 5 个字符
为什么是 setTimeout 而不是 setInterval
细心的同学可能注意到,我们每隔 100 毫秒生产一个随机数并不是调用的 setInterval,而是使用的 setTimeout,
为什么仅仅是延时了一下并没有重复生产,结果却是正确的呢?
这就需要了解流的两种工作方式
- 流动模式:数据由底层系统读出,并尽可能快地提供给应用程序
- 暂停模式:必须显示地调用 read() 方法来读取若干数据块
流在默认状态下是处于暂停模式的,也就是需要程序显式的调用 read() 方法,可我们的例子中并没有调用就可以得到数据,
因为我们的流通过 pipe() 方法切换成了流动模式,这样我们的 _read() 方法会自动被反复调用,直到数据读取完毕, 流水不断
所以我们每次 _read() 方法里面只需要读取一次数据即可。
流动模式和暂停模式切换
流从默认的暂停模式切换到流动模式可以使用以下几种方式:
- 通过添加 data 事件监听器来启动数据监听
- 调用 resume() 方法启动数据流
- 调用 pipe() 方法将数据转接到另一个 可写流
从流动模式 切换为 暂停模式又两种方法:
- 在流没有 pipe() 时,调用 pause() 方法可以将流暂停 pause 不能止住水流
- pipe() 时,需要移除所有 data 事件的监听,再调用 unpipe() 方法
data 事件
使用了 pipe() 方法后 数据 就从 可读 流进入了 可写流,
但对我们好像是个黑盒,数据究竟是怎么流向的呢?我们看到切换流动模式 和 暂停模式的时候有两个重要的名词
- 流动模式对应的 data 事件
- 暂停模式对应的 read() 方法
这两个机制是我们能够驱动数据流动的原因,先来看一下流动模式 data 事件,
一旦我们监听了可读流的 data 时、事件,流就进入了流动模式,我们可以改写一下上面调用流的代码
const RandomNumberStream = require('./RandomNumberStream');
const rns = new RandomNumberStream(5);
rns.on('data', chunk => {
console.log(chunk);
});
这样我们可以看到控制台打印出了类似下面的结果
<Buffer 39 35 37 0a>
<Buffer 31 30 35 37 0a>
<Buffer 38 35 31 30 0a>
<Buffer 33 30 35 35 0a>
<Buffer 34 36 34 32 0a>
当可读流生产出可供消费的数据后就会触发 data 事件,
data 事件监听器绑定后,数据会被尽可能地传递。
data 事件的监听器可以在第一个参数收到可读流传递过来的 Buffer 数据,
这也就是我们打印的 chunk,如果想显示为数字,可以调用 Buffer 的 toString() 方法。
当数据处理完成后还会触发一个 end 事件,应为流的处理不是同步调用,
所以如果我们希望完事后做一些事情就需要监听这个事件,我们在代码最后追加一句:
rns.on('end', () => {
console.log('done');
});
这样可以在数据接收完了显示 'done'
当然数据处理过程中出现了错误会触发 error 事件,我们同样可以监听,做异常处理:
rns.on('error', (err) => {
console.log(err);
});
read(size)
流在暂停模式下需要程序显式调用 read() 方法才能得到数据。
read() 方法会从内部缓冲区中拉取并返回若干数据,当没有更多可用数据时,会返回null。
使用 read() 方法读取数据时,如果传入了 size 参数,那么它会返回指定字节的数据;
当指定的size字节不可用时,则返回null。
如果没有指定size参数,那么会返回内部缓冲区中的所有数据。
现在有一个矛盾了,在流动模式下流生产出了数据,
然后触发 data 事件通知给程序,这样很方便。
在暂停模式下需要程序去读取,那么就有一种可能是读取的时候还没生产好,
如果我们才用轮询的方式未免效率有些低。
NodeJS 为我们提供了一个 readable 的事件,事件在可读流准备好数据的时候触发,
也就是先监听这个事件,收到通知又数据了我们再去读取就好了:
const rns = new RandomNumberStream(5);
rns.on('readable', () => {
let chunk;
while((chunk = rns.read()) !== null){
console.log(chunk);
}
});
这样我们同样可以读取到数据,值得注意的一点是并不是每次调用 read() 方法都可以返回数据,
前面提到了如果可用的数据没有达到 size 那么返回 null,所以我们在程序中加了个判断。
数据会不会漏掉
开始使用流动模式的时候我经常会担心一个问题,上面代码中可读流在创建好的时候就生产数据了,
那么会不会在我们绑定 readable 事件之前就生产了某些数据,触发了 readable 事件,我们还没有绑定,这样不是极端情况下会造成开头数据的丢失嘛
可事实并不会,按照 NodeJS event loop 我们创建流和调用事件监听在一个事件队列里面,
儿生产数据由于涉及到异步操作,已经处于了下一个事件队列,我们监听事件再慢也会比数据生产块,数据不会丢失。
看到这里,大家其实对 data事件、readable事件触发时机, read() 方法每次读多少数据,
什么时候返回 null 还有又一定的疑问,因为到现在为止我们接触到的仍然是一个黑盒,
后面我们介绍了可写流后会在 back pressure 机制部分对这些内部细节结合源码详细讲解,且听下回分解吧。
NodeJS 难点(网络,文件)的 核心 stream 三:readable ?的更多相关文章
- NodeJS 难点(网络,文件)的 核心 stream 二:stream是什么
对于大部分有后端经验的的同学来说 Stream 对象是个再合理而常见的对象,但对于前端同学 Stream 并不是那么理所当然,github 上甚至有一篇 9000 多 Star 的文章介绍到底什么是 ...
- NodeJS 难点(网络,文件)的 核心 stream 四: writable
什么是可写流 白板 可写流是对数据流向设备的抽象,用来 消费 上游流过来的数据 通过可写流程序可以把数据写入设备, 常见的是 本地磁盘文件或者 TCP.HTTP 等网络响应. 看一个之前用过的例子 ...
- NodeJS 难点(网络,文件)的 核心 stream 一:Buffer
stream应用一图片转存服务 stream github教程 文件操作和网络都依赖了一个很重要的对象—— Stream, 而这个 <node深入浅出> 没有分析的, 所以读完这本书, 在 ...
- NodeJS学习笔记 (28)流操作-stream(ok)
模块概览 nodejs的核心模块,基本上都是stream的的实例,比如process.stdout.http.clientRequest. 对于大部分的nodejs开发者来说,平常并不会直接用到str ...
- 文件上传的三种模式-Java
文件上传的三种方式-Java 前言:因自己负责的项目(jetty内嵌启动的SpringMvc)中需要实现文件上传,而自己对java文件上传这一块未接触过,且对 Http 协议较模糊,故这次采用渐进的方 ...
- 百度APP移动端网络深度优化实践分享(三):移动端弱网优化篇
本文由百度技术团队“蔡锐”原创发表于“百度App技术”公众号,原题为<百度App网络深度优化系列<三>弱网优化>,感谢原作者的无私分享. 一.前言 网络优化解决的核心问题有三个 ...
- 使用AVFoundation仅仅生成缩略图,不进行播放视频(本地和网络文件都可以创建视频缩略图)
使用MPMoviePlayerController来生成缩略图足够简单,但是如果仅仅是是为了生成缩略图而不进行视频播放的话,此刻使用 MPMoviePlayerController就有点大材小用了.其 ...
- JAVA多线程下载网络文件
JAVA多线程下载网络文件,开启多个线程,同时下载网络文件. 源码如下:(点击下载 MultiThreadDownload.java) import java.io.InputStream; im ...
- Linux 学习笔记_12_文件共享服务_3_NFS网络文件服务
NFS网络文件服务 NFS---- Network File System 用于UNIX/Linux[UNIX类操作系统]系统间通过网络进行文件共享,用户可以把网络中NFS服务器提供的共享目录挂载到本 ...
随机推荐
- 读书笔记:Spring boot实战
第一章 入门 Spring boot最重要的四个核心 : 1.自动配置:针对很多spring应用程序常见的应用功能,spring boot能自动提供相关配置 2.起步依赖:告诉spring boot需 ...
- Sublime Text 3.1.1 Build 3176 注册码破解
在hosts(C:\Windows\System32\drivers\etc)加入如下内容: 127.0.0.1 www.sublimetext.com127.0.0.1 li ...
- ORA-01940:无法删除当前已连接的用户
当时时间比较着急,直接采取了釜底抽薪的办法——拔网线,这招很管用,再次执行drop user就成功了.考虑到以后不一定所有场合都允许你采取这种简单粗暴的办法,就在虚拟机上重现了这个报错,寻求一种较为温 ...
- tp5.0 composer命令插件
1.单元测试composer require topthink/think-testing 1.* (5.0) composer require topthink/think-testing 5.1官 ...
- LRU缓存淘汰算法
什么是LRU算法? LRU是Least Recently Used的缩写,即最近最少使用,在有限的内容块中存储最近使用次数最多的数据,当内容块已满时,把最少使用的数据删除以便存储新的内容.
- Python 爬虫-Requests库入门
2017-07-25 10:38:30 response = requests.get(url, params=None, **kwargs) url : 拟获取页面的url链接∙ params : ...
- codeforces 1042d//Petya and Array// Codeforces Round #510 (Div. 2)
题意:给出一个数组,求其中和小于t的区间数. 先计算前缀和数组sum[i].对当前的sum[i],查询树状数组中有几个比(sum[i]-t)大的数,那么用sum[i]减它就是一个合法区间.再将当前的s ...
- CSS单位分析
px:单位代表像素,1px代表一个像素点. %:设置子元素为父容器的占比. em:代表该元素中一个字体所占字符,常用在文字首行缩进.其具有继承性. rem:始终代表html中的字符所在位置. vm:1 ...
- Hackintosh Power Management
Also, be aware that hibernation (suspend to disk or S4 sleep) is not supported on hackintosh. You sh ...
- JS-图片控制-动画管理模块
animateManage.js ;(function(window,document,undefined){ var _aniQueue = [], //动画队列 --- ani:动画,Queue: ...