在 JavaScript 中,一般只处理字符串层面的数据,但是在 Node.js 中,需要处理网络、文件等二进制数据。

  由此,引入了BufferStream的概念,两者都是字节层面的操作。

  Buffer 表示一块专门存放二进制数据的缓冲区。Stream 表示流,一种有序、有起点和终点的二进制传输手段。

  Stream 会从 Buffer 中读取数据,像水在管道中流动那样转移数据。

  本系列所有的示例源码都已上传至Github,点击此处获取。

一、Buffer

  Buffer 是 JavaScript 中的 Uint8Array 的子类,Uint8Array 是一种类型化数组,处理 8 位无符号整数。

  其行为类似于数组(有 length 属性,可迭代等),但并不是真正的数组,其元素是 16 进制的两位数。

  Buffer 在创建时就会确定占用内存的大小,之后就无法再调整,并且它会被分配一块 V8 堆栈外的原始内存。

  Buffer 的应用场景比较多,例如在zlib模块中,利用 Buffer 来操作二进制数据实现资源压缩的功能;在crypto模块的一些加密算法,也会使用 Buffer。

1)创建

  在 Node 版本 <= 6 时,创建 Buffer 实例是 通过构造函数创建的:new Buffer(),但后面的版本就废弃了。

  现在常用的创建方法有:

  • Buffer.from() :传入已有数据,转换成一个 Buffer 实例,数据可以是字符串、对象、数组等。
  • Buffer.alloc():分配指定字节数量的 Buffer 实例。
  • Buffer.allocUnsafe() :功能与 Buffer.alloc() 相同,但其所占内存中的旧数据不会被清除,可能会泄漏敏感数据。

2)编码

  在创建一个 Buffer 实例后,就可以像数组那样访问某个字符,而打印出的值是数字,如下所示,这些数字是 Unicode 码。

let buf = Buffer.from('strick')
console.log(buf[0]); // 115
console.log(buf[1]); // 116

  若在创建时包含中文字符,那么就会多 3 个 16 进制的两位数,如下所示。

let buf = Buffer.from('strick')
console.log(buf); // <Buffer 73 74 72 69 63 6b>
buf = Buffer.from('strick平')
console.log(buf); // <Buffer 73 74 72 69 63 6b e5 b9 b3>

  Buffer.from() 的第二个参数是编码,默认值是 utf8,而 1 个中文字符经过 UTF-8 编码后通常会占用 3 个字节,1 个英文字符只占用 1 个字节。

  在调用 toString() 方法后就能根据指定编码(不传默认是 UTF-8)将 Buffer 解码为字符串。

console.log(buf.toString());    // strick平

  Node.js 支持的其他编码包括 latin1、base64、ascii 等,具体可参考官方文档

3)内存分配原理

  Node.js 内存分配都是在 C++ 层面完成的,采用 Slab 分配器(Linux 中有广泛应用)动态分配内存,并且以 8KB 为界限来区分是小对象还是大对象(参考自深入浅出Node.js)。

  可以简单看下Buffer.from()的源码,当它的参数是字符串时,其内部会调用 fromStringFast() 函数(在src/lib/buffer.js中),然后根据字节长度分别处理。

  如果当前所占内存不够,那么就会调用 createPool() 扩容,通过调用 createUnsafeBuffer() 创建 Buffer,其中 FastBuffer 继承自 Uint8Array。

// 以 8KB 为界限
Buffer.poolSize = 8 * 1024;
// Buffer.from() 内会调用此函数
function fromStringFast(string, ops) {
const length = ops.byteLength(string);
// 长度大于 4KB(>>> 表示无符号右移 1 位)
if (length >= (Buffer.poolSize >>> 1))
return createFromString(string, ops.encodingVal);
// 当前所占内存不够(poolOffset 记录已经使用的字节数)
if (length > (poolSize - poolOffset))
createPool();
let b = new FastBuffer(allocPool, poolOffset, length);
const actual = ops.write(b, string, 0, length);
if (actual !== length) {
// byteLength() may overestimate. That's a rare case, though.
b = new FastBuffer(allocPool, poolOffset, actual);
}
poolOffset += actual;
alignPool();
return b;
}
// 初始化一个 8 KB 的内存空间
function createPool() {
poolSize = Buffer.poolSize;
allocPool = createUnsafeBuffer(poolSize).buffer;
markAsUntransferable(allocPool);
poolOffset = 0;
}
// 创建 Buffer
function createUnsafeBuffer(size) {
zeroFill[0] = 0;
try {
return new FastBuffer(size);
} finally {
zeroFill[0] = 1;
}
}
// FastBuffer 继承自 Uint8Array
class FastBuffer extends Uint8Array {}

二、流

  流(Stream)的概念最早见于 Unix 系统,是一种已被证实有效的编程方式。

  Node.js 内置的流模块会被其他多个核心模块所依赖,它具有可读、可写或可读写的特点,并且所有的流都是 EventEmitter 的实例,也就是说被赋予了异步的能力。

  官方总结了流的两个优点,分别是:

  • 内存效率: 无需加载大量的数据到内存中即可进行处理。
  • 时间效率: 当获得数据之后就能立即开始处理数据,而不必等到整个数据加载完,这样消耗的时间就变少了。

1)流类型

  流的基本类型有4种:

  • Readable:只能读取数据的流,例如 fs.createReadStream(),可注册的事件包括 data、end、error、close等。
  • Writable:只能写入数据的流,例如 fs.createWriteStream(),HTTP 的请求和响应,可注册的事件包括 drain、error、finish、pipe 等。
  • Duplex:Readable 和 Writable 都支持的全双工流,例如 net.Socket,这种流会维持两个缓冲区,分别对应读取和写入,允许两边同时独立操作。
  • Transform:在写入和读取数据时修改或转换数据的 Duplex 流,例如 zlib.createDeflate()。

  来看一个官方的 Readable 流示例,先是用 fs.readFile() 直接将整个文件读到内存中。当文件很大或并发量很高时,将消耗大量的内存。

const http = require('http')
const fs = require('fs') http.createServer(function(req, res) {
fs.readFile(__dirname + '/data.txt', (err, data) => {
res.end(data)
})
}).listen(1234)

  再用 fs.createReadStream() 方法通过流的方式来读取文件,其中 req 和 res 两个参数也是流对象。

  data.txt 文件中的内容将会一段段的传输给 HTTP 客户端,而不是等到读取完了再一次性响应,两者对比,高下立判。

http.createServer((req, res) => {
const readable = fs.createReadStream(__dirname + '/data.txt')
readable.pipe(res);
}).listen(1234)

2)pipe()

  在上面的示例中,pipe() 方法的作用是将一个可读流 readable 变量中的数据传输到一个可写流 res 变量(也叫目标流)中。

  pipe() 方法地主要目的是平衡读取和写入的速度,让数据的流动达到一个可接受的水平,防止因为读写速度的差异,而导致内存被占满。

  在 pipe() 函数内部会监听可读流的 data 事件,并且会自动调用可写流的 end() 方法。

  当内部缓冲大于配置的最高水位线(highWaterMark)时,也就是读取速度大于写入速度时,为了避免产生背压问题,Node.js 就会停止数据流动。

  当再次重启流动时,会触发 drain 事件,其具体实现可参考此文

  pipe() 方法会返回目标流,虽然支持链式调用,但必须是 Duplex 或 Transform 流,否则会报错,如下所示。

http.createServer((req, res) => {
const readable = fs.createReadStream(__dirname + '/data.txt')
const writable = fs.createWriteStream(__dirname + '/tmp.txt')
// Error [ERR_STREAM_CANNOT_PIPE]: Cannot pipe, not readable
readable.pipe(writable).pipe(res);
}).listen(1234)

3)end()

  很多时候写入流是不需要手动调用 end() 方法来关闭的。但如果在读取期间发生错误,那就不能关闭写入流,发生内存泄漏。

  为了防止这种情况发生,可监听可读流的错误事件,手动关闭,如下所示。

readable.on('error', function(err) {
writeable.close();
});

  接下来看一种网络场景,改造一下之前的示例,让可读流监听 data、end 和 error 事件,当读取完毕或出现错误时关闭可写流。

http.createServer((req, res) => {
const readable = fs.createReadStream(__dirname + '/data.txt')
readable.on('data', chunk => {
res.write(chunk);
});
readable.on('end',() => {
res.end();
})
readable.on('error', err => {
res.end('File not found');
});
}).listen(1234)

  若不手动关闭,那么页面将一直处于加载中,在KOA源码中,多处调用了此方法。

  注意,若取消对 data 事件的监听,那么页面也会一直处于加载中,因为流一开始是静止的,只有在注册 data 事件后才会开始活动。

4)大JSON文件

  网上看到的一道题,用 Node.js 处理一个很大的 JSON 文件,并且要读取到 JSON 文件的某个字段。

  直接用 fs.readFile() 或 require() 读取都会占用很大的内存,甚至超出电脑内存。

  直接用 fs.createReadStream() 也不行,读到的数据不能格式化成 JSON 对象,难以读取字段。

  CNode论坛上对此问题也做过专门的讨论

  借助开源库JSONStream可以实现要求,它基于jsonparse,这是一个流式 JSON 解析器。

  JSONStream 的源码去掉注释和空行差不多 200 行左右,在此就不展开分析了。

参考资料:

缓冲区 Stream多文件合并 pipe

legacy.js模块实现分析 Stream两种模式

Stream背压

深入理解Node.js之Buffer 

Node.js Buffer Node.js 流

Node.js 语法基础 —— Buffter & Stream

node源码分析

通过源码解析 Node.js 中导流(pipe)的实现

Node.js精进(3)——流的更多相关文章

  1. node.js中stream流中可读流和可写流的使用

    node.js中的流 stream 是处理流式数据的抽象接口.node.js 提供了很多流对象,像http中的request和response,和 process.stdout 都是流的实例. 流可以 ...

  2. 理解 Node.js 中 Stream(流)

    Stream(流) 是 Node.js 中处理流式数据的抽象接口. stream 模块用于构建实现了流接口的对象. Node.js 提供了多种流对象. 例如,对 HTTP 服务器的request请求和 ...

  3. Node.js精进(7)——日志

    在 Node.js 中,提供了console模块,这是一个简单的调试控制台,其功能类似于浏览器提供的 JavaScript 控制台. 本系列所有的示例源码都已上传至Github,点击此处获取. 一.原 ...

  4. [Node.js] Node.js中的流

    原文地址:http://www.moye.me/2015/03/29/streaming_in_node/ 什么是流? 说到流,就涉及到一个*nix的概念:管道——在*nix中,流在Shell中被实现 ...

  5. Node.js:Stream(流)

    Stream 是一个抽象接口,Node 中有很多对象实现了这个接口.例如,对http 服务器发起请求的request 对象就是一个 Stream,还有stdout(标准输出). Node.js,Str ...

  6. Node.js精进(4)——事件触发器

    Events 是 Node.js 中最重要的核心模块之一,很多模块都是依赖其创建的,例如上一节分析的流,文件.网络等模块. 比较知名的 Express.KOA 等框架在其内部也使用了 Events 模 ...

  7. Node.js精进(5)——HTTP

    HTTP(HyperText Transfer Protocol)即超文本传输协议,是一种获取网络资源(例如图像.HTML文档)的应用层协议,它是互联网数据通信的基础,由请求和响应构成. 在 Node ...

  8. Node.js精进(6)——文件

    文件系统是一种用于向用户提供底层数据访问的机制,同时也是一套实现了数据的存储.分级组织.访问和获取等操作的抽象数据类型. Node.js 中的fs模块就是对文件系统的封装,整合了一套标准 POSIX ...

  9. Node.js精进(1)——模块化

    模块化是一种将软件功能抽离成独立.可交互的软件设计技术,能促进大型应用程序和系统的构建. Node.js内置了两种模块系统,分别是默认的CommonJS模块和浏览器所支持的ECMAScript模块. ...

随机推荐

  1. 在 Docker 上快速运行 Apache Airflow 2.2.4

    Docker 安装 Apache Airflow 参考资料 Running Airflow in Docker 安装依赖 Docker Engine Docker Composite 快速运行 Apa ...

  2. ZABBIX Proxy容器启动的配置过程

    ZABBIX Proxy容器启动的配置过程 环境介绍 版本 zabbix6 zabbix server 与 zabbix proxy 非同一台主机,zabbix proxy为主动方式提交给server ...

  3. 面试突击39:synchronized底层是如何实现的?

    想了解 synchronized 是如何运行的?就要先搞清楚 synchronized 是如何实现? synchronized 同步锁是通过 JVM 内置的 Monitor 监视器实现的,而监视器又是 ...

  4. 1. 初识 JavaScript

    1.1 JavaScript 是什么 布兰登·艾奇(Brendan Eich,1961年-). 神奇的大哥用10天完成 JavaScript 设计. 最初命名为 LiveScript,后来在与 Sun ...

  5. 新华三Gen10服务器进SSA查看、配置阵列

    1.开机自检进F10 2.F10后选择[smart storage administrator](跳到第5步)或选择第一项IP[intelligent provisioning] 3.选择执行维护 4 ...

  6. 这样理解 HTTP,面试再也不用慌了~

    开源Linux 长按二维码加关注~ 上一篇:SSH只能用于远程Linux主机? 1 HTTP HTTP 协议是个无状态协议,不会保存状态. 2 Post 和 Get 的区别 先引入副作用和幂等的概念. ...

  7. NS2的LEACH仿真出来的nam文件拓扑的节点为什么x=0,且y=0

    查看.tr文件和.nam发文件下所有的节点的x,y值都是(0,0),nam图像更没有运行出来 于是我将if { $opt(sc) == "" } {puts "*** N ...

  8. JUC自定义线程池练习

    JUC自定义线程池练习 首先上面该线程池的大致流程 自定义阻塞队列 首先定义一个双向的队列和锁一定两个等待的condition 本类用lock来控制多线程下的流程执行 take和push方法就是死等, ...

  9. Linux中文件/文件系统的压缩、打包和备份总结(基于rhel7)

    文件/文件系统的压缩.打包 Linux有哪些压缩工具可供选择 按压缩比:xz>bzip2>gzip,按压缩时长:gzip>bzip2>xz,另外还有zip可以选择. gzip只 ...

  10. Git 后续——分支与协作

    Git 后续--分支与协作 本文写于 2020 年 9 月 1 日 之前一篇文章写了 Git 的基础用法,但那其实只是「单机模式」,Git 之所以在今天被如此广泛的运用,是脱不开分支系统这一概念的. ...