我们今天来学习一下nodejs中的Stream相关知识,我们从以下几个例子开始讲起。
- 第一个例子,新建1.js
js
const fs = require('fs')
const stream = fs.createWriteStream('./big_file.txt')
for (let i = 0; i < 1000000; i++) {
stream.write(`这是第${i}行内容,我们需要很多很多内容,要不停地写文件啊啊啊啊啊啊回车\n`)
}
stream.end()//别忘了关掉
console.log('done')
- 通过运行上面的这段代码,我们能得到一个10000行的文件
- 分析
- 我们通过打开流,多次往里面塞内容,关闭流
- 最终我们得到一个1mb左右的文件
- 什么是Steam流
- 释义
- stream是水流,但默认没有水
- stream.write可以让水流中有水(数据)
- 每次写的小数据叫做chunk(块)
- 产生数据的一段叫做source(源头)
- 得到数据的一段叫做sink(水池)
- 第二个例子,没有使用数据流进行读写,新建2.js
js
const http = require("http")
const fs = require("fs")
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)
console.log('8888')
- 直接用浏览器或者chrome访问
- 分析
-
用任务管理器看看Node.js内存占用,会很大概100mb,这就是不用流的问题
-
访问前内存
-
访问后开始读取文件的内存
-
- 第三个例子,使用Stream改写第二个例子
js
const http = require("http")
const fs = require("fs")
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)
console.log('8888')
- 分析
- 内存占用会少很多
- 什么是管道(pipe)
- 释义
- 两个流可以用一个管道相连,stream1的末尾连接上stream2的开端,只要stream1有数据,就会流到stream2
- 常用代码
js
stream1.pipe(stream2)
// 链式操作
a.pipe(b).pipe(c)
// 等价于
a.pipe(b)
b.pipe(c)
- 管道可以通过事件实现,一般不这么写直接用pipe
js
// stream1 一有数据就塞给 stream2
stream1.on('data',(chunk)=>{
stream2.write(chunk)
})
// stream1停了,就停掉stream2
stream1.on('end',()=>s{
stream2.end()
})
- Stream对象的原型、事件
- 新建4.js
ini
const fs = require('fs')
const s = fs.createReadStream('./big_file.txt')
console.log(s)
- 运行
node --inspect-brk 4.js
- 打开浏览器进入调试模式,打上断点
- 可以在控制台看到steam的层级结构,也就是Stream对象的原型链
- 自身属性(由fs.ReadStream构造)
- 原型:stream.Readable.prototype
- 二级原型:stream.Stream.prototype
- 三级原型:events.EventEmitter.prototype
- 四级原型:Object.prototype
- Stream对象都继承了EventEmitter
- Stream支持的事件和方法
js
stream.on('data', (chunk)=>{
console.log(chunk)
// chunk是一个buffer,就是每次把数据读成二进制的形式放在内存里,没有时间变成字符串,字符串需要编码,这里打出来就是十六进制表示01
console.log(chunk.toString()) //就可以变成字符串
})
- 这里特别注意下drain事件,因为面试常考,但是实际开发用的不多。可以参考下面的例子,
- 简单理解,在处理大量数据或以较慢的速度写入数据的情况下,写入数据可能会超过可写流的缓冲区容量,导致暂时停止写入。drain事件的触发表示缓冲区已经排空,可以继续写入数据了。
js
const fs = require('fs');
const writableStream = fs.createWriteStream('example.txt');
// 假设这是一个大型数据流
const data = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
// 当写入的数据量超过缓冲区大小时,write 方法会返回 false
while (!writableStream.write(data)) {
console.log('缓冲区已满,等待 drain 事件');
// 在 drain 事件发生后继续写入
}
// 当缓冲区排空时,会触发 drain 事件
writableStream.on('drain', () => {
console.log('缓冲区已排空,可以继续写入');
});
console.log('数据写入完成');
- Stream的分类
- 有四类
- 可读流 (Readable),提供从数据源读取数据的流。示例:文件读取流、HTTP 请求流。
js
const fs = require('fs');
const readableStream = fs.createReadStream('example.txt');
- 可写流 (Writable),提供向目标写入数据的流。示例:文件写入流、HTTP 响应流。
js
const fs = require('fs');
const writableStream = fs.createWriteStream('output.txt');
- 双工流(可读可写双向) (Duplex) ,同时具有读取和写入功能的流。示例:网络套接字、WebSocket。
js
const net = require('net');
const duplexStream = new net.Socket();
- 转换流(可读可写变化) (Transform),类似于双工流,但是可以修改或转换数据。示例:数据压缩、加密。
js
const zlib = require('zlib');
const transformStream = zlib.createGzip();
- 可读和可写流的特点
- 可读(Readable Stream)
- 可读流有静止态paused和流动态flowing
- 默认处于paused态
- 添加data事件监听,它就变为flowing态
- 删掉data事件监听,它就变为paused态
- pause()可以将它变为paused
- resume()可以将它变为flowing
- 可写(Writeable Stream)
- drain流干了事件
- 表示可以加点水了
- 调用stream.write(chunk)的时候,可能会得到false
- false的意思是你写太快了,数据积压了
- 这个时候我们就不能再write了,要监听drain
- 等drain事件触发了,我们才能继续write
- finish事件
- 调用stream.end()之后
- 而且缓冲区数据都已经传给底层系统之后
- 触发finish事件
- drain流干了事件