一、三个例子了解Stream流
流的好处就是每次可以只传一小段数据(chunk,传的是二进制,而不是字符串),这样就可以使服务器内存占用变得非常低(一点一点的读和写)
第一个Stream例子
cosnt fs = require('fs')
const stream = fs.createWriteStream('./big_file.txt')
for(let i=0; i<100000;i++){
stream.write(`这是第${i}行内容,我们需要很多狠毒内容,要不停的写文件啊啊啊\n`)
}
stream.end() // 别忘了关掉 stream
console.log('done')
使用ls -lh
可以查看当前目录下文件的大小
分析
- 打开流,多次往里面塞内容(每次塞是不会覆盖之前的东西的,也就是说是分开的),关闭流
- 看起开就是可以多次写,没什么大不了
- 最终我们得到一个文件
Sream流
- stream是水流,但默认没有水 (图中的虚线部分)
stream.write
可以让水流中有水(数据)- 每次写的小数据叫做
chunk
(块) - 产生数据的一段叫做
source
(源头) - 得到数据的一段叫做
sink
(水池)
备注
chunk一般表示不完整的数据;data表示完整的数据
第二个例子
如果用readFile读一个很大的文件,将消耗很大的服务器内存
// 先引入fs 和 http
const server = http.createServer()
server.on('request',(request,response)=> {
fs.readFile('./big_file.txt',(error,data)=>{
if(error) throw error
response.end(data)
console.log('done')
})
})
server.listen(8888)
分析
- 用任务管理器看看 Node.js 内存占用35M
一个用户请求过来就用了35M内存,如果10个、100个用户请求过来呢?服务器有多少内存够用??
我们需要借助Stream来解决这个问题
第三个例子
如果使用Stream
的方式改写将会消耗更小的内存
用Stream改写第二个例子
const fs = require('fs')
const http = require('http')
const server = http.createServer();
server.on('request',(request,response)=>{
const stream = fs.createReadStream('./big_file.txt')
stream.pipe(response)
stream.on('end',()=> console.log('done'))
})
server.listen(8888)
可以看出使用stream流的方式传数据,时间会稍微长一点(因为是一点一点的传),但是占用的内存明显小了很多
分析
- 查看Node.js内存占用,基本不会高于15M
- 文件
stream
和response stream
通过管道相连,也就是说response
也是一个流
备注
- 流(stream) 通过管道(pipe) 流向 response
管道
管道可以把2个以上流通过管道连起来,连起来之后就可以实现数据在不同地方的转化,比如:从文件转换为网络
管道就是监听一个流,把它的数据塞给另外一个流
备注
- stream1 是文件流,读bigfile文件
- stream2 是http流,即response
- 它们之间本来没有关系,但是如果用管子(pipe)把它们接起来,那么他们就有关系了。stream1中所有的数据都会自动的流向stream2,即读一段bigfile文件的内容,就会传给http,http就会传给用户...
释义
两个流可以用一个管道相连
stream1 的末尾连接上 stream2 的开端
只要stream1 有数据,就会流到stream2
常用代码
stream1.pipe(stream2)
备注
- stream1是一个会发出数据的stream
- stream2是一个需要接收数据的stream
- 即一个可读的流通过一个管道(pipe)连接一个可写的流
链式操作
a.pipe(b).pipe(c)
// 等价于
a.pipe(b)
b.pipe(c)
管道续
管道可以通过事件实现
// stream1 一有数据就塞给 stream2
stream1.on('data',(chunk)=>{
const flag = stream2.write(chunk)
if(flag === false){ // 不写文件了 }
stream2.on('drain',()=>{
// 继续写文件
})
})
// stream1 停了,就停掉 stream2
stream1.on('end',()=>{
stream2.end()
})
一般不这么写,写这么多代码,其实就是一个pipe
搞定
如果flag
为true表示『没堵车』;如果flag
为false表示『堵车了』
堵车的意思就是图中stream1
流的速度太快了,stream2
的管道宽度有限,就“堵车了”
如果“堵车了” stream1
就不再写文件了
那不能“堵车了”就一直不走了或者说一直不写文件了吧
什么时候开始继续写文件呢?
drain
的意思是流干了
意思是当stream
不“堵车了”,stream1
就可以继续写文件了
drain
的事件就是发现,之前“湍急的河流”现在已经“干涸了”
二、Stream 对象的原型、事件
尝试打印这个对象
新建4.js
const fs = require('fs')
const s = fs.createReadStream('./big_file.txt')
console.log(s);
如果运行node 4.js
会发现:打印出来的对象只有一层
如果我们想详细的了解这个对象的结构就需要运行:
node --inspect-brk 4.js
,意思就是打断点
然后随便打开一个网页,打开控制台,过一会就会出现Node的调试工具
接着,打断点,执行
在控制台中打印这个对象
Stream 对象的原型链
s = fs.createReadStream(path)
- 从上图可以看出
- 它的对象层级为
- 自身属性(由fs.ReadStream这一构造函数构造出来的)
- 原型:
stream.Readable.prototype
- 二级原型:
stream.Stream.prototype
- 三级原型:
events.EventEmitter.prototype
,所有的Stream
对象都继承了EventEmitter
,它拥有pipe
这一API - 四级原型:
Object.prototype
Stream 对象都继承了 EventEmitter
备注:
除了可以在浏览器中访问一个地址外,也可以通过命令curl
访问一个地址
支持的事件和方法
const fs = require('fs')
const http = require('http')
const server = http.createServer();
server.on('request',(request,response)=>{
const stream = fs.createReadStream('./big_file.txt')
stream.pipe(response)
stream.on('data',(chunk)=>{
console.log('读取了一次数据')
console.log(chunk.toString())
})
stream.on('end',()=> console.log('全部读完了'))
})
server.listen(8888)
备注:
- 比较重要的事件就是上面红色标注的事件
drain
的意思是这一次写完了finish
的意思是整个全部写完了
三、Stream 的分类共4种
备注
- 可读的文件说明它会产生数据,因此我们可以读它
- 可写的文件说明它可以处理数据,所以我们可以往它里面写东西
- 可读或可写相当于单向的马路,
duplex
和transform
是双向的马路 duplex
中你不会读到你写的内容,不会产生交叉transform
和duplex
则不同,transform
相当于一个处理器 自己读自己写的内容,中间有一层转换器,比如:webpack中的bable就相当于一个transform,它可以将ES6转成浏览器能识别的JS,我们在写入ES6的时候,我们在另外一边可以读到ES5,中间的问号“?”,就是把ES6转为ES5,因此这种流是非常有用的,甚至是比duplex还有用,因为它可以把我们流的数据,变成另外一种形式的数据,如:把scss变成css、把TS变成JS。也好比是一个黑色的车进去了,经过清洁喷漆处理后,就变成了白色的车(上图所示)
四、Readable 和 Writable 的特点
Readable Stream
静止态 paused 和流动态flowing
- 默认处于paused 态
- 添加data事件监听,它就变成 flowing 态,流动
flowing
态发出数据的间隔不是人为控制的,而是它自己控制的 - 删掉data事件监听,它就变成paused态
- pause()可以将它变为 paused
- resume() 可以将它变成 flowing
备注
可读的流可以认为是一个『内容生产者』,比如我们读书,书就相当于内容生产者,因为书中有内容我们才去读。那么可读的Stream也是,因为它里面有内容,我们采取读它
它里面的内容会一点一点的发出来,但是有可能它就不发了,一旦不发了,它就处于静止态paused
如果它又恢复了,发内容的状态,它就变成了流动态flowing
就像下面的代码,如果不写stream.pipe(response)
,那么就意味着不使用管道pipe
把stream
流向response
那么此时它有没有读呢?
如果有,那么就存在一个问题了,如果我们还没做任何事情它就开始读了,那么它读的文件流向何处?没人监听它,是不是就意味着自然的消失了
const fs = require('fs')
const http = require('http')
const server = http.createServer();
server.on('request',(request,response)=>{
const stream = fs.createReadStream('./big_file.txt')
// stream.pipe(response)
})
server.listen(8888)
那么什么时候开始读呢?
当我们添加data事件,它就变成flowing
态
pipe就是监听第一个流的data事件,然后把数据写入到第二个流中,也就是说虽然写的是pipe
但是实际上写的是监听data
一旦流意识到“有人监听我了”,那么它就开始往外发内容了,你就可以读数据了
除了通过添加或删除data事件监听,让它变成flowing
或paused
态来控制它往外发内容
我们还可以主动的调用pause()
或resume()
方法来控制它往外发内容
当然,对于可写的流就没有这两种状态,你自己不写不就可以了?
Writable Stream
drain 『流干了』事件
- 表示可以加点水了
- 我们调用
stream.write(chunk)
的时候,可能会得到false - false的意思是你写的太快了,数据积压了
- 这个时候我们就不能再
write
了,要监听drain
- 等
drain
事件触发了,我们才能继续write
- 看文档中的代码就懂了
const fs = require('fs')
function writeOneMillionTimes(writer, data) {
let i = 1000000;
write();
function write() {
let ok = true;
do {
i--;
if (i === 0) {
// Last time!
writer.write(data);
} else {
// See if we should continue, or wait.
// Don't pass the callback, because we're not done yet.
ok = writer.write(data);
if(ok === false){
console.log('写的太快了,不能再写了')
}
}
} while (i > 0 && ok);
if (i > 0) {
// Had to stop early!
// Write some more once it drains.
writer.once('drain', ()=>{
console.log('干涸了')
write()
});
}
}
}
const writer = fs.createWriteStream('./big_file.txt')
writeOneMillionTimes(writer,'hello world')
如果写的太快了,就相当于stram1
中的车流流的太快了,stream2
管道宽度有限,快要“堵车了”,ok为false的时候,此时就不能再写了
这里的ok就相当于下面的flag
const flag = stream2.write(chunk)
:往stream2
里写数据
当stream2
中的车流越来越少,接近“干涸的”时候,通过监听drain
事件,stram1
就可以继续写了
finish 事件
- 调用stream.end()之后,而且
- 缓冲区数据都已经传给底层系统之后
- 触发finish事件
备注
什么是『缓冲区数据』?
当我们往文件里写东西的时候,并不是将文件直接写在硬盘上,而是有一个缓冲,到了一定程度之后才会写到硬盘
之前讲的内容都是在使用Stream
比如:创建一个可读的stream,是通过文件系统创建的const stream = fs.createReadStream('./big_file.txt')
,那是文件系统的Stream
比如: 创建一个可写的stream,是通过文件系统创建的const stream = fs.createWriteStream('./big_file.txt');
我们用的Stream都是别人提供的,
如何创建自己的流,给别人用呢?
比如: 创建一个可读的流,让别人来读我;创建一个可写的流,让别人来写我?
五、创建自己的流
创建一个 Writable Stream
让别人来给我们提供数据,我们只需要处理数据
const {Writable} = require('stream')
const outStream = new Writable({
write(chunk, encoding, callback) {
console.log(chunk.toString())
callback()
}
})
process.stdin.pipe(outStream)
// 如果不理解pipe可以写成监听data
process.stdin.on('data',(chunk)=>{
outStream.write(chunk)
})
你输入什么,就将什么输出出来
备注
- callback()必须调用,只有调用了才会进入下一次,否则就一直在那卡住
process.stdin
就是一个用于输入的Stream
,通过管道pipe
,将数据流向outStream
创建一个 Readable Stream
我们向别人提供数据
const { Readable } = require('stream')
const inStream = new Readable()
inStream.push('ABCDEFGHIJKLM')
inStream.push('NOPQRSTUVWXYZ')
inStream.push(null) // no more data
//inStream.pipe(process.stdout)
inStream.on('data',(chunk)=>{
process.stdout.write(chunk)
console.log('写了一次数据')
})
保存为文件为readable.js 然后用node运行
我们先把所有数据都push进去了,然后pipe(或者监听data)
备注
inStream.push(null)
的意思就是已经把所有的数据提供完了- 当我们提供数据的时候,默认数据是没有流出去的,默认可读的流是
pause
暂停状态,它不会把数据发出去,当监听它的data事件或者使用pipe
的时候,才把数据流出去 inStream.pipe(process.stdout)
的意思就是把数据通过管道流向用户的输出- 由于推数据的时候是分2次推的,因此读的时候也是分2次读
- 另外值的注意的是,数据是在我们推完之后,再去读的,不是一边推一边读
续
上面的例子是把所有数据都push进去了,然后再监听data事件,这样不太好
我们尝试把它改为:默认不push数据,当你找我要数据的时候,我再推送数据,而不是像之前的那样先把数据都push完了,你再找我要数据
const { Readable } = require('stream')
// 输出 A-Z
const inStream = new Readable({
read(size){
const char = String.fromCharCode(this.currentCharCode++)
this.push(char)
console.log(`推了${char}`)
if(this.currentCharCode > 90){ // "Z"
this.push(null) // 不再推了
}
}
})
inStream.currentCharCode = 65 // “A”
inStream.pipe(process.stdout)
保存文件为readable2.js 然后用node 运行
这次的数据是按需供给的,对方调用read 我们才会给一次数据
如果想要做一个可读的流,最好等别人调用你的read再去push
而不是先push完了,再等别人去调
两个方法都没错,只是更加推荐第二种方法
创建一个双向流 - Duplex Stream
其实只要同时实现write()
方法和 read()
方法就可以了
const { Duplex } = require('stream')
const inoutStream = new Duplex({
write(chunk, encoding, callback) {
console.log(chunk.toString())
callback()
},
read(size){
const char = String.fromCharCode(this.currentCharCode++)
this.push(char)
if(this.currentCharCode > 90){ // "Z"
this.push(null) // 数据推送结束
}
}
})
inoutStream.currentCharCode = 65 // 'A'
process.stdin.pipe(inoutStream).pipe(process.stdout)
创建一个Transform 流
这个要实现transform
方法
const { Transform } = require('stream')
const upperCaseTransform = new Transform({
transform(chunk, encoding, callback) {
this.push(chunk.toString().toUpperCase())
callback()
}
})
process.stdin.pipe(upperCaseTransform).pipe(process.stdout)
我们从用户的标准输入里监听它的data事件(pipe本质就是监听data事件)
只要用户输入任意字符,就会调用我们的transform
流,这个transform
流就会把这个字符变成大写
变成大写之后又会将结果输出给用户的标准输出
这样,用户就会看到自己输入的字变成大写了
备注
transform就是通过chunk.toString()
读到数据
然后将数据变化形式,通过.toUpperCase()
将数据大写
再通过push
方法将数据写进去
就像bable一样
bable读我们写的ES6代码
将ES6代码变成ES5
最后再写给你
六、Transform 流举例
内置的 Transform Stream
const fs = require('fs')
const zlib = require('zlib')
const file = process.argv[2]
fs.createReadStream(file)
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream(file + '.gz'))
将原本10M的文件压缩到了21kb
备注
- file就是用户传的路径
- 它要做的事是读这个可能很大的文件,那就一点一点的读,每读一点,就传给Gzip压缩,数据经过Gzip流压缩之后又传给一个可写的流,这个可写的流会把数据保存在以
.gz
结尾的文件中
续1
如果文件很大,希望有个进度条,用点表示
如果压缩完成,希望打印“压缩完成”
const fs = require('fs')
const zlib = require('zlib')
const file = process.argv[2]
fs.createReadStream(file)
.pipe(zlib.createGzip())
.on('data',()=>process.stdout.write("."))
.pipe(fs.createWriteStream(file + '.gz'))
.on('finish',()=>{console.log('压缩完成')})
打印出了5个点,说明传了5次数据给Gzip
备注
这句话的意思就是,当你在Gzip的过程中,就监听你的数据的情况,只要你的Gzip出一点数据,就打印出一个点
如果压缩完成,如何打印压缩完成这几个字呢?监听它的finish事件
续2
用变换流对数据进行处理,每次收到数据就打印进度“.”
const fs = require('fs')
const zlib = require('zlib')
const file = process.argv[2]
const { Transform } = require('stream')
const reportProgress = new Transform({
// 每次收到数据打一个点
transform(chunk, encoding, callback) {
process.stdout.write(".")
callback(null,chunk) // 等价于 this.push(chunk)
}
})
fs.createReadStream(file)
.pipe(zlib.createGzip())
.pipe(reportProgress)
.pipe(fs.createWriteStream(file + '.gz'))
.on('finish',()=>{console.log('压缩完成')})
效果不是一样吗,这样写有什么用呢?
很有用!!!这样我们就可以对数据进行无限的处理,如果想把数据变成大写的,就变成大写的;如果想给数据加分号,就加分号,这就已经很像webpack中的loader
比如在webpakck中
使用vue-loader加载vue的文件 => sass-loader 处理 => css-loader处理 => postcss-loader => style-loader
其实它们的原理就是变换流 transform
给我一个数据,可以将这个数据变换一下传给后面,后面可以读也可以写
续3
对数据进行压缩和加密
使用Node.js自带的crypto
加密模块
const fs = require('fs')
const zlib = require('zlib')
const file = process.argv[2]
const crypto = require('crypto')
const { Transform } = require('stream')
const reportProgress = new Transform({
// 每次收到数据打一个点
transform(chunk, encoding, callback) {
process.stdout.write(".")
callback(null,chunk) // 等价于 this.push(chunk)
}
})
fs.createReadStream(file)
.pipe(crypto.createCipher('aes192','123456'))
.pipe(zlib.createGzip())
.pipe(reportProgress)
.pipe(fs.createWriteStream(file + '.gz'))
.on('finish',()=>console.log('压缩完成'))
打开加密后的文件里面就是乱码
知道了密码,也可以对文件进行解密
备注
- 打开a.txt文件并展示前100行,可以用
head -n 100 a.txt
- 因为Gzip只能解密它自己压缩的文件,因此我们要先加密最后才压缩
Stream的用途非常广
Node.js中的Stream
数据流中的挤压问题
背压 back pressure
就是之前说的,如果我们往一个流里面写数据写的太多了,就会堵住
如果堵住了,怎么处理这个积压,变得流畅起来
这篇文章就介绍了
推荐阅读
Node.js Stream 文档
文档里有想要的所有的细节