node.js学习记录: 理解Buffer

邓良
• 阅读 4873
Buffer是一个类数组对象,里面存储的是字节,有点类似于字节数组,我们常用的场景是网络请求时对流数据的处理或文件操作时对字符串的处理。

在Node中,应用需要处理网络协议、操作数据库、处理图片、接收上传文件等,在网络流和文件的操作中,还要处理大量二进制数据,JavaScript自有的字符串远远不能满足这些需求(javascript字符串是utf-8存储的,处理二进制数据能力很弱,而网络层对于不同资源的请求响应和文件都是二进制数据来交互的),于是Buffer对象应运而生,nodejs提供了这么一个接口,来创建一个专门存放二进制数据的缓存区,并提供一些方法对缓存区数据进行处理。

Buffer(直译 缓冲,nodejs中用于处理二进制的数据)在nodejs中可以全局访问,不需要用require关键字加载

Buffer是一个像Array的对象,但它主要用于操作字节。

node.js学习记录: 理解Buffer

我们可以发现,Buffer是一个对象,也是一个构造函数,具有自己的属性和静态方法,通过它new出来的实例,代表可以把引擎分配出一段内存,基本是一段数组,它的元素为16进制的两位数,即0到255的数值。

我们可以从模块结构对象结构层次两方面来认识。

  • 模块结构

Buffer是一个典型的JavaScript与C++结合的模块,性能相关部分用C++实现,飞性能相关部分用JavaScript实现。

  • buffer对象
1.new Buffer(size):分配一个新的 buffer 大小是 size 的8位字节. 
2.new Buffer(array):分配一个新的 buffer 使用一个8位字节 array 数组. 
3.new Buffer(str, [encoding]):encoding String类型 - 使用什么编码方式,参数可选.

4.类方法: Buffer.isEncoding(encoding):如果给定的编码 encoding 是有效的,返回 true,否则返回 false。 
5.类方法: Buffer.isBuffer(obj):测试这个 obj 是否是一个 Buffer. 返回Boolean
6.类方法: Buffer.concat(list, [totalLength]):list {Array}数组类型,Buffer数组,用于被连接。totalLength {Number}类型 上述Buffer数组的所有Buffer的总大小。
> new Buffer ('Hello 镜心的小树屋','base64')
<Buffer 1d e9 65 a0>

node.js学习记录: 理解Buffer

  • Buffer 内存分配

Buffer对象的内存分配不是在V8的堆内存中,而是在Node的C++层面实现内存的申请,在Javascript中分配内存。
为了高效使用申请的内存,Node采用slab分配机制。
slab就是一块申请好的固定大小的内存区域,slab有三种状态:

  • full:完全分配状态
  • partial:部分分配状态
  • empty: 没有分配状态

当我们需要一个Buffer对象,可以通过以下方式分配指定大小Buffer对象

new Buffer(size)

Node以8KB为界限来区分Buffer是大对象还是小对象
node.js学习记录: 理解Buffer

这个8KB的值就是每个slab的大小值,在JavaScript层面,以它作为单位单元进行内存的分配

  • 写入缓冲区
var buffer = new Buffer(8);//创建一个分配了8个字节内存的缓冲区
console.log(buffer.write('a','utf8'));//输出1

这会将字符"a"写入缓冲区,node返回经过编码以后写入缓冲区的字节数量,这里的字母autf-8编码占用1个字节。

  • 复制缓冲区

Node.js提供了一个将Buffer对象整体内容复制到另一个Buffer对象中的方法。我们只能在已经存在的Buffer对象之间复制,所以必须创建它们。

buffer.copy(bufferToCopyTo)

其中,bufferToCopyTo是要复制的目标Buffer对象。如下示例:

var buffer1 = new Buffer(8);
buffer1.write('nice to meet u','utf8');
var buffer2 = new Buffer(8);
buffer1.copy(buffer2);
console.log(buffer2.toString());//nice to meet u

进一步了解 看这篇 https://github.com/ElemeFE/no...

ES6的TypedArray

node.js学习记录: 理解Buffer

Buffer concat vs String concat

源码 => 仓库

const fs = require('fs')

// 从某个文件创建一个字节流
const stream = fs.createReadStream('test.md', {highWaterMark: 11})
stream.setEncoding('utf8')

var data = ''
stream.on('data', function (chunk) {
  data += chunk
})
// console.log('dasddd',stream)

stream.on('end', function () {
  console.log(data)
})

输出

node.js学习记录: 理解Buffer

面试常见问题

Buffer 一般用于处理什么数据? 其长度能否动态变化? more

注意以上有api已经废弃

buffer.alloc

const buf = Buffer.alloc(1024*1024); //分配一块 1M的内存
let offset = 0;

// 开始编码
offset = 0; // 重置偏移量
buf[0] = 0;
// 我们在开发网络通讯协议的时候操作 Buffer 都应该用大端序的 API,也就是 BE 结尾的。
buf.writeInt32BE(1000, 1);
buf[5] = 1; //codec => 1 代表是JSON 序列化

offset += 10;

const payload = {
  service: 'com.alipay.nodejs.HelloService:1.0',
  methodName: 'plus',
  args: [ 1, 2 ],
}

const bodyLength = buf.write(JSON.stringfy(payload), offset);
buf.writeInt32BE(bodyLength, 6);
offset += bodyLength;
buf.slice(0, offset); // 返回  

buf.writeInt32BE

buf.write

buf.slice

浅拷贝,类似Array.slice()
建一个指向与原始 Buffer 同一内存的新 Buffer,但使用 start 和 end 进行了裁剪。

修改新建的 Buffer 切片,也会同时修改原始的 Buffer,因为两个对象所分配的内存是重叠的。

Buffer.concat

/** 
 * 正确拼接Buffer
 * 
 * 正确的拼接方式是用一个数组来储存接收到的所有Buffer片段并记录下所有片段的总长度
 * 然后调用Buffer.concat()方法生成一个合并的Buffer对象。Buffer.concat()方法封装了从小Buffer对象向大Buffer对象的复制过程,实现十分细腻
 * @list 要合并的Buffer数组 或 Uint8Array数组
 * @length 合并后的Buffer的总长度
 */

Buffer.concat = function (list, length) {
  if(!Array.isArray(list)) {
    throw new Error('Usage: Buffer.concat(list, [length])')
  }

  if(list.length === 0) {
    return Buffer.alloc(0)
  } else if (list.length === 1) {
    return list[0]
  } 

  if(typeof length !== 'number') {
    length = 0
    for (let i = 0; i < list.length; i++) {
      let buf = list[i]
      length += buf.length
    }
  }

  let buffer = Buffer.alloc(length)
  var pos = 0;
  for (let i = 0; i < list.length; i++) {
    let buf = list[i]
    buf.copy(buffer, pos)
    pos+=buf.length
  }

  return buffer
}

Long类型的处理

JavaScript 的基本类型里面表示数字的只有 Number,它能够表达的整数范围是 -(2^53 - 1) ~ (2^53 - 1),而 Java 里面的 Long 类型的范围是 -(2^64 - 1) ~ (2^64 - 1)。那么在 RPC 调用中遇到 Long 类型我们该如何处理呢?

// 2^64-1 正确的值应该是 18446744073709551615,在 JS 引擎中运行得到的值却是 18446744073709552000
Math.pow(2, 64) - 1 // 18446744073709552000

首先我们得搞清楚 Long 类型是怎么存储的,一个 Long 数字占用 8 Bytes,我们可以把它拆分成两个 32 位整数(各占 4 Bytes)来表示,分别称之为「高位」和「低位」,低位存储的是长整形对 2^32 取模后的值,高位储存的是长整形整除 2^32 后的值,下面是几个 Long 类型整数用二进制的表示形式:

Long: 4294967296

High: 1     Low: 0
+-----------+-----------+
|00 00 00 01|00 00 00 00|
+-----------+-----------+
  
Long: 1000

High: 0     Low: 1000
+-----------+-----------+
|00 00 00 00|00 00 03 e8|
+-----------+-----------+
  
Long: 45565600000000

High: 10609 Low: 291956736
+-----------+-----------+
|00 00 29 71|11 66 e8 00|
+-----------+-----------+

那么在 JS 里面如何表示一个 Long 类型?这里我们使用了社区的 Long 模块,它可以通过 Number 或者字符串获得一个长整形,并且支持各种运算操作

const Long = require('long')
// const buf = new Buffer([ 0x00, 0x00, 0x29, 0x71, 0x11, 0x66, 0xe8, 0x00 ]);  
//  该api已经废弃,使用 Buffer.from()替代
const buf = Buffer.from([ 0x00, 0x00, 0x29, 0x71, 0x11, 0x66, 0xe8, 0x00 ])
let long = new Long(
  buf.readInt32BE(4),
  buf.readInt32BE(0),
)
long = long.add(1)
long.toString(); // 45565600000001
// const longBuf = new Buffer(long.toBytes()) // <Buffer 00 00 29 71 11 66 e8 01>
const longBuf = Buffer.from(long.toBytes())
Long.fromString('18446744073709551615') // Long { low: -1, high: -1, unsigned: false }

好用的包

uffer 原生 API 比较底层,对于用户不够友好,为了方便使用,社区封装了一个 一些模块:

常用关于buffer操作

https://github.com/shelljs/sh...

line 274


// Normalizes Buffer creation, using Buffer.alloc if possible.
// Also provides a good default buffer length for most use cases.
var buffer = typeof Buffer.alloc === 'function' ?
  function (len) {
    return Buffer.alloc(len || config.bufLength);
  } :
  function (len) {
    return new Buffer(len || config.bufLength);
  };
exports.buffer = buffer;

参考:

ArrayBuffer MDN
nodejs buffer api
《深入浅出 nodejs》朴灵
慕课网 进击Node.js基础(二)
nodejs Docs
https://github.com/ElemeFE/no...
聊聊 Node.js RPC(一)— 协议

未完待续。。。

点赞
收藏
评论区
推荐文章
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
CuterCorley CuterCorley
4年前
uni-app入门教程(5)接口的基本使用
前言本文主要介绍uniapp提供的一些基础接口,包括:网络请求接口,用于通过指定的请求方法,携带特定的数据,向特定的地址请求并返回请求结果;图片处理接口,包括选择、预览、获取信息、保存到本地等接口;文件处理接口,包括文件上传和下载接口;数据缓存接口,包括以同步或异步的方式保存、获取或删除数据的接口。一、网络请求小程序要想正常运转,都需要与服务器端进
Wesley13 Wesley13
4年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Stella981 Stella981
4年前
Python标准库笔记(8) — pprint模块
struct模块提供了用于在字节字符串和Python原生数据类型之间转换函数,比如数字和字符串。  该模块作用是完成Python数值和C语言结构体的Python字符串形式间的转换。这可以用于处理存储在文件中或从网络连接中存储的二进制数据,以及其他数据源。1\.模块函数和Struct类  它除了提供一个Struct类之外,还
Stella981 Stella981
4年前
Android异步操作总结
Android中经常会有一些操作比如网络请求,文件读写,数据库操作,比较耗时,我们需要将其放在非UI线程去处理,此时,我们需要处理任务前后UI的变化和交互。我们需要通过类似js中异步请求处理,这里总结我所了解到的,方便自己记忆,也方便别人的浏览。1.AsyncTasknewAysncTask().execute();AsyncTask会
Stella981 Stella981
4年前
Linux应急响应(四):盖茨木马
0x00前言Linux盖茨木马是一类有着丰富历史,隐藏手法巧妙,网络攻击行为显著的DDoS木马,主要恶意特点是具备了后门程序,DDoS攻击的能力,并且会替换常用的系统文件进行伪装。木马得名于其在变量函数的命名中,大量使用Gates这个单词。分析和清除盖茨木马的过程,可以发现有很多值得去学习和借鉴的地方。0x01应急场景
Stella981 Stella981
4年前
Python文件处理
Python文件处理Python文件处理在python中,要对一个文件进行操作,得把文件抽象为Streams流或者说fileobject或者叫filelikeobjects。这样将文件当作一个流对象来处理就方便多了。Stream对象提供了很多操作方法(如read(),write()等)
Wesley13 Wesley13
4年前
初探 Objective
作者:Cyandev,iOS和MacOS开发者,目前就职于字节跳动0x00前言异常处理是许多高级语言都具有的特性,它可以直接中断当前函数并将控制权转交给能够处理异常的函数。不同语言在异常处理的实现上各不相同,本文主要来分析一下ObjectiveC和C这两个语言。为什么要把ObjectiveC和
Stella981 Stella981
4年前
Linux的文件描述符
(1).文件描述符的定义  文件描述符是内核为了高效管理已被打开的文件所创建的索引,用于指向被打开的文件,所有执行I/O操作的系统调用都通过文件描述符;文件描述符是一个简单的非负整数,用以表明每个被进程打开的文件。程序刚刚启动时,第一个打开的文件是0,第二个是1,以此类推。也可以理解为文件的身份ID。  用户通过操作系统处理信息的过程中,使用的交互设
Wesley13 Wesley13
4年前
Java多线程导致的的一个事物性问题
业务场景我们现在有一个类似于文件上传的功能,各个子站点接受业务,业务上传文件,各个子站点的文件需要提交到总站点保存,文件是按批次提交到总站点的,也就是说,一个批次下面约有几百个文件。      考虑到白天提交这么多文件会影响到子站点其他系统带宽,我们将分站点的文件提交到总站点这个操作过程独立出来,放到晚上来做,具体时间是晚上7:00到早上7:00。
小万哥 小万哥
1年前
Java 文件处理完全指南:创建、读取、写入和删除文件详细解析
Java文件操作文件处理简介文件处理是任何应用程序的重要部分。Java提供了许多用于创建、读取、更新和删除文件的方法。Java文件处理Java中的文件处理主要通过java.io包中的File类完成。该类允许我们处理文件,包括创建、读取、写入和删除文件。创建
邓良
邓良
Lv1
我绕得过江山错落,绕不过你。
文章
2
粉丝
0
获赞
0