我们常说的stream由三个部分构成,即source(源头) + stream(stream中的数据为chunk 块状的) + sink(水池)
认识writeStream
首先,我们来看下面这一段代码
const fs = require('fs')
const stream = fs.createWriteStream('./big_file.txt')
for (let i = 0; i < 10000000; i++) {
stream.write(`这里有很多数据,这是第${i + 1}条\n`)
}
stream.end()
上面的这一段代码可以创建一个很大的文件,现在,我们创建一个服务器尝试请求它。
const http = require('http')
const fs = require('fs')
const server = http.createServer()
server.on('request', (request, response) => {
fs.readFile('./big_file.txt', (error, data) => {
response.write(data)
response.end()
})
})
server.listen('8888')
我们发现,仅仅响应一个文件,node就占用了很多的内存
所以我们需要修改一下我们的代码,不能通过fs来读取,而是通过stream,这样的话,就会读取多次,每次只会读取一段chunk,而不是一次性读取整个文件,从而降低内存占用。
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)
})
server.listen('8888')
我们看到,使用了readStream之后,内存占用减小了一半不止
上面的代码中,我们使用到了pipe,它的中文意思为 管道,相当于使用了一个管道将读写连接了起来,对数据的传输量进行了限制。
管道其实也可以通过事件来实现,但是我们一般不这么写
// stream1 一有数据就传给 stream2
stream1.on('data', (chunk)=>{
stream2.write(chunk)
})
// stream1 结束的话,同时结束 stream2
stream1.on('end', ()=>{
stream2.end()
})
Readable 和 Writable 支持的事件与方法
readable Stream 的 data事件 与 end事件,我们在上面的代码中已经使用过了,我们现在在来看两个比较重要的Writeable Stream的事件—— drain 与 finish
- drain
drain的意思是流干了,即pipe中传来的数据已经被写完了。
这种情况常发生在读写大量数据时,由于数据传的太快了,硬盘/内存来不及写数据,会造成数据堆积,而当堆积的数据被写完时,就会触发drain事件。
我们稍稍修改一下我们的服务器,就能监听到drain事件了
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)
response.on('drain', () => {
console.log('drain')
})
})
server.listen('8888')
上面的代码会多次触发drain
我们可以通过flag来判断源头端是否读的太快了
stream1.on('data', (chunk)=>{
let flag = stream2.write(chunk)
//flag = true 表示正常,false表示读的太快堵车了
})
- finish事件
finish事件会在 调用stream.end()之后 + 缓冲区的数据传给操作系统之后 就会触发
此外,我们还要注意以下,readableStream 默认是处于禁止态的,只有当添加data事件监听后才会变为流动态。我们也可以通过pause()和resume()进行控制。
双向Stream
stream除了 read 和 write 之外,还有两个双向的流,即Duplex和Transform
虽然Duplex和Transform都是双向流,但他们之间还是有区别的
Duplex类似于readableStream和writableStream的结合。
而Transform则更类似于babel,是自己读自己写的,类似于通过babel将ES6文件转换为ES5文件
自定义stream
我们上面的代码都是中使用stream,现在,我们可以尝试一下自定义stream
- 自定义writableStream
const stream = require('stream')
const {Writable} = stream
const outStream = new Writable({
write(chunk, encoding, callback) {
console.log(chunk.toString())
callback()
}
})
process.stdin.pipe(outStream)
- 自定义readableStream
const stream = require('stream')
const {Readable} = stream
const inStream = new Readable({
read(size) {
const char = String.fromCharCode(this.currentCharCode++)
if (this.currentCharCode < 92) {
this.push(char)
}
}
})
inStream.currentCharCode = 65
inStream.pipe(process.stdout)
- 自定义DuplexStream
const {Duplex} = require('stream')
const inOutStream = new Duplex({
read(size) {
const char = String.fromCharCode(this.currentCharCode++)
this.push(char)
if (this.currentCharCode > 90) {
this.push(null)
}
},
write(chunk, encoding, callback) {
const data = chunk.toString()
console.log(data)
callback()
}
})
inOutStream.currentCharCode = 65
process.stdin.pipe(inOutStream).pipe(process.stdout)
自定义DuplexStream其实就是相当于将自定义readableStream和自定义writableStream结合起来。
- 自定义tramsform
const {Transform} = require('stream')
const tr = new Transform({
transform(chunk, encoding, callback) {
this.push(chunk.toString().toUpperCase())
callback()
}
})
process.stdin.pipe(tr).pipe(process.stdout)
上面的代码可以实现将小写的字母自动转换为大写。
node中内置的TransformStream——zlib
通过zlib,我们可以实现对文件的加密压缩
const zlib = require('zlib')
const fs = require('fs')
const crypto = require('crypto')
fs.createReadStream('./big_file.txt')
.pipe(crypto.createCipher('aes192', '123456'))
//加密
.pipe(zlib.createGzip())
//压缩
.on("data", () => {
process.stdout.write('.')
})
.pipe(fs.createWriteStream('./gz.gz'))
.on('finish', () => {
console.log('DONE')
})
我们也可以将监听data事件单独抽离出来
const zlib = require('zlib')
const fs = require('fs')
const crypto = require('crypto')
const {Transform} = require('stream')
const reportTransform = new Transform({
transform(chunk, encoding, callback) {
process.stdout.write('.')
callback(null, chunk)
}
})
fs.createReadStream('./big_file.txt')
.pipe(crypto.createCipher('aes192', '123456'))
.pipe(zlib.createGzip())
.pipe(reportTransform)
.pipe(fs.createWriteStream('./gz.gz'))
.on('finish', () => {
console.log('DONE')
})
如下图所示,stream 在nodejs中有非常广泛的运用