学习目标:
- 掌握基础的API;
- 文件流的使用和实现 (fs.createReadStream, fs.createWriteStream) 。
- nodejs 中的stream;
node 中如何读写文件:
常用api: writeFile, readFile;
let fs = require('fs');
let path = require('path');
// 读写文件
fs.writeFile(path.join(__dirname, 'test', '1.txt'), Date.now() + '\n', function () {
// console.log('ok');
})
fs.readFile(path.join(__dirname,'test', '1.txt'), function (err,data) {
// console.log(data.toString())
})
思考上述代码会存在什么问题?
在读取大文件的时候(比如4个G的视频), 如何使用readFile 将整个视频数据读到内存中,那么将会出现内存不够用的情况。(导致程序卡死)
那如何解决这个问题呢?
可以采用 文件流 (createReadStream,createWriteStream)
文件流的使用
通过文件流的形式,我们就可以操作大文件了。
var from = fs.createReadStream(path.join(__dirname, 'test', 'demo1.txt'));
var to = fs.createWriteStream(path.join(__dirname, 'test', 'demo3.txt'));
from.pipe(to);
为什么上面的代码会内存溢出呢?
上诉代码是一种语法糖,我们看看它原始的模样。(上面的代码和下面的代码是一个意思)
// 使用事件的方式操作读写流
var ws = fs.createWriteStream(path.join(__dirname, 'test', 'demo1.txt'),{
highWaterMark: 3
});
var rs = fs.createReadStream(path.join(__dirname, 'test', 'demo2.txt'),{
highWaterMark: 3
});
// 读到数据出发的事件
rs.on('data', function (data) {
var flag = ws.write(data);
if (!flag)
rs.pause();
});
// 缓冲区的文件,消耗完触发的事件
ws.on('drain', function () {
rs.resume();
});
// 数据读完了触发的事件
rs.on('end', function () {
ws.end();
});
api 功能讲解:
- 属性
- highWaterMark // 水位线
- 方法
- ws.write 将数据写到磁盘中。
- rs.pause 暂停读取磁盘的内容到内存中。
- rs.resume 继续读取磁盘的内容到内存中。
- ws.end 写入内容已经结束。
- 事件
- rs.on('data',hander) 每当从磁盘读取内容到内存中触发
- rs.on('end',hander) 磁盘内容读取完成触发。
- ws.on('drain',hander) 内存的中的数据都写入文件时触发。
原理图:
在现实生活中,如果漏斗满了,我们很容易想到先不给漏斗在添加水了。而文件流代码中的实现也如此。
原理:
一边读文件一边写文件,当读的文件在内存中占用太多时,则不在会读取内容到内存中。让内存中的内容先写入文件,写完之后在读。
根据上面的思路,我们可以根据一个fs的基础api实现如上功能:
let BUFFER_SIZE = 3;
let buf = Buffer.alloc(BUFFER_SIZE);
var readOffset = 0;
fs.open(path.join(__dirname, 'test', 'demo1.txt'), 'r', 0600, function (err, readFd) {
fs.open(path.join(__dirname, 'test', 'demo2.txt'), 'w', (err, writeFd) => {
function read() {
fs.read(readFd, buf, 0, BUFFER_SIZE, null, (err, bytesRead) => {
if (bytesRead>0){
fs.write(writeFd, buf, 0, bytesRead, read);
}else{
fs.close(readFd,()=>{})
fs.close(writeFd, () => {})
}
});
}
read()
})
});
代码功能介绍
- buf 他是一个缓冲区的概念。
- fs.open 打开文件,获取文件标识符
- fs.close 关闭文件。
- fs.read 从磁盘读取数据
- fs.write 将数据写入磁盘
通过上述代码,我们也可以实现大文件读取。
缺点:
- 代码耦合性高。
- 文件回调嵌套很深,不利于代码阅读。
node 是如何解耦的?
采用发布订阅模式
node events 模块讲解
let EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
// 只处理一次,避免无限循环。
myEmitter.on('newListener', (event, listener) => {
if (event === 'data') {
// 在前面插入一个新的监听器。
console.log(22)
}
});
myEmitter.on('open', function () {
console.log("open");
});
myEmitter.on('data', function (data) {
console.log(data);
});
myEmitter.once('aa',()=>{
console.log(1)
})
myEmitter.emit('aa');
myEmitter.emit('aa');
api讲解:
- 事件
- newListener 每次注册事件的时候出发(on)。
- once 只出发一次。
通过发布订阅实现解耦:
ReadStream.js
let fs = require("fs");
let EventEmitter = require("events");
class ReadStream extends EventEmitter {
constructor(path, options) {
super(path, options);
this.path = path;
this.fd = options.fd;
this.flags = options.flags || "r";
this.encoding = options.encoding;
this.start = options.start || 0;
this.pos = this.start;
this.end = options.end;
this.flowing = false;
this.autoClose = true;
this.highWaterMark = options.highWaterMark || 64 * 1024;
this.buffer = Buffer.alloc(this.highWaterMark);
this.length = 0;
this.on("newListener", (type, listener) => {
if (type == "data") {
this.flowing = true;
this.read();
}
});
this.on("end", () => {
if (this.autoClose) {
this.destroy();
}
});
this.open();
}
read() {
if (typeof this.fd != "number") { return this.once("open", () => this.read());}
let n = this.end ?
Math.min(this.end - this.pos, this.highWaterMark) :
this.highWaterMark;
fs.read(this.fd, this.buffer, 0, n, this.pos, (err, bytesRead) => {
if (err) { return; }
if (bytesRead) {
let data = this.buffer.slice(0, bytesRead);
data = this.encoding ? data.toString(this.encoding) : data;
this.emit("data", data);
this.pos += bytesRead;
if (this.end && this.pos > this.end) {
return this.emit("end");
}
if (this.flowing) this.read();
} else {
this.emit("end");
}
});
}
open() {
fs.open(this.path, this.flags, this.mode, (err, fd) => {
if (err) return this.emit("error", err);
this.fd = fd;
this.emit("open", fd);
});
}
end() {
if (this.autoClose) { this.destroy(); }
}
destroy() {
fs.close(this.fd, () => { this.emit("close"); });
}
}
ReadStream.prototype.pipe = function (dest) {
this.on("data", (data) => {
let flag = dest.write(data);
if (!flag) { this.pause(); }
});
dest.on("drain", () => {this.resume();});
this.on("end", () => {dest.end();});
};
ReadStream.prototype.pause = function () {
this.flowing = false;
};
ReadStream.prototype.resume = function () {
this.flowing = true;
this.read();
};
module.exports = ReadStream;
WriteStream.js
let fs = require('fs');
let EventEmitter = require('events');
class WriteStream extends EventEmitter {
constructor(path, options) {
super(path, options);
this.path = path;
this.fd = options.fd;
this.flags = options.flags || 'w';
this.mode = options.mode || 0o666;
this.encoding = options.encoding;
this.start = options.start || 0;
this.pos = this.start;
this.writing = false;
this.autoClose = true;
this.highWaterMark = options.highWaterMark || 16 * 1024;
this.buffers = [];
this.length = 0;
this.open();
}
open() {
fs.open(this.path, this.flags, this.mode, (err, fd) => {
if (err) return this.emit('error', err);
this.fd = fd;
this.emit('open', fd);
})
}
write(chunk, encoding, cb) {
if (typeof encoding == 'function') {
cb = encoding;
encoding = null;
}
chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, this.encoding || 'utf8');
let len = chunk.length;
this.length += len;
let ret = this.length < this.highWaterMark;
if (this.writing) {
this.buffers.push({
chunk,
encoding,
cb,
});
} else {
this.writing = true;
this._write(chunk, encoding, this.clearBuffer.bind(this));
}
return ret;
}
_write(chunk, encoding, cb) {
if (typeof this.fd != 'number') {
return this.once('open', () => this._write(chunk, encoding, cb));
}
fs.write(this.fd, chunk, 0, chunk.length, this.pos, (err, written) => {
if (err) {
if (this.autoClose) {this.destroy();}
return this.emit('error', err);
}
this.length -= written;
this.pos += written;
cb && cb();
});
}
clearBuffer() {
let data = this.buffers.shift();
if (data) {
this._write(data.chunk, data.encoding, this.clearBuffer.bind(this))
} else {
this.writing = false;
this.emit('drain');
}
}
end() {
if (this.autoClose) {
this.emit('end');
this.destroy();
}
}
destroy() {
fs.close(this.fd, () => {this.emit('close');})
}
}
module.exports = WriteStream;
上述就是通过发布订阅模式对于文件流的简单实现。
注意:理解了上述思路和代码,当你去调试node fs 文件流的源码时,还是会发现自己看不懂,因为他是基于node steam 流实现的。 **
node 中的stream(流)
Node.js 中有四种基本的流类型:
Writable- 可写入数据的流(例如fs.createWriteStream())。Readable- 可读取数据的流(例如fs.createReadStream())。Duplex- 可读又可写的流(例如net.Socket)。Transform- 在读写过程中可以修改或转换数据的Duplex流(例如zlib.createDeflate())。
此外,该模块还包括实用函数 stream.pipeline()、stream.finished() 和 stream.Readable.from()。
继承四种基本流需要实现的方法:
node stream
实现一个自己的可读流,可写流
var stream = require('stream');
var util = require('util');
util.inherits(MyReadable, stream.Readable);
// 可读流
function MyReadable(options) {
stream.Readable.call(this, options);
this._index = 0;
this.tem = 0;
}
MyReadable.prototype._read = function () {
this.push('黄鹏a');
this.push(null);
};
var myReadable = new MyReadable({
highWaterMark: 2
});
// 可写流
util.inherits(MyWriter, stream.Writable);
function MyWriter(opt) {
stream.Writable.call(this, opt);
}
MyWriter.prototype._write = function (chunk, encoding, callback) {
stock.push(chunk.toString('utf8'));
callback();
};
var myWriter = new MyWriter();
let stock = [];
通过steam模拟文件流
function eventMethods() {
myReadable.on('data', function (data) {
var flag = myWriter.write(data);
if (!flag)
myReadable.pause();
});
myReadable.on('end', function (data) {
// console.log("读完了");
myWriter.end();
});
// 缓冲区的文件,消耗完触发的事件
myWriter.on('drain', function () {
myReadable.resume();
});
myWriter.on('finish', () => {
console.log(stock)
})
}
pipe 方式
function pipeMethods() {
myReadable.pipe(myWriter)
myWriter.on('finish', () => {
console.log(stock, 'finish')
})
}
转换流的使用(将字符串转换成base64)
function tansformMethods() {
const upperCase = new stream.Transform({
transform(chunk, encoding, callback) {
this.push(chunk.toString('base64'));
callback();
}
});
myReadable.pipe(upperCase).pipe(myWriter);
myWriter.on('finish', (data) => {
console.log(stock)
})
}
扩展与思考
var http=require("http");
var fs=require("fs");
var server= http.createServer(function(req,res){
// 告诉浏览器发送什么样的资源
res.writeHead(200,{"Content-Type":'application/html'});
fs.createReadStream('/index.html').pipe(res);
});
上述代码,就是用node启动一个http服务器,据观察api发现,其实req,res 也是一种stream , 以后在发开时,使用流的思维想res,req ,很多问题就可以解决了。