Node.js 流操作
1. 概述
Node.js 中的流(Stream)是处理流式数据的核心机制,它允许应用程序以分块方式处理数据,而非一次性加载全部数据到内存。这种机制在处理大文件、网络I/O或实时数据流时尤为重要,能显著降低内存占用并提升性能。
2. 流的类型
Node.js 提供了四种基本的流类型,它们都是 EventEmitter 的实例,通过事件进行通信:
| 类型 | 特点 | 常见示例 |
|---|---|---|
| 可读流 (Readable) | 数据来源的抽象,用于读取数据 | fs.createReadStream, http.IncomingMessage |
| 可写流 (Writable) | 数据目的地点的抽象,用于写入数据 | fs.createWriteStream, http.ServerResponse |
| 双工流 (Duplex) | 同时可读可写 | net.Socket (TCP套接字) |
| 转换流 (Transform) | 特殊的双工流,可在读写过程中转换数据 | zlib.createGzip(), zlib.createGunzip() |
3. 流的基本操作
3.1 可读流操作
创建可读流:
js
// read.txt
我有一壶酒,足以慰风尘!
javascript
const fs = require('fs')
// 可读流 readable
const readStream = fs.createReadStream('read.txt', {
highWaterMark: 3 // 每次读取3个字节
})
readStream.on('data', chunk => {
console.log(chunk.toString())
/*
我
有
一
壶
酒
,
足
以
慰
风
尘
!
*/
})
readStream.on('end', () => {
console.log('读取完毕')
})
readStream.on('error', err => {
console.error('读取出错', err)
})
注意:
当可读流设置highWaterMark时,使用toString方法的时候,会出现截断数据的情况会出现乱码的情况,比如highWaterMark = 5,或者里面有英文的时候,会出现
"我�
�一�
��酒
,�
�以�
��风
尘�
�"
因为常用中文是三个字节,如果全中文不是3的倍数会出现截断中文的问题,或者英文只有一个字节,也会出现中文被截断,如果要tostring方法需要使用new StringDecoder().write() 方法
需要了解ASCII(0xxxxxxx) utf8格式 (110xxxxx 1110xxxx 1111xxxx -> 10xxxxxx )标准
🔍 背压问题:
- readStream.on('data') 方法会出现背压问题,判断writable.write(chunk) === false
- writeable可写流缓冲区清空,恢复可读流会触发
drain,- highWaterMark 是软限制,不是硬上限。Node.js 允许略微超过,但一旦超过就会触发背压。
- 即使底层还没真正"满",只要缓冲区接近阈值,就会返回 false 以提前控制流量。
3.2 可写流操作
创建可写流:
javascript
const fs = require('fs');
const writeStream = fs.createWriteStream('write.txt', {
highWaterMark: 3 // 每次写入3个字节
})
writeStream.write('我有一壶酒,足以慰风尘!', 'utf8')
// 写入完成事件
writeStream.on('finish', () => {
console.log('写入完成');
});
writeStream.end(() => {
console.log('写入完毕')
})
4. 流的高级操作
4.1 管道操作 (Pipe)
管道是将一个可读流连接到一个可写流的机制,用于数据的传输。这是最常用的流操作方式。
文件复制示例:
javascript
const fs = require('fs')
const readStream = fs.createReadStream('read.txt', {
highWaterMark: 3, // 每次读取3个字节
encode: 'utf8'
})
const writeStream = fs.createWriteStream('write.txt', {
highWaterMark: 6 // 每次写入6个字节
})
// 管道流
readStream.pipe(writeStream)
readStream.on('error', (err) => {
console.error('读取出错', err)
})
writeStream.on('error', (err) => {
console.error('写入出错', err)
})
readStream.on('data', (chunk) => {
console.log('读取数据块:', chunk);
})
readStream.on('end', () => {
console.log('读取结束');
})
writeStream.on('finish', () => {
console.log('写入成功');
})
注意
ReadStream 默认highWaterMark为64KB
WriteStream 默认highWaterMark为16KB
系统读写速度是读的速度大于写的速度,
设置的时候highWaterMark注意一下
4.2 链式流 (Chaining)
链式流通过连接多个流来创建数据处理流水线,常用于压缩、解压缩等场景。
文件压缩示例:
javascript
const fs = require('fs');
const zlib = require('zlib');
// 读取文件并压缩后写入新文件
fs.createReadStream('large-file.txt')
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream('large-file.txt.gz'));
console.log('文件压缩中...');
4.3 转换流 (Transform)
转换流在数据流经时进行转换处理,常用于数据处理、压缩、加密等场景。
自定义转换流示例:
javascript
const fs = require('fs')
const Transform = require('stream').Transform
const readStream = fs.createReadStream('read.txt', {
highWaterMark: 3, // 每次读取3个字节
encode: 'utf8'
})
const transformStream = new Transform({
transform(chunk, encoding, callback) {
const transformedChunk = chunk.toString() + '|';// 每个数据块后面加上 '|'
// 也就可以使用this.push 方法追加
// 如:
// this.push(transformedChunk);
// callback()
callback(null, transformedChunk);
}
});
readStream.pipe(transformStream).pipe(process.stdout);
// 我|有|一|壶|酒|,|足|以|慰|风|尘|!|
5. 实际应用场景
5.1 大文件处理
使用流处理大文件,避免内存溢出:
javascript
const fs = require('fs');
const readStream = fs.createReadStream('big-file.log');
const writeStream = fs.createWriteStream('processed.log');
// 过滤日志行
readStream.pipe(new Transform({
transform(chunk, encoding, callback) {
const lines = chunk.toString().split('\n');
const filteredLines = lines.filter(line => line.includes('ERROR'));
callback(null, filteredLines.join('\n') + '\n');
}
}))
.pipe(writeStream);
writeStream.on('finish', () => {
console.log('日志处理完成');
});
5.2 网络数据流处理
处理HTTP请求和响应的流式数据:
javascript
const http = require('http');
http.createServer((req, res) => {
// 从请求流中读取数据
req.on('data', (chunk) => {
console.log('接收数据:', chunk.toString());
});
// 处理完成后发送响应
req.on('end', () => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('请求已处理');
});
}).listen(3000);
5.3 可读可写流Duplex
js
const { Duplex } = require('stream');
class EchoDuplex extends Duplex {
constructor(options) {
super(options);
this._buffer = []; // 用于暂存写入的数据
}
// 可写端:处理写入的数据
_write(chunk, encoding, callback) {
// 将写入的数据推入缓冲区,并触发可读端的读取
this._buffer.push(chunk);
// 模拟异步处理
setImmediate(() => {
// 将数据推送到可读端
while (this._buffer.length > 0) {
const data = this._buffer.shift();
if (!this.push(data)) {
// 如果 push 返回 false,说明可读端背压,暂停写入
break;
}
}
callback(); // 告知写入完成
});
}
// 可读端:当消费者调用 read() 时触发
_read(size) {
// 此处无需主动推送,因为数据已在 _write 中通过 this.push() 推送
// 如果有更多数据待发送,可在此继续 push
}
// 可选:处理 end()
_final(callback) {
// 所有写入完成后调用
callback();
}
}
// 使用示例
const echoStream = new EchoDuplex();
// 监听可读事件
echoStream.on('data', (chunk) => {
console.log('Echo received:', chunk.toString());
});
// 写入数据
echoStream.write('Hello');
echoStream.write(' World!');
echoStream.end(); // 结束写入
6. 流操作最佳实践
- 始终处理错误事件:流操作中,错误处理是关键,避免程序崩溃
- 使用pipe()替代手动事件监听 :
pipe()方法能自动处理数据传输,代码更简洁 - 注意背压处理:当写入速度慢于读取速度时,流会自动暂停,确保不会内存溢出
- 合理设置编码 :使用
encoding: 'utf8'避免二进制数据处理问题 - 避免在data事件中进行复杂计算:可能导致数据处理不及时,影响性能
7. 总结
Node.js 流是处理大量数据的核心机制,通过四种基本类型(可读、可写、双工、转换)和管道操作,可以高效地处理文件、网络数据等场景。使用流能显著降低内存占用,提高应用程序的性能和响应能力,是构建高性能Node.js应用的必备技能。
掌握流操作后,开发者可以轻松处理GB级文件、实现高效的数据转换流水线,并构建响应迅速的网络应用。