说明:文章中的代码运行环境是Nodejs v12
Stream是什么?
了解Stream前需要知道流。
流是用于Node.js中处理数据的抽象接口,流可以是可读的、可写的、或者两个都可以的。所有的流都是EventEmitter的实例。
Node.js提供了许多流对象,HTTP Request、HTTP Response、process.stdout都是流的实例。
Stream模块提供了实现流接口的API。
github.com/nodejs/node…
流使用案例—文件下载
使用文件读取方式响应
const http = require('http');
const fs = require('fs');
const path = require('path');
function showMemory() {
const format = (bytes) => {
return (bytes / 1024 / 1024).toFixed(2) + ' MB';
};
let mem = process.memoryUsage();
let str = 'Process memory \n ';
Object.keys(mem).forEach(key => {
str += key + ':' + format(mem[key])+ ' ';
});
console.log(str);
}
http.createServer((req, res) => {
const filename = 'read.txt';// 6.4MB
const filepath = path.resolve(__dirname, filename);
console.log('filepath: ' + filepath);
showMemory();
console.time('file');
res.setHeader("Content-Type", "application/octet-stream");
res.setHeader("Content-Disposition", "attachment; filename="+encodeURI(filename));
fs.readFile(filepath, (_, data) => {
res.write(data);
res.end();
console.timeEnd('file');
console.log('**** response end **** \n');
showMemory();
});
}).listen(3000);
进程使用的内存,明显增加,增量和文件大小接近
使用stream下载文件
http.createServer((req, res) => {
const filename = 'read.txt';// 6.4MB
const filepath = path.resolve(__dirname, filename);
console.log('filepath: ' + filepath);
showMemory();
console.time('file');
// 读取
const readerStream = fs.createReadStream(filepath);
readerStream.pipe(res);
res.setHeader("Content-Type", "application/octet-stream");
res.setHeader("Content-Disposition", "attachment; filename="+encodeURI(filename));
console.log('**** response end **** \n');
console.timeEnd('file');
showMemory();
}).listen(3000);
进程内存变化比较小,响应时间也更短
流类型
Node.js有四种基本的流类型:
- 可读流(
stream.Readable)
可以从中读取数据的流,如fs.createWriteStream() - 可写流(
stream.Writeable)
可以写入数据的流,如fs.createReadStream() - 双向流(
stream.Duplex)
可以写、读,如net.Socket - 转换流(
stream.Transform)
基于双向流,在读或写的时候用来更改或转换数据,如zlib.createDeflate()
流消费者的API
所有的流都是EventEmitter的实例,可以通过on来监听事件。
可读流、可写流的事件和函数较多,这里只列出了一些重要的,流的属性这里未说明,可去官方文档查看详细完整内容。
可读流
Events
- data 流将数据的所有权移交给消费者时触发。
- end 流中没有更多数据供消费者使用时触发
- pause/resume 暂停/恢复
- close 当流及其底层资源(如文件描述符)都已关闭,则触发close事件。该事件表明不再触发更多事件,并且不会发生进一步计算。
- error
- readable
Methods
- setEncoding(string)
- pause/resume()
- pipe/unpipe(WriteableStream)
- read([size])
demo
const fs = require('fs');
// 打开一个流:
const rs = fs.createReadStream('small.txt');
const chunks = [];
let size = 0;
// 流传给消费者一个数据块的时候触发
rs.on('data', function (chunk) {
console.log('DATA:')
chunks.push(chunk);
size = size + chunk.byteLength;
});
// 流中没有可以消费的数据时触发
rs.on('end', function () {
console.log('END, file content is: ');
const buffer = Buffer.concat(chunks, size);
console.log(buffer.toString('utf8'));
});
rs.on('error', function (err) {
console.log('ERROR: ' + err);
});
fs.createReadStream默认的hightwaterMark是64kb(stream 的Readable是16kb),所以这里的data只触发了一次
改变hightWaterMark的值,使data可以多次触发。(图中注释有误, 应该是10byte)
两种模式
可读流有两种模式,会在读取数据过程中进行转换
const fs = require('fs');
// 打开一个流,highWaterMark每次读取100 byte, small.txt有701 byte
const rs = fs.createReadStream('small.txt', {highWaterMark: 100});
rs.setEncoding('utf8');
let count = 0;
// 流传给消费者一个数据块的时候触发
rs.on('data', function (chunk) {
count++;
console.log('触发了data事件:count = ' + count);
if(count === 5) {
rs.pause();
console.log('\n调用pause,转为暂停模式');
console.log('stream readableFlowing: ', rs.readableFlowing);
console.log('stream isPaused: ', rs.isPaused());
setTimeout(() => {
console.log('\n1秒后,调用resume方法,转为流动模式');
rs.resume();
}, 1000);
}
});
// 流中没有可以消费的数据时触发
rs.on('end', function () {
console.log('END');
});
rs.on('resume', function () {
console.log('触发了resume事件');
});
可写流
Events
- drain 如果stream.write(chunk)返回false,drain事件将在适合继续写入时触发
- finish 调用stream.end(),并且所有数据都刷新到底层系统后,则触发finish事件
- pipe 可读流上调用stream.pipe(wirteStream),将此可写流添加到目标集时,触发pipe事件
- unpipe 可读流上调用stream.unpipe(wirteStream),将此可写流移出目标集时,触发unpipe事件
- close
- error
Methods
- setDefaultEncoding(encoding)
- write(chunk[,encoding][,callback])
如果内部缓冲区小于后创建流时配置的highWaterMark,则返回值为 true。
如果返回 false,则应停止继续将数据写入流,直到发出 'drain' 事件 - end(chunk[,encoding][,callback])
不再有数据写入可写流
demo
const fs = require('fs');
const ws = fs.createWriteStream('write.txt', {
highWaterMark: 4
});
ws.setDefaultEncoding('utf8');
console.log(ws.write('立'));
console.log('缓冲区中写入的字节数', ws.writableLength);
console.log(ws.write('夏前后 '));
ws.on('drain', () => {
console.log('\n触发了drain事件');
console.log('缓冲区中写入的字节数', ws.writableLength);
console.log(ws.write('种瓜点豆'));
ws.end();
});
ws.on('close', () => {
console.log('\n触发了close事件');
});
ws.on('finish', () => {
console.log('\n触发了finish事件');
});
双工流
双工流,同时实现了可读流和可写流的API。注意双工流的可读流和可写流是相互独立的。
client.js
const net = require('net');
const client = net.connect({port: 3000, host: '127.0.0.1'}, function() {
console.log('Client: 与服务器端连接完成');
// 可写流的API
client.write('hello Server');
});
const chunks = [];
let size = 0;
// 可读流的API
client.on('data', function(chunk) {
chunks.push(chunk);
size = size + chunk.byteLength;
});
client.on('end', function() {
console.log('\nClient: 触发了end事件');
const buffer = Buffer.concat(chunks, size);
console.log('Client: 收到服务器端数据,内容为{'+ buffer.toString('utf8') +'}');
});
client.on('close', function() {
console.log('\nClient: 触发了close事件');
});
client即是双工流,可写流用于向服务器端发送数据,可读流用户接收服务器端的响应,二者互不干扰。下面的server对象同理。
server.js
const net = require('net');
// tcp服务端
const server = net.createServer(function(socket){
console.log('Server: 收到来自客户端的请求');
socket.on('data', function(data) {
// 给客户端返回数据
console.log('\nServer: 收到客户端数据,内容为{'+ data +'}');
socket.write('hello Client');
socket.end();
});
socket.on('end', function(){
console.log('\nServer: 触发了end事件');
});
socket.on('close', function(){
console.log('\nServer: 触发了close事件');
});
});
server.listen(3000, '127.0.0.1', function(){
console.log('Server: listen 3000');
});
转换流
转换流也是双工流,它的区别是转换流中可读流和可写流有关联。
对数据先执行写入流,处理后通过可读流将数据响应出来。
常见的转换流有zlib, crypto
const crypto = require('crypto');
const md5 = crypto.createHash('md5');
console.log('md5 is transform stream: ' + isTransformStream(md5));
const message = 'hello';
const digest = md5.update(message, 'utf8').digest('hex');
console.log('\ndigest: ' + digest);
function isTransformStream (stream) {
const isStream = stream !== null &&
typeof stream === 'object' &&
typeof stream.pipe === 'function';
if(isStream) {
return typeof stream._transform === 'function'
&& typeof stream._transformState === 'object';
}
return false;
}