熟悉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);
// => <Buffer e6 b7 b1 e5 85 a5 e6 b5 85 e5 87 ba 6e 6f 64 65 2e 6a 73>

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

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

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

上述代码分配了一个长 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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先,需要了解DAC的基本原理。DAC(Digital to Analog Converter)是一种将数字信号转换为模拟信号的电路。在STM32F103C8T6单片机中,它有两个12位的DAC通道,可以将数字信号转换为模拟电压输出。 以下是一个简单的函数发生器的代码,它可以产生正弦波、方波和三角波信号。 ```c #include "stm32f10x.h" #define PI 3.1415926 void DAC_Configuration(void); void DAC_SetValue(uint16_t value); int main(void) { uint16_t sin_wave[256]; uint16_t tri_wave[256]; uint16_t sqr_wave[256]; uint16_t i; for(i=0; i<256; i++) { sin_wave[i] = (uint16_t)(2047*sin(2*PI*i/256) + 2048); tri_wave[i] = (uint16_t)(i*16); sqr_wave[i] = (i<128) ? 0 : 4095; } DAC_Configuration(); while(1) { for(i=0; i<256; i++) { DAC_SetValue(sin_wave[i]); // 正弦波 //DAC_SetValue(tri_wave[i]); // 三角波 //DAC_SetValue(sqr_wave[i]); // 方波 for(int j=0; j<1000; j++); // 稍作延时 } } } void DAC_Configuration(void) { GPIO_InitTypeDef GPIO_InitStructure; DAC_InitTypeDef DAC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); DAC_InitStructure.DAC_Trigger = DAC_Trigger_None; DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None; DAC_InitStructure.DAC_LFSRUnmask_TriangleAmplitude = DAC_TriangleAmplitude_4095; DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Enable; DAC_Init(DAC_Channel_1, &DAC_InitStructure); DAC_Cmd(DAC_Channel_1, ENABLE); DAC_InitStructure.DAC_Trigger = DAC_Trigger_None; DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None; DAC_InitStructure.DAC_LFSRUnmask_TriangleAmplitude = DAC_TriangleAmplitude_4095; DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Enable; DAC_Init(DAC_Channel_2, &DAC_InitStructure); DAC_Cmd(DAC_Channel_2, ENABLE); } void DAC_SetValue(uint16_t value) { DAC_SetChannel1Data(DAC_Align_12b_R, value); DAC_SetChannel2Data(DAC_Align_12b_R, value); DAC_SoftwareTriggerCmd(DAC_Channel_1, ENABLE); DAC_SoftwareTriggerCmd(DAC_Channel_2, ENABLE); } ``` 在这个代码中,我们首先定义了三个数组,分别用于存储正弦波、三角波和方波的波形数据。然后我们调用了`DAC_Configuration`函数,用于初始化DAC通道1和通道2。最后我们在主函数中使用一个无限循环,并在循环中逐个输出波形数据。 在`DAC_Configuration`函数中,我们首先开启了GPIOA和DAC的时钟,并设置了PA4和PA5为模拟输入。接着我们使用`DAC_Init`函数初始化了DAC通道1和通道2,并开启了输出缓存。最后我们调用了`DAC_Cmd`函数,使能了DAC通道1和通道2。 在`DAC_SetValue`函数中,我们使用`DAC_SetChannel1Data`和`DAC_SetChannel2Data`函数分别输出了波形数据,并使用`DAC_SoftwareTriggerCmd`函数触发了DAC的输出。由于我们使用了两个DAC通道,所以需要分别输出波形数据。 需要注意的是,由于DAC的输出电压范围是0V到3.3V,所以我们需要将波形数据缩放到0到4095之间。在这个例子中,我们使用了2048作为正弦波的偏置电压,这样正弦波就可以在0V到3.3V之间变化。对于三角波和方波,我们直接将波形数据缩放到0到4095之间。 最后,我们可以根据需要调整波形的频率和幅度,以生成不同的信号。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值