资讯专栏INFORMATION COLUMN

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

TANKING / 2034人阅读

摘要:端输入数据到端,对就是输入流,得到的对象就是可读流对就是输出端得到的对象是可写流。在中,这四种流都是的实例,它们都有事件,可读流具有监听数据到来的事件等,可写流则具有监听数据已传给低层系统的事件等,和都同时实现了和的事件和接口。

原文地址在我的博客

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

正文 二进制缓冲区Buffer

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

// 创建一个长度为 10、且用 30 填充的 Buffer。
const buf1 = Buffer.alloc(10, 30)
console.log(buf1)// 
// 字符串转Buffer
const buf2 = Buffer.from("javascript")
console.log(buf2)// 
// 字符串转 buffer
console.log(buf2.toString())// javascript
console.log(buf2.toString("hex")) //6a617661736372697074

一个 Buffer 类似于一个整数数组,可以取下标,有length属性,有剪切复制操作等,很多API也类似数组,但Buffer的大小在被创建时确定,且无法调整。Buffer处理的是字节,两位十六进制,因此在整数范围就是0~255。

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

Buffer内存分配与性能优化

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

流Stream

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

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

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

值得一提的是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架构如下:

读写操作:

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/89707.html

相关文章

  • Node.js设计模式》使用流进行编码

    摘要:如何创建并使用。正如我们所预料到的那样,使用来进行大文件的读取显然是错误的。使用进行压缩文件我们必须修复我们的应用程序,并使其处理大文件的最简单方法是使用的。确切地说,由返回的流。 本系列文章为《Node.js Design Patterns Second Edition》的原文翻译和读书笔记,在GitHub连载更新,同步翻译版链接。 欢迎关注我的专栏,之后的博文将在专栏同步: En...

    xinhaip 评论0 收藏0
  • 阅读gulp源码小结

    摘要:源码简介源码核心部分寥寥行。同时本身是直接继承于模块。写在末尾阅读代码的这一次,是我第一次阅读这种开源的模块化项目。深深的被震撼到了,认识到了模块化的巨大力量。就能完成非常复杂的事情,而不需要凡是亲力亲为,一行行代码,一个个小问题依次解决。 gulp源码简介 gulp源码核心部分寥寥60+行。但是通过这60+行代码,gulp给我们带来的确是前端自动化构建的便利。以往以为其源码肯定蛮复杂...

    Rocture 评论0 收藏0
  • Node.js 中度体验

    摘要:创建简单应用使用指令来载入模块创建服务器使用方法创建服务器,并使用方法绑定端口。全局安装将安装包放在下。的核心就是事件触发与事件监听器功能的封装。通常我们用于从一个流中获取数据并将数据传递到另外一个流中。压缩文件为文件压缩完成。 创建简单应用 使用 require 指令来载入 http 模块 var http = require(http); 创建服务器 使用 http.create...

    CastlePeaK 评论0 收藏0
  • Node.js学习总结

    摘要:表示当前正在执行的脚本的文件名。默认编码为模式为,为回调函数,回调函数只包含错误信息参数,在写入失败时返回。参数使用说明如下通过方法返回的文件描述符。 Node.js回调 Node.js异步编程的直接体现就是回调。 阻塞代码: const fs = require(fs); let data = fs.readFileSync(input.txt); console.log(data...

    kamushin233 评论0 收藏0
  • node.js入门学习笔记整理——基础篇

    摘要:的介绍一般是这样在中,类是随内核一起发布的核心库。库为带来了一种存储原始数据的方法,可以让处理二进制数据,每当需要在中处理操作中移动的数据时,就有可能使用库。这样传递数据会更快。 零、开始之前 1、 首先解释一下node.js是什么? 2、node.js和javascript有什么不同? 1)因为javascript主要是用在browser,而node.js是在server或者你的电脑...

    Tamic 评论0 收藏0

发表评论

0条评论

TANKING

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<