在 Node.js 中,读取文件的方式有两种,一种是用 fs.readFile,另外一种是利用 fs.createReadStream 来读取。
fs.readFile 对于每个 Node.js 使用者来说最熟悉不过了,简单易懂,很好上手。但它的缺点是会先将数据全部读入内存,一旦遇到大文件的时候,这种方式读取的效率就非常低下了。
fs模块中几种文件读写方式的区别
用途 | 使用异步方式 | 使用同步方式 |
---|---|---|
将文件完整读入缓存区 | readFile | readFileSync |
将文件部分读入缓存区 | read | readSync |
将数据完整写入文件 | writeFile | writeFileSync |
将缓存区中的部分内容写入文件 | write | writeSync |
- 使用readFile或者readFileSync方法读取文件内容时,Nodejs首先将文件内容完整地读入缓存区,再从缓存区读取内容
- 使用writeFile方法或writeFileSync方法写入文件内容时,Nodejs首先将文件内容完整地读入缓存区,然后一次性将缓存区内容写入文件中
也就是说readFile方法或readFileSync方法读取文件内容或者使用writeFIle或writeFileSync方法写入文件内容时,nodejs将该文件视为一个整体,为其分配缓存区并且一次性将文件内容读取到缓存区中,在这个期间,nodejs将不能执行任何其他操作。
而 fs.createReadStream 则是通过 Stream 来读取数据,它会把文件(数据)分割成小块,然后触发一些特定的事件,我们可以监听这些事件,编写特定的处理函数。这种方式相对上面来说,并不好上手,但它效率非常高。
事实上, Stream 在 Node.js 中并非仅仅用在文件处理上,其他地方也可以看到它的身影,如 process.stdin/stdout, http, tcp sockets, zlib, crypto 等都有用到。
特点
- 基于事件通讯
- 可以通过 pipe 来连接流
种类
- Readable Stream 可读数据流
- Writeable Stream 可写数据流
- Duplex Stream 双向数据流,可以同时读和写
- Transform Stream 转换数据流,可读可写,同时可以转换(处理)数据
对象模式
通过 Node API 创建的流, 只能够对字符串或者 buffer 对象进行操作. 但其实流的实现是可以基于其他的 Javascript 类型(除了 null, 它在流中有特殊的含义)的. 这样的流就处在 "对象模式(objectMode)" 中. 在创建流对象的时候, 可以通过提供 objectMode 参数来生成对象模式的流. 试图将现有的流转换为对象模式是不安全的.
缓冲区
Node.js 中 stream 的缓冲区, 以开头的 C语言 拷贝文件的代码为模板讨论, (抛开异步的区别看) 则是从 src 中读出数据到 buf 中后, 并没有直接写入 dest 中, 而是先放在一个比较大的缓冲区中, 等待写入(消费) dest 中. 即, 在缓冲区的帮助下可以使读与写的过程分离.
Readable 和 Writable 流都会将数据储存在内部的缓冲区中. 缓冲区可以分别通过 writable._writableState.getBuffer() 和 readable._readableState.buffer 来访问. 缓冲区的大小, 由构造 stream 时候的 highWaterMark 标志指定可容纳的 byte 大小, 对于 objectMode 的 stream, 该标志表示可以容纳的对象个数.
可读流
当一个可读实例调用 stream.push() 方法的时候, 数据将会被推入缓冲区. 如果数据没有被消费, 即调用 stream.read() 方法读取的话, 那么数据会一直留在缓冲队列中. 当缓冲区中的数据到达 highWaterMark 指定的阈值, 可读流将停止从底层汲取数据, 直到当前缓冲的报备成功消耗为止.
可写流
在一个在可写实例上不停地调用 writable.write(chunk) 的时候数据会被写入可写流的缓冲区. 如果当前缓冲区的缓冲的数据量低于 highWaterMark 设定的值, 调用 writable.write() 方法会返回 true (表示数据已经写入缓冲区), 否则当缓冲的数据量达到了阈值, 数据无法写入缓冲区 write 方法会返回 false, 直到 drain 事件触发之后才能继续调用 write 写入.
// Write the data to the supplied writable stream one million times.
// Be attentive to back-pressure.
function writeOneMillionTimes(writer, data, encoding, callback) {
let i = 1000000;
write();
function write() {
var ok = true;
do {
i--;
if (i === 0) {
// last time!
writer.write(data, encoding, callback);
} else {
// see if we should continue, or wait
// don't pass the callback, because we're not done yet.
ok = writer.write(data, encoding);
}
} while (i > 0 && ok);
if (i > 0) {
// had to stop early!
// write some more once it drains
writer.once('drain', write);
}
}
}
Duplex 与 Transform
Duplex 流和 Transform 流都是同时可读写的, 他们会在内部维持两个缓冲区, 分别对应读取和写入, 这样就可以允许两边同时独立操作, 维持高效的数据流. 比如说 net.Socket 是一个 Duplex 流, Readable 端允许从 socket 获取、消耗数据, Writable 端允许向 socket 写入数据. 数据写入的速度很有可能与消耗的速度有差距, 所以两端可以独立操作和缓冲是很重要的.
pipe
stream 的 .pipe()
, 将一个可写流附到可读流上, 同时将可写流切换到流模式, 并把所有数据推给可写流. 在 pipe
传递数据的过程中, objectMode
是传递引用, 非 objectMode
则是拷贝一份数据传递下去.
pipe 方法最主要的目的就是将数据的流动缓冲到一个可接受的水平, 不让不同速度的数据源之间的差异导致内存被占满. 关于 pipe 的实现参见 David Cai 的 通过源码解析 Node.js 中导流(pipe)的实现
以下是一个关于导流的简单例子:
'use strict'
import {createReadStream, createWriteStream} from 'fs'
createReadStream('/path/to/a/big/file').pipe(createWriteStream('/path/to/the/dest'))
我们可以把pipe方法的主要功能分解为:
- 不断从来源可读流中获得一个指定长度的数据。
- 将获取到的数据写入目标可写流。
- 平衡读取和写入速度,防止读取速度大大超过写入速度时,出现大量滞留数据。
事件
可读数据流的事件
- readable 数据向外流时触发
- data 对于那些没有显式暂停的数据流,添加data事件监听函数,会将数据流切换到流动态,尽快向外提供数据
- end 读取完数据时触发。注意不能和 writeableStream.end() 混淆,writeableStream 并没有 end 事件,只有 .end() 方法
- close 数据源关闭时触发
- error 读取数据发生错误时触发
可写数据流的事件
- drain writable.write(chunk) 返回 false 之后,缓存全部写入完成,可以重新写入时就会触发
- finish 调用 .end 方法时,所有缓存的数据释放后触发,类似于可读数据流中的 end 事件,表示写入过程结束
- pipe 作为 pipe 目标时触发
- unpipe 作为 unpipe 目标时触发
- error 写入数据发生错误时触发
复制视频代码
var fs = require('fs')
var readStream = fs.createReadStream('264.mp4')
var writeStream = fs.createWriteStream('The.Big.Bang.Theory.S10E07.mp4')
readStream.on('data',function(chunk) {
if (writeStream.write(chunk) === false){
console.log('still cached');
readStream.pause()
}
})
readStream.on('end',function(chunk) {
writeStream.end()
})
writeStream.on('drain',function() {
console.log('data drains');
readStream.resume()
})
var http =require('http')
var fs = require('fs')
http
.createServer(function (req,res) {
// fs.readFile('./buffer/logo.png',function (err,res) {
// if (err) {
// res.end('file not exist')
// }
// else {
// res.writeHeader(200,{'Context-Type':'text/html'})
// res.end(data)
// }
// })
fs.createReadStream('../buffer/logo.png').pipe(res)
})
.listen(8090)
线上爬取得图片可以这样:
var http =require('http')
var fs = require('fs')
var request = require('request')
http
.createServer(function (req,res) {
// fs.readFile('./buffer/logo.png',function (err,res) {
// if (err) {
// res.end('file not exist')
// }
// else {
// res.writeHeader(200,{'Context-Type':'text/html'})
// res.end(data)
// }
// })
//fs.createReadStream('../buffer/logo.png').pipe(res)
request('https://www.baidu.com/img/bd_logo1.png').pipe(res)
})
.listen(8090)
然后安装request
这个模块
npm install request
有了pipe(),我们可以重构下上面复制视频
的代码
var fs = require('fs')
fs.createReadStream('264.mp4').pipe(fs.createWriteStream('The.Big.Bang.Theory.S10E07.mp4'))
总结:
- 可读流是负责读取web的数据,把数据缓存到内部的
Buffer
数组 - 可写流负责消费数据,从可读流获取到数据,然后对得到的
chunk
数据块处理,至于怎么处理,取决于可写流内的write()方法
举例说明下 :新建 stream_read_write.js
var Readable = require('stream').Readable
var Writable = require('stream').Writable
var readStream = new Readable()
var writeStream = new Writable()
readStream.push('I')
readStream.push('Love')
readStream.push('jxdxsw.com\n')
readStream.push(null)
writeStream._write = function (chunk,encode,callback) {
console.log(chunk.toString());
callback()
}
readStream.pipe(writeStream)
var stream = require('stream')
var util = require('util')
function ReadStream() {
stream.Readable.call(this)
}
util.inherits(ReadStream,stream.Readable)
ReadStream.prototype._read = function() {
this.push('I')
this.push('Love')
this.push('jxdxsw.com\n')
this.push(null)
}
function WriteStream() {
stream.Writable.call(this)
this._cached = new Buffer('')
}
util.inherits(WriteStream,stream.Writable)
WriteStream.prototype._write = function(chunk,encode,callback) {
console.log(chunk.toString());
callback()
}
function TransformStream() {
stream.Transform.call(this)
}
util.inherits(TransformStream,stream.Transform)
TransformStream.prototype._transform = function(chunk,encode,callback) {
this.push(chunk)
callback()
}
TransformStream.prototype._flush = function(callback) {
this.push('Oh my God!')
callback()
}
var rs = new ReadStream()
var ws = new WriteStream()
var ts = new TransformStream
rs.pipe(ts).pipe(ws)
拓展
module.exports = (stream, throwError) => {
return new Promise((resolve, reject) => {
if (stream._readableState && stream._readableState.ended) {
return resolve();
}
if (!stream.readable || stream.destroyed) {
return resolve();
}
stream.resume();
function cleanup() {
stream.removeListener('end', onEnd);
stream.removeListener('close', onEnd);
stream.removeListener('close', onError);
}
function onEnd() {
cleanup();
resolve();
}
function onError(err) {
cleanup();
// don't throw error by default
if (throwError) {
reject(err);
} else {
resolve();
}
}
stream.on('end', onEnd);
stream.on('close', onEnd);
stream.on('error', onError);
});
};
上面代码是包stream-wormhole实现
更多使用可以看这里 egg-multipart
常见npm模块
var through = require('through');
var tr = through(write, end);
function write (buf) { this.queue(buf.toString().toUpperCase()) }
function end () {}
process.stdin.pipe(tr).pipe(process.stdout);
参考
Node.js: fs.readFile/writeFile 和 fs.createReadStream/writeStream 区别
Stream API 的使用
进击Node.js基础(二)
https://github.com/ElemeFE/no...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。