一:stream
stream是nodejs中处理流式数据的抽象接口,stream模块为实现了stream接口的对象提供了基础的api。在nodejs中有很多流失对象,例如http请求、process.stdout等。流可以分为可读型、可写型、可读可写型,所有的流式对象都继承了EventEmiter,都是EventEmiter类的实例。在代码中引入流stream模块:
const stream=require('stream')
四种基本的流类型
- Readable:可读的流 (例如 fs.createReadStream()、request)
- Writeable: 可写的流(例如 fs.createWriteStream()、response)
- Duplex:双工流,可读可写,例如socket
Transform:在读写过程中可以修改和变换数据的 Duplex 流 (例如 zlib.createDeflate()).
缓存
-
Writable 和 Readable 流都会将数据存储到内部的缓冲器(buffer)中。这些缓冲器可以 通过相应的 writable._writableState.getBuffer() 或 readable._readableState.buffer 来获取。
-
缓冲器的大小取决于传递给流构造函数的 highWaterMark 选项。 对于普通的流, highWaterMark 选项指定了总共的字节数。对于工作在对象模式的流, highWaterMark 指定了对象的总数。
-
当可读流的实现调用stream.push(chunk)方法时,数据被放到缓冲器中。如果流的消费者没有调用stream.read()方法, 这些数据会始终存在于内部队列中,直到被消费。
-
当内部可读缓冲器的大小达到 highWaterMark 指定的阈值时,流会暂停从底层资源读取数据,直到当前 缓冲器的数据被消费 (也就是说, 流会在内部停止调用 readable._read() 来填充可读缓冲器)。
-
可写流通过反复调用 writable.write(chunk) 方法将数据放到缓冲器。 当内部可写缓冲器的总大小小于 highWaterMark 指定的阈值时, 调用 writable.write() 将返回true。 一旦内部缓冲器的大小达到或超过 highWaterMark ,调用 writable.write() 将返回 false 。
-
stream API 的关键目标, 尤其对于 stream.pipe() 方法, 就是限制缓冲器数据大小,以达到可接受的程度。这样,对于读写速度不匹配的源头和目标,就不会超出可用的内存大小。
-
Duplex 和 Transform 都是可读写的。 在内部,它们都维护了 两个 相互独立的缓冲器用于读和写。 在维持了合理高效的数据流的同时,也使得对于读和写可以独立进行而互不影响。
二:可读流 Readable
nodejs中的可读流有:
- client端的http responses
- server端的http requests
- fs 中的可读 streams
- zlib 模块的 streams
- crypto 模块的 streams
- tcp sockets
- child process 模块的 stdout 和 stderr
- process.stdin
1创建可读流
var rs = fs.createReadStream(path,[options]);
- path读取文件的路径
- options
- flags打开文件要做的操作,默认为’r’
- encoding默认为null
- start开始读取的索引位置
- end结束读取的索引位置(包括结束位置)
- highWaterMark读取缓存区默认的大小64kb
2data事件:数据来了
数据一块一块的来了
3pasuse事件: 暂停读取
缓冲区大小限制加上读写速不一致,有时可能需要暂停读取
4resume方法:重启读取任务
继续读取
5end事件:接收完毕
所有流数据已被接收完毕,回调方法里可以写业务逻辑了,该事件会在读完数据后被触发
6close事件
7error事件
8可读流简易实现
- 实现 readable.js
const fs = require('fs')
const EventEmiter = require('events')
class ReadAble extends EventEmiter {
constructor(path, options) {
super()
this.path = path //要读的文件的路径
this.highWaterMark = options.highWaterMark || 64 * 1024
this.flags = options.flags || 'r';
this.encoding = options.encoding || null
this.buffer = Buffer.alloc(this.highWaterMark) // 缓存池
this.flowing = null // 流的状态
this.start = 0;//初始化开始位置
this.fd = -1 //文件打开句柄
//开始读取文件
this.open(() => {
// 监测外部的读取事件
this.on('newListener', (event, listener) => {
//当外部监听data事件时就开始读取文件
if (event === 'data') {
this.flowing = true
this.read()
}
})
})
}
open(cb) {
fs.open(this.path, this.flags, (err, fd) => {
if (err) {
this.emit('error', err)
return
}
else {
this.emit('open') //向外发射open事件,通知文件已被打开
this.fd = fd
cb()
}
})
}
read() {
let offset = 0
let length = this.highWaterMark //每次读取highWaterMark字节
fs.read(this.fd, this.buffer, offset, length, this.start, (error, bytesRead, buffer) => {
if (error) {
this.emit('error', error)
this.destory()
return
}
else {
if (bytesRead) {
this.start += bytesRead; // 下一次读取的起始位置
let data = this.buffer
if (this.encoding) {
data = this.buffer.toString(this.encoding)
}
this.emit('data', data)
if (this.flowing) {
this.read()
}
}
else {
this.emit('end')
this.destory()
}
}
})
}
resume() {
console.log('重启');
this.flowing = true
this.read()
}
pause() {
console.log('暂停');
this.flowing = false
}
destory() {
if (this.fd != -1) {
fs.closeSync(this.fd)
}
this.emit('close')
}
/**
* @parmas: ws 可写流
* @Description: 实现管道
*/
pipe(ws){
this.on('data',chunk=>{
let full=ws.write(chunk)
if (!full) {
this.pause()
}
ws.on('drain',_=>{
this.resume()
})
})
}
}
module.exports = ReadAble
- 调用
const ReadAble = require('./readable.js')
const filepath = './tmp.txt'
const filepath2 = './tmp2.txt'
const options = {
highWaterMark: 3,
flags: 'r',
encoding: 'utf8'
}
var endFlag = false
const rs = new ReadAble(filepath, options)
rs.on('data', chunk => {
// console.log('--------------------------')
console.log(chunk);
})
rs.on('end', _ => {
endFlag=true
console.log('读取完毕')
})
rs.on('error', error => {
console.log('error', error)
})
const fs=require('fs')
var ws=fs.createWriteStream(filepath2,{
flags:'w+'
})
// 管道写入ws
rs.pipe(ws)
// 用于测试暂停重启
var tmer1 = setInterval(() => {
if (!endFlag) {
rs.pause()
var timer2 = setTimeout(() => {
rs.resume()
}, 150);
}
else {
clearInterval(tmer1)
clearTimeout(timer2)
}
}, 300);
- 实验数据 tmp.txt
123456789
- 实验结果
暂停
重启
123
456
789
读取完毕
三:可写流 Writeable
nodejs中的可写流有:
- 客户端http requests
- 服务器端的http responses
- fs模块的写入流
- zlib streams
- crypto streams
- tcp sockets
- child process模块的stdin
- process.stdout, process.stderr
1创建可写流
var ws = fs.createWriteStream(path,[options]);
2wirte()方法
3drain()方法
- 当一个流不处在 drain 的状态, 对 write() 的调用会缓存数据块, 并且返回 false。
- 一旦所有当前所有缓存的数据块都排空了(被操作系统接受来进行输出), 那么 ‘drain’ 事件就会被触发
建议, 一旦 write() 返回 false, 在 ‘drain’ 事件触发前, 不能写入任何数据块
4end()方法
表明接下来没有数据要被写入 Writable 通过传入可选的 chunk 和 encoding 参数,可以在关闭流之前再写入一段数据 如果传入了可选的 callback 函数,它将作为 ‘finish’ 事件的回调函数
5finish()方法
在调用了 stream.end() 方法,且缓冲区数据都已经传给底层系统之后, ‘finish’ 事件将被触发。
四:可写流与可读流使用实例
此示例演示了使用可写流与可读流实现复制文件的功能,涉及到的api有readable的data事件、end事件、pause方法、resume方法,writeable的drain事件、write方法、end方法,读写流都设置了highWaterMark可以直观的观察各事件的触发时机
var fs = require('fs'),
path = require('path'),
out = process.stdout;
var filePath = '/Users/hyb/Downloads/node-v10.14.1.pkg';
var readStream = fs.createReadStream(filePath, {
highWaterMark: 16 //默认64k
});
var writeStream = fs.createWriteStream('./node.pkg', {
highWaterMark: 16 //默认16k
});
var stat = fs.statSync(filePath);
var totalSize = stat.size;
var passedLength = 0;
var lastSize = 0;
var startTime = Date.now();
readStream.on('data', function (chunk) {
passedLength += chunk.length;
// 写入的缓存区已满 达到writeable的highWaterMark
if (writeStream.write(chunk) === false) {
out.write(`writeStream highWaterMark;readStream.pause ${Date.now()} \n`)
readStream.pause();
}
});
readStream.on('end', function () {
writeStream.end();
});
writeStream.on('drain', function () {
out.write(`writeStream drain;readStream.resume' ${Date.now()}\n`)
readStream.resume();
});
setTimeout(function show() {
var percent = Math.ceil((passedLength / totalSize) * 100);
var size = Math.ceil(passedLength / 1000000);
var diff = size - lastSize;
lastSize = size;
out.clearLine();
out.cursorTo(0);
out.write('已完成' + size + 'MB, ' + percent + '%, 速度:' + diff * 2 + 'MB/s');
if (passedLength < totalSize) {
setTimeout(show, 500);
} else {
var endTime = Date.now();
console.log();
console.log('共用时:' + (endTime - startTime) / 1000 + '秒。');
}
}, 500);