认识node核心模块--从Buffer、Stream到fs

[原文地址][1]在个人博客javascript

node中的Buffer和Stream会给刚接触Node的前端工程师们带来困惑,缘由是前端并无相似概念(or 有咱们也没意识到)。然而,在后端,在node中,Buffer和Stream到处体现。Buffer是缓冲区的意思,Stream是流的意思。在计算机中,缓冲区是存储中间变量,方便CPU读取数据的一块存储区域;流是类比水流形容数据的流动。Buffer和Stream通常都是字节级操做。本文将介绍这两个模块的具体细节后再介绍文件模块,以让读者有更清晰的认识。html

正文

二进制缓冲区Buffer

在前端,咱们只需作字符串级别的操做,不多接触字节、进制等底层操做,一方面这足以知足平常需求,另外一方面Javascript这种应用层语言并非干这个的;然而在后端,处理文件、网络协议、图片、视频等时是很是常见的,尤为像文件、网络流等操做处理的都是二进制数据。为了让javascript可以处理二进制数据,node封装了一个Buffer类,主要用于操做字节,处理二进制数据。前端

// 建立一个长度为 十、且用 30 填充的 Buffer。
const buf1 = Buffer.alloc(10, 30)
console.log(buf1)// <Buffer 1e 1e 1e 1e 1e 1e 1e 1e 1e 1e>
// 字符串转Buffer
const buf2 = Buffer.from('javascript')
console.log(buf2)// <Buffer 6a 61 76 61 73 63 72 69 70 74>
// 字符串转 buffer
console.log(buf2.toString())// javascript
console.log(buf2.toString('hex')) //6a617661736372697074复制代码

一个 Buffer 相似于一个整数数组,能够取下标,有length属性,有剪切复制操做等,不少API也相似数组,但Buffer的大小在被建立时肯定,且没法调整。Buffer处理的是字节,两位十六进制,所以在整数范围就是0~255。java

能够看到,Buffer能够与string互相转化,还能够设置字符集编码。Buffer用来处理文件I/O、网络I/O传输的二进制数据,string用来呈现。在处理文件I/O、网络I/O传输的二进制数据时,应该尽可能以Buffer形式直接传输,速度会获得很好的提高,但操做字符串比操做Buffer仍是快不少的。node

Buffer内存分配与性能优化webpack

Buffer是一个典型的javascript与C++结合的模块,与性能有关的用C++来实现,javascript 负责衔接和提供接口。Buffer所占的内存不是V8分配的,是独立于V8堆内存以外的内存,经过C++层面实现内存申请、javascript 分配内存。值得一提的是,每当咱们使用Buffer.alloc(size)请求一个Buffer内存时,Buffer会以8KB为界限来判断分配的是大对象仍是小对象,小对象存入剩余内存池,不够再申请一个8KB的内存池;大对象直接采用C++层面申请的内存。所以,对于一个大尺寸对象,申请一个大内存比申请众多小内存池快不少。git

流Stream

前面讲到,流类比水流形容数据的流动,在文件I/O、网络I/O中数据的传输均可以称之为流,流是能统一描述全部常见输入输出类型的模型,是顺序读写字节序列的抽象表示。数据从A端流向B端与从B端流向A端是不同的,所以,流是有方向的。A端输入数据到B端,对B就是输入流,获得的对象就是可读流;对A就是输出端、获得的对象是可写流。有的流便可以读又能够写,如TCP链接,Socket链接等,称为读写流(Duplex)。还有一种在读写过程当中能够修改和变换数据的读写流称为Transform流。github

在node中,这些流中的数据就是Buffer对象,可读、可写流会将数据存储到内部的缓存中,等待被消费;DuplexTransform 则是都维护了两个相互独立的缓存用于读和写。 在维持了合理高效的数据流的同时,也使得对于读和写能够独立进行而互不影响。web

在node中,这四种流都是EventEmitter的实例,它们都有close、error事件,可读流具备监听数据到来的data事件等,可写流则具备监听数据已传给低层系统的finish事件等,DuplexTransform 都同时实现了 ReadableWritable 的事件和接口 。gulp

值得一提的是writable的drain事件,这个事件表示缓存的数据被排空了。为何有这个事件呢?原由是调用可写流的write和可读流的read都会有一个缓存区用来缓存写/读的数据,缓存区是有大小的,一旦写的内容超过这个大小,write方法就会返回false,表示写入中止,这时若是继续read完缓存区数据,缓存区被排空,就会触发drain事件,能够这样来防止缓存区爆仓:

var rs = fs.createReadStream(src);
var ws = fs.createWriteStream(dst);

rs.on('data', function (chunk) {
    if (ws.write(chunk) === false) {
        rs.pause();
    }
});

rs.on('end', function () {
    ws.end();
});

ws.on('drain', function () {
    rs.resume();
});复制代码

一些常见流分类:

  • 可写流:HTTP requests, on the client、HTTP responses, on the server、fs write streams、zlib streams、crypto streams、TCP sockets、child process stdin、process.stdout, process.stderr
  • 可读流:HTTP responses, on the client、HTTP requests, on the server、fs read streams、zlib streams、crypto streams、TCP sockets、child process stdout and stderr、process.stdin
  • 可读可写流:TCP sockets、zlib streams、crypto streams
  • 变换流:zlib streams、crypto streams

另外,提到流就不得不提到管道的概念,这个概念也很是形象:水流从一端到另外一端流动须要管道做为通道或媒介。流也是这样,数据在端之间的传送也须要管道,在node中是这样的:

// 将 readable 中的全部数据经过管道传递给名为 file.txt 的文件
const readable = getReadableStreamSomehow();
const writable = getWritableStreamSomehow('file.txt');
// readable 中的全部数据都传给了 'file.txt'
readable.pipe(writable);

// 对流进行链式地管道操做
const r = fs.createReadStream('file.txt');
const z = zlib.createGzip();
const w = fs.createWriteStream('file.txt.gz');
r.pipe(z).pipe(w);复制代码

注意,只有可读流才具备pipe能力,可写流做为目的地。

pipe不只能够做为通道,还能很好的控制管道里的流,控制读和写的平衡,不让任一方过分操做。另外,pipe能够监听可读流的data、end事件,这样就能够构建快速的响应:

// 一个文件下载的例子,使用回调函数的话须要等到服务器读取完文件才能向浏览器发送数据
var http = require('http') ;
var fs = require('fs') ;
var server = http.createServer(function (req, res) {
    fs.readFile(__dirname + '/data.txt', function (err, data) {
        res.end(data);
    }) ;
}) ;
server.listen(8888) ;

// 而采用流的方式,只要创建链接,就会接受到数据,不用等到服务器缓存完data.txt
var http = require('http') 
var fs = require('fs') 
var server = http.createServer(function (req, res) {
    var stream = fs.createReadStream(__dirname + '/data.txt') 
    stream.pipe(res) 
}) 
server.listen(8888)复制代码

所以,使用pipe便可解决上面那个爆仓问题。

fs文件模块

fs文件模块是高阶模块,继承了EventEmitter、stream、path等底层模块,提供了对文件的操做,包括文件的读取、写入、改名、删除、遍历目录、连接POSIX文件系统等操做。与node设计思想和其余模块不一样的是,fs模块中的全部操做都提供了异步和同步两个版本。fs模块主要由下面几部分组成:

  • 对底层POSIX文件系统的封装,对应于操做系统的原生文件操做
  • 继承Stream的文件流 fs.createReadStream和fs.createWriteStream
  • 同步文件操做方法,如fs.readFileSync、fs.writeFileSync
  • 异步文件操做方法, fs.readFile和fs.writeFile

模块API架构以下:

fs主要操做
fs主要操做

读写操做:

const fs = require('fs'); // 引入fs模块
/* 读文件 */

// 使用流
const read = fs.createReadStream('sam.js',{encoding:'utf8'});
read.on('data',(str)=>{
    console.log(str);
})
// 使用readFile
fs.readFile('test.txt', {}, function(err, data) {
    if (err) {
        throw err;
    }
    console.log(data);
});
// open + read
fs.open('test.txt','r',(err, fd) => {
    fs.fstat(fd,(err,stat)=>{
        var len = stat.size;  //检测文件长度
        var buf = new Buffer(len);
        fs.read(fd,buf,0,len,0,(err,bw,buf)=>{
            console.log(buf.toString('utf8'));
            fs.close(fd);
        })
    });
});

/* 写文件与读取文件API形式相似 */复制代码

读/写文件都有三种方式,那么区别是什么呢?

  • createReadStream/createWriteStream建立一个将文件内容读取为流数据的ReadStream对象,这个方法主要目的就是把数据读入到流中,获得是可读流,方便以流进行操做
  • readFile/writeFile:Node.js会将文件内容视为一个总体,为其分配缓存区而且一次性将文件内容读/写取到缓存区中,在这个期间,Node.js将不能执行任何其余处理,因此当读写大文件的时候,有可能形成缓存区“爆仓”
  • read/write读/写文件内容是不断地将文件中的一小块内容读/写入缓存区,最后从该缓存区中读取文件内容

同步API也是如此。其中最经常使用的是readFile,读取大文件则采起用,read则提供更为细节、底层的操做,并且read要配合open。

获取文件的状态:

fs.stat('eda.txt', (err, stat) => {
  if (err)
    throw err
  console.log(stat)
})
/* Stats { dev: 16777220, mode: 33279, nlink: 1, uid: 501, gid: 20, rdev: 0, blksize: 4194304, ino: 4298136825, size: 0, blocks: 0, atimeMs: 1510317983760.94, - 文件数据最近被访问的时间 mtimeMs: 1510317983760.94, - 文件数据最近被修改的时间。 ctimeMs: 1510317983777.8538, - 文件状态最近更改的时间 birthtimeMs: 1509537398000, atime: 2017-11-10T12:46:23.761Z, mtime: 2017-11-10T12:46:23.761Z, ctime: 2017-11-10T12:46:23.778Z, birthtime: 2017-11-01T11:56:38.000Z }*/复制代码

监听文件:

const FSWatcher = fs.watch('eda.txt', (eventType, filename) => {
    console.log(`${eventType}`)
})
FSWatcher.on('change', (eventType, filename) => {
    console.log(`${filename}`)
})
// watch和返回的FSWatcher实例的回调函数都绑定在了 change 事件上

fs.watchFile('message.text', (curr, prev) => {
  console.log(`the current mtime is: ${curr.mtime}`);
  console.log(`the previous mtime was: ${prev.mtime}`);
})复制代码

监听文件仍然有两种方法:

  • watch 调用的是底层的API来监视文件,很快,可靠性也较高
  • watchFile 是经过不断轮询 fs.Stat (文件的统计数据)来获取被监视文件的变化,较慢,可靠性较低,另外回调函数的参数是 fs.Stat 实例

所以尽量多的使用watch,watchFile 用于须要获得文件更多信息的场景。

其余

建立、删除、复制、移动、重命名、检查文件、修改权限...

总结

由Buffer到Stream,再到fs文件模块,将它们串联起来能对整块知识有更清晰的认识,也对webpack、gulp等前端自动化工具构建工做流的机制和实现有了更深的了解。学习其余知识亦是如此——知道前因后果,知道为何会存在,知道它们之间的联系,就能让碎片化的知识串联起来,能让它们make sense,可以让本身“上的厅堂、下得厨房”。

参考:

nodeJs高阶模块--fs

deep into node

相关文章
相关标签/搜索