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服务器提供的共享目录挂载到本 ...
随机推荐
- 【译】第1节--- EF Code First 介绍
原文:http://www.entityframeworktutorial.net/code-first/entity-framework-code-first.aspx 本教程涵盖了code fir ...
- React native 的DatePickerIOS组件
1.首先如代码所示 引入DatePickerIOS组件 设置他的时间是当前时间 export default class AlertDemo extends Component { render() ...
- [tixml]保存,读取
保存: //xml的实体 TiXmlElement* rootElement = new TiXmlElement("spark"); rootElement->SetAtt ...
- eclipse Git配置
Git 1:选择git 2:下载对应版本 安装 Git常用命令: 显示信息类命令 git ls-files -u 显示冲突的文件,-s是显示标记为冲突已解决的文件 git diff 对比工作区和st ...
- typescripts学习
可选与默认参数 可选参数:在参数名后面,冒号前面添加一个问号,则表明该参数是可选的.如下代码: function buildName(firstName: string, lastName?: str ...
- elementUI和iview兼容么
听说iview的作者居然是91年的,我要赶快加油了. https://zhuanlan.zhihu.com/p/25739512
- Windows 2008 更改网卡绑定顺序
用 ncpa.cpl 或者用鼠标右键点网上邻居进去也好. 来到网卡列表画面. 然后,你会发觉没有菜单去操作[高级设置], 这里,最高级的步骤来了, 就是你需要按一个 [Alt]把菜单给显示出来,太神奇 ...
- 4-3 atom订阅源
部分章节还没有实做练习. 网上购买了安道的Rails指南中文版.联系了这个作者问了一个问题Rails5的翻译问题. try(), 判断是否存在,不存在的话返回nil.例子:pasting @perso ...
- codeforces 578b//"Or" Game// Codeforces Round #320 (Div. 1)
题意:n个数字,最多操作k次,每次乘x,要使结果数组的与值最大. 能推断出结果是对一个元素操作k次,并且这个元素的二进制最高位比较大.并不一定是取最大的,比如1100和1010,乘以一次2,两种选法分 ...
- [洛谷P1507]NASA的食物计划 以及 对背包问题的整理
P1507 NASA的食物计划 题面 每个物品有三个属性,"所含卡路里":价值\(v\),"体积":限制1\(m_1\),以及"质量":限制 ...