熟悉Buffer

前言

在引入 TypedArray 之前,JavaScript 语言没有用于读取或处理二进制数据流的机制。 Buffer 类是作为 Node.js API 的一部分引入的,以允许对 TCP 流、文件等二进制数据进行操作。

本文所讲内容都基于 Node 的 v10.x 版本。

Buffer 结构

Buffer 是一个类数组对象,主要用于操作字节。下面我们从模块结构和对象结构的层面上来认识它。

模块结构

Buffer 是一个典型的 JavaScript 与 C++ 结合的模块,它将性能相关部分用 C++ 实现,将非性能相关的部分用 JavaScript 实现,如图:

img-1

Buffer 所占用的内存不是通过 V8 分配的,属于堆外内存。

由于 Buffer 太过常见,Node 在进程启动时就已经加载了它,并将其放在全局对象(global) 上。所以在使用 Buffer 时,无须通过 require() 即可直接使用。

Buffer 对象

Buffer 对象类似于数组,它的元素为 16 进制的两位数,即 0 到 255 的数值。示例代码如下所示:

var str = '深入浅出node.js';
var buf = Buffer.from(str, 'utf8');
console.log(buf);
// => 

由上面的示例可见,不同编码的字符串占用的元素个数各不相同,上面代码中的中文字在 UTF-8 编码下占用 3 个元素,字母和半角标点符号占用 1 个元素。

Buffer 受 Array 类型的影响很大,可以访问 length 属性得到长度,也可以通过下标访问元素, 在构造对象时也十分相似,代码如下:

var buf = Buffer.alloc(10);
console.log(buf.length); // => 10
console.log(buf); // => 

上述代码分配了一个长 10 字节的 Buffer 对象。可以通过下标访问刚初始化的 Buffer 的元素, 代码如下:

console.log(buf[0]); // => 0

它的元素值是 0,这是 Buffer 分配时,没提供初始化值的缺省值。

同样,我们也可以通过下标对它进行赋值:

buf[0] = 255;
console.log(buf[0]); // => 255

值得注意的是,如果给元素赋值不是 0 到 255 的整数或是小数时会怎样呢?示例代码如下所示:

buf[0] = -100;
console.log(buf[0]); // 156
buf[1] = 300;
console.log(buf[1]); // 44
buf[2] = 3.1415;
console.log(buf[2]); // 3

给元素的赋值如果小于 0,就将该值逐次加 256,直到得到一个 0 到 255 之间的整数。如果得到的数值大于 255,就逐次减 256,直到得到 0~255 区间内的数值。如果是小数,舍弃小数部分,只保留整数部分。

Buffer 内存分配

Buffer 对象的内存分配不是在 V8 的堆内存中,而是在 Node 的 C++ 层面实现内存的申请的。因为处理大量的字节数据不能采用需要一点内存就向操作系统申请一点内存的方式,这可能造成大量的内存申请的系统调用,对操作系统有一定压力。为此 Node 在内存的使用上应用的是在 C++ 层面申请内存、在 JavaScript 中分配内存的策略。

Buffer 的转换

Buffer对象可以与字符串之间相互转换。目前支持的字符串编码类型有如下这几种。

  • ASCII
  • UTF-8
  • UTF-16LE/UCS-2
  • Base64
  • Latin1/Binary
  • Hex

字符串转 Buffer

字符串转 Buffer 对象主要是通过 Buffer.from 函数完成的:

Buffer.from(string[, encoding]);

Buffer.from 函数转换的 Buffer 对象,存储的只能是一种编码类型。encoding 参数不传递时,默认按UTF-8编码进行转码和存储。

一个 Buffer 对象可以存储不同编码类型的字符串转码的值,调用write()方法可以实现该目的,代码如下:

buf.write(string[, offset[, length]][, encoding])

由于可以不断写入内容到 Buffer 对象中,并且每次写入可以指定编码,所以 Buffer 对象中可以存在多种编码转化后的内容。需要小心的是,每种编码所用的字节长度不同,将 Buffer 反转回字符串时需要谨慎处理。

Buffer 转字符串

实现 Buffer 向字符串的转换也十分简单,Buffer 对象的 toString() 可以将 Buffer 对象转换为字符串,代码如下:

buf.toString([encoding[, start[, end]]])

比较精巧的是,可以设置 encoding(默认为UTF-8)、start、end 这3个参数实现整体或局部的转换。如果 Buffer 对象由多种编码写入,就需要在局部指定不同的编码,才能转换回正常的编码。

Buffer 不支持的编码类型

目前比较遗憾的是,Node 的 Buffer 对象支持的编码类型有限,只有少数的几种编码类型可以在字符串和 Buffer 之间转换。为此,Buffer 提供了一个 isEncoding() 函数来判断编码是否支持转换:

Buffer.isEncoding(encoding)

将编码类型作为参数传入上面的函数,如果支持转换返回值为 true,否则为 false。很遗憾的是,在中国常用的 GBK、GB2312 和 BIG-5 编码都不在支持的行列中。

对于不支持的编码类型,可以借助Node生态圈中的模块完成转换。iconv 和 iconv-lite 两个模块可以支持更多的编码类型转换,包括 Windows 125 系列、ISO-8859 系列、IBM/DOS 代码页系列、Macintosh 系列、KOI8 系列,以及 US-ASCII,也支持宽字节编码 GBK 和 GB2312。

iconv-lite 采用纯 JavaScript 实现,iconv 则通过 C++ 调用 libiconv 库完成。前者比后者更轻量,无须编译和处理环境依赖直接使用。在性能方面,由于转码都是耗用 CPU,在 V8 的高性能下,少了 C++ 到 JavaScript 的层次转换,纯 JavaScript 的性能比 C++ 实现得更好。

以下为 iconv-lite 的示例代码:

var iconv = require('iconv-lite');
// 字符串转Buffer
var buf = iconv.encode('Sample input string', 'win1251');
console.log(buf);
// Buffer转字符串
var str = iconv.decode(buf, 'win1251');
console.log(str); // => Sample input string

另外,iconv 和 iconv-lite 对无法转换的内容进行降级处理时的方案不尽相同。iconv-lite 无法转换的内容如果是多字节,会输出 �;如果是单字节,则输出 ?。iconv 则有三级降级策略,会尝试翻译无法转换的内容,或者忽略这些内容。如果不设置忽略,iconv 对于无法转换的内容将会得到 EILSEQ 异常。如下是 iconv 的示例代码兼选项设置方式:

var Iconv = require('iconv').Iconv;

var iconv = new Iconv('UTF-8', 'ASCII');
try {
  iconv.convert('ça va'); // throws EILSEQ
} catch (err) {
  console.log(err);
}

var iconv = new Iconv('UTF-8', 'ASCII//IGNORE');
var str = iconv.convert('ça va'); // returns "a va"
console.log(str.toString());

var iconv = new Iconv('UTF-8', 'ASCII//TRANSLIT');
var str = iconv.convert('ça va'); // "ca va"
console.log(str.toString());

var iconv = new Iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE');
var str = iconv.convert('ça va が'); // "ca va "
console.log(str.toString());

Buffer 与性能

Buffer 在文件 I/O 和网络 I/O 中运用广泛,尤其在网络传输中,它的性能举足轻重。在应用中,我们通常会操作字符串,但一旦在网络中传输,都需要转换为 Buffer,以进行二进制数据传输。 在 Web 应用中,字符串转换到 Buffer 是时时刻刻发生的,提高字符串到 Buffer 的转换效率,可以很大程度地提高网络吞吐率。

在展开 Buffer 与网络传输的关系之前,我们可以先来进行一次性能测试。下面的例子中构造了一个10 KB 大小的字符串。我们首先通过纯字符串的方式向客户端发送,代码如下:

var http = require('http');
var helloworld = '';
for (var i = 0; i < 1024 * 10; i++) {
  helloworld += 'a';
}
// helloworld = Buffer.from(helloworld);
http
  .createServer(function(req, res) {
    res.writeHead(200);
    res.end(helloworld);
  })
  .listen(8001);

我们通过 ab 进行一次性能测试,发起 100 个并发客户端:

ab -c 100 -t 2 http://127.0.0.1:8001/

得到的测试结果如下所示:

HTML transferred:       81848320 bytes
Requests per second:    3966.46 [#/sec] (mean)
Time per request:       25.211 [ms] (mean)
Time per request:       0.252 [ms] (mean, across all concurrent requests)
Transfer rate:          40226.90 [Kbytes/sec] received

测试的 QPS(每秒查询次数)是3966.46,传输率为每秒40226.90 KB。

接下来我们取消掉 helloworld = Buffer.from(helloworld); 前的注释,使向客户端输出的是一个 Buffer 对象,无须在每次响应时进行转换。再次进行性能测试的结果如下所示:

HTML transferred:       112220160 bytes
Requests per second:    5455.92 [#/sec] (mean)
Time per request:       18.329 [ms] (mean)
Time per request:       0.183 [ms] (mean, across all concurrent requests)
Transfer rate:          55190.45 [Kbytes/sec] received

QPS 的提升到5455.92,传输率为每秒55190.45 KB,性能相比前面有较大提升。

通过预先转换静态内容为 Buffer 对象,可以有效地减少 CPU 的重复使用,节省服务器资源。 在 Node 构建的 Web 应用中,可以选择将页面中的动态内容和静态内容分离,静态内容部分可以通过预先转换为 Buffer 的方式,使性能得到提升。由于文件自身是二进制数据,所以在不需要改变内容的场景下,尽量只读取 Buffer,然后直接传输,不做额外的转换,避免损耗。

参考文献

  • 深入浅出 Node.js(朴灵)

  • https://nodejs.org/docs/latest-v10.x/api/buffer.html

你可能感兴趣的:(Node,Node,Buffer)