node.JS二进制操作模块buffer对象使用方法详解
在es6引入typedarray之前,javascript语言没有读取或操作二进制数据流的机制。buffer类被引入作为nodejs的api的一部分,使其可以在tcp流和文件系统操作等场景中处理二进制数据流。现在typedarray已经被添加进es6中,buffer类以一种更优与更适合node.js用例的方式实现了uint8array。
buffer对象概述
由于应用场景不同,在node中,应用需要处理网络协议、操作数据库、处理图片、接收上传文件等,在网络流和文件的操作中,还要处理大量二进制数据,javascript自有的字符串远远不能满足这些需求,于是buffer对象应运而生
buffer是一个典型的javascript与c++结合的模块,它将性能相关部分用c++实现,将非性能相关的部分用javascript实现。buffer类的实例类似于整数数组,除了其是大小固定的、且在v8堆外分配物理内存。buffer的大小在其创建时就已确定,且不能调整大小
由于buffer太过常见,node在进程启动时就已经加载了它,并将其放在全局对象(global)上。所以在使用buffer时,无须通过require()即可直接使用
/* { [function: buffer] poolsize: 8192, from: [function], alloc: [function], allocunsafe: [function], allocunsafeslow: [function], isbuffer: [function: isbuffer], compare: [function: compare], isencoding: [function], concat: [function], bytelength: [function: bytelength] } */ console.log(buffer);
创建buffer对象
在 node.js v6之前的版本中,buffer实例是通过buffer构造函数创建的,它根据提供的参数返回不同的 buffer,而新版本的nodejs则提供了对应的方法
1、new buffer(size)。传一个数值作为第一个参数给buffer()(如new buffer(10)),则分配一个指定大小的新建的buffer对象
分配给这种buffer实例的内存是未初始化的(没有用0填充)。虽然这样的设计使得内存的分配非常快,但已分配的内存段可能包含潜在的敏感旧数据
这种buffer实例必须手动地被初始化,可以使用buf.fill(0)或写满这个buffer。虽然这种行为是为了提高性能而有意为之的,但开发经验表明,创建一个快速但未初始化的buffer与创建一个慢点但更安全的buffer之间需要有更明确的区分
var buf = new buffer(5); console.log(buf);//<buffer e0 f7 1d 01 00> buf.fill(0); console.log(buf);//<buffer 00 00 00 00 00>
[注意]当我们为一个buffer对象分配空间大小后,其长度就是固定的,不能更改
var buf = new buffer(5); console.log(buf);//<buffer b8 36 70 01 02> buf[0] = 1; console.log(buf);//<buffer 01 36 70 01 02> buf[10] = 1; console.log(buf);//<buffer 01 79 43 6f 6e>
buffer.allocunsafe(size)
在新版本中,由buffer.allocunsafe(size)方法替代,来分配一个大小为 size 字节的新建的没有用0填充的buffer。可以使用buf.fill(0)初始化buffer实例为0
var buf = buffer.allocunsafe(10); console.log(buf);//<buffer 75 63 74 42 79 4c 65 6e 67 74> buf.fill(0); console.log(buf);//<buffer 00 00 00 00 00 00 00 00 00 00>
buffer.alloc(size[, fill[, encoding]])
在新版本中,使用buffer.alloc(size)方法可以生成一个安全的buffer对象,参数size <integer> 新建的 buffer 期望的长度;fill <string> | <buffer> | <integer> 用来预填充新建的 buffer 的值。 默认: 0;encoding <string> 如果 fill 是字符串,则该值是它的字符编码。 默认: 'utf8'
分配一个大小为 size 字节的新建的 buffer 。 如果 fill 为 undefined ,则该 buffer 会用 0 填充
var buf = buffer.alloc(5); console.log(buf);//<buffer 00 00 00 00 00>
2、new buffer(array或buffer)。传一个数组或buffer作为第一个参数,则将所传对象的数据拷贝到buffer
var buf1 = new buffer([1, 2, 3, 4, 5]); console.log(buf1);//<buffer 01 02 03 04 05> var buf2 = new buffer(buf1); console.log(buf2);//<buffer 01 02 03 04 05>
buffer.from(array或buffer)
在新版本中,由buffer.from(array或buffer)方法替代
var buf1 = buffer.from([1, 2, 3, 4, 5]); console.log(buf1);//<buffer 01 02 03 04 05> var buf2 = buffer.from(buf1); console.log(buf2);//<buffer 01 02 03 04 05>
3、new buffer(string[, encoding])。第一个参数是字符串,第二个参数是编码方式,默认是'utf-8'
var buf1 = new buffer('this is a tést'); console.log(buf1.tostring());//this is a tést console.log(buf1.tostring('ascii'));//this is a tc)st var buf2 = new buffer('7468697320697320612074c3a97374', 'hex'); console.log(buf2.tostring());//this is a tést
node.js 目前支持的字符编码包括:
'ascii' - 仅支持 7 位 ascii 数据。如果设置去掉高位的话,这种编码是非常快的。
'utf8' - 多字节编码的 unicode 字符。许多网页和其他文档格式都使用 utf-8 。
'utf16le' - 2 或 4 个字节,小字节序编码的 unicode 字符。支持代理对(u+10000 至 u+10ffff)。
'ucs2' - 'utf16le' 的别名。
'base64' - base64 编码。当从字符串创建 buffer 时,这种编码可接受“url 与文件名安全字母表”。
'latin1' - 一种把 buffer 编码成一字节编码的字符串的方式。
'binary' - 'latin1' 的别名。
'hex' - 将每个字节编码为两个十六进制字符。
buffer.from(string[, encoding])
在新版本中,由buffer.from(string[, encoding]方法替代
var buf1 = buffer.from('this is a tést'); console.log(buf1.tostring());//this is a tést console.log(buf1.tostring('ascii'));//this is a tc)st var buf2 = buffer.from('7468697320697320612074c3a97374', 'hex'); console.log(buf2.tostring());//this is a tést
4、new buffer(arraybuffer[, byteoffset [, length]])。参数arraybuffer <arraybuffer> 一个 arraybuffer,或一个 typedarray 的 .buffer 属性;byteoffset <integer> 开始拷贝的索引。默认为 0;length <integer> 拷贝的字节数。默认为 arraybuffer.length - byteoffset
var arr = new uint16array(2); arr[0] = 5000; arr[1] = 4000; var buf = new buffer(arr.buffer); console.log(buf);//<buffer 88 13 a0 0f> arr[1] = 6000; console.log(buf);//<buffer 88 13 70 17>
buffer.from(arraybuffer[, byteoffset [, length]])
在新版本中,由buffer.from(arraybuffer[, byteoffset [, length]])方法替代
var arr = new uint16array(2); arr[0] = 5000; arr[1] = 4000; var buf = buffer.from(arr.buffer); console.log(buf);//<buffer 88 13 a0 0f> arr[1] = 6000; console.log(buf);//<buffer 88 13 70 17>
buffer对象类似于数组,它的元素为16进制的两位数,即0到255的数值
console.log(buffer.from('test'));//<buffer 74 65 73 74>
长度
不同编码的字符串占用的元素个数各不相同,中文字在utf-8编码下占用3个元素,字母和半角标点符号占用1个元素
var buf = buffer.from('match'); console.log(buf.length);//5 var buf = buffer.from('火柴'); console.log(buf.length);//6
下标
buffer受array类型的影响很大,可以访问length属性得到长度,也可以通过下标访问元素
var buf = buffer.alloc(10); console.log(buf.length); // => 10
上述代码分配了一个长10字节的buffer对象。我们可以通过下标对它进行赋值
buf[0] = 100; console.log(buf[0]); // => 100
要注意的是,给元素的赋值如果小于0,就将该值逐次加256,直到得到一个0到255之间的整数。如果得到的数值大于255,就逐次减256,直到得到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
fromcharcode
通常地,创建的buffer对象的内容是其uft-8字符编码
var buf = buffer.from('match'); console.log(buf); //<buffer 6d 61 74 63 68>
如果要访问其对应的字符,则需要使用字符串的fromcharcode()方法
console.log(string.fromcharcode(buf[0]));//'m'
内存分配
buffer对象的内存分配不是在v8的堆内存中,而是在node的c++层面实现内存的申请的。因为处理大量的字节数据不能采用需要一点内存就向操作系统申请一点内存的方式,这可能造成大量的内存申请的系统调用,对操作系统有一定压力。为此node在内存的使用上应用的是在c++层面申请内存、在javascript中分配内存的策略
为了高效地使用申请来的内存,node采用了slab分配机制。slab是一种动态内存管理机制,最早诞生于sunos操作系统(solaris)中,目前在一些*nix操作系统中有广泛的应用,如freebsd和linux。简单而言,slab就是一块申请好的固定大小的内存区域。slab具有如下3种状态:full:完全分配状态;partial:部分分配状态;empty:没有被分配状态
当我们需要一个buffer对象,可以通过以下方式分配指定大小的buffer对象:
new buffer(size);//旧 buffer.alloc(size);//新
poolsize
poolsize属性是用于决定预分配的、内部 buffer 实例池的大小的字节数。默认地,node以8kb为界限来区分buffer是大对象还是小对象:
buffer.poolsize = 8 * 1024;
这个8kb的值也就是每个slab的大小值,在javascript层面,以它作为单位单元进行内存的分配
1、分配小buffer对象
如果指定buffer的大小少于8kb,node会按照小对象的方式进行分配。buffer的分配过程中主要使用一个局部变量pool作为中间处理对象,处于分配状态的slab单元都指向它。以下是分配一个全新的slab单元的操作,它会将新申请的slowbuffer对象指向它:
var pool; function allocpool() { pool = new slowbuffer(buffer.poolsize); pool.used = 0; }
构造小buffer对象时的代码如下:
new buffer(1024);//旧 buffer.alloc(1024);//新
这次构造将会去检查pool对象,如果pool没有被创建,将会创建一个新的slab单元指向它:
if (!pool || pool.length - pool.used < this.length) allocpool();
同时当前buffer对象的parent属性指向该slab,并记录下是从这个slab的哪个位置(offset)开始使用的,slab对象自身也记录被使用了多少字节,代码如下:
this.parent = pool; this.offset = pool.used; pool.used += this.length; if (pool.used & 7) pool.used = (pool.used + 8) & ~7;
这时候的slab状态为partial。当再次创建一个buffer对象时,构造过程中将会判断这个slab的剩余空间是否足够。如果足够,使用剩余空间,并更新slab的分配状态。下面的代码创建了一个新的buffer对象,它会引起一次slab分配:
new buffer(3000);//旧 buffer.alloc(3000);//新
如果slab剩余的空间不够,将会构造新的slab,原slab中剩余的空间会造成浪费。例如,第一次构造1字节的buffer对象,第二次构造8192字节的buffer对象,由于第二次分配时slab中的空间不够,所以创建并使用新的slab,第一个slab的8kb将会被第一个1字节的buffer对象独占。下面的代码一共使用了两个slab单元:
new buffer(1);//旧 buffer.alloc(1);//新 new buffer(8192);//旧 buffer.alloc(8192);//新
要注意的是,由于同一个slab可能分配给多个buffer对象使用,只有这些小buffer对象在作用域释放并都可以回收时,slab的8kb空间才会被回收。尽管创建了1个字节的buffer对象,但是如果不释放它,实际可能是8kb的内存没有释放
2、分配大buffer对象
如果需要超过8kb的buffer对象,将会直接分配一个slowbuffer对象作为slab单元,这个slab单元将会被这个大buffer对象独占
// big buffer, just alloc one this.parent = new slowbuffer(this.length); this.offset = 0;
这里的slowbuffer类是在c++中定义的,虽然引用buffer模块可以访问到它,但是不推荐直接操作它,而是用buffer替代
上面提到的buffer对象都是javascript层面的,能够被v8的垃圾回收标记回收。但是其内部的parent属性指向的slowbuffer对象却来自于node自身c++中的定义,是c++层面上的buffer对象,所用内存不在v8的堆中
综上,真正的内存是在node的c++层面提供的,javascript层面只是使用它。当进行小而频繁的buffer操作时,采用slab的机制进行预先申请和事后分配,使得javascript到操作系统之间不必有过多的内存申请方面的系统调用。对于大块的buffer而言,则直接使用c++层面提供的内存,而无需细腻的分配操作
转换
buffer对象可以与字符串之间相互转换。目前支持的字符串编码类型有如下几种:ascii、utf-8、utf-16le/ucs-2、base64、binary、hex
write()
一个buffer对象可以存储不同编码类型的字符串转码的值,调用write()方法可以实现该目的
buf.write(string, [offset], [length], [encoding])
string <string> 要写入 buf 的字符串
offset <integer> 开始写入 string 的位置。默认: 0
length <integer> 要写入的字节数。默认: buf.length - offset
encoding <string> string 的字符编码。默认: 'utf8';返回: <integer> 写入的字节数
根据 encoding 的字符编码写入 string 到 buf 中的 offset 位置。 length 参数是写入的字节数。 如果 buf 没有足够的空间保存整个字符串,则只会写入 string 的一部分。 只部分解码的字符不会被写入
var buf = buffer.alloc(5); console.log(buf); //<buffer 00 00 00 00 00> var len = buf.write('test',1,3); console.log(buf);//<buffer 00 74 65 73 00> console.log(len);/3
由于可以不断写入内容到buffer对象中,并且每次写入可以指定编码,所以buffer对象中可以存在多种编码转化后的内容。需要小心的是,每种编码所用的字节长度不同,将buffer反转回字符串时需要谨慎处理
tostring()
实现buffer向字符串的转换也十分简单,buffer对象的tostring()可以将buffer对象转换为字符串
buf.tostring([encoding], [start], [end])
encoding - 使用的编码。默认为 'utf8'
start - 指定开始读取的索引位置,默认为 0
end - 结束位置,默认为缓冲区的末尾
返回 - 解码缓冲区数据并使用指定的编码返回字符串
var buf =buffer.alloc(26); for (var i = 0 ; i < 26 ; i++) { buf[i] = i + 97; } console.log( buf.tostring('ascii'));//abcdefghijklmnopqrstuvwxyz console.log( buf.tostring('ascii',0,5));//abcde console.log( buf.tostring('utf8',0,5));//abcde console.log( buf.tostring(undefined,0,5));//abcde
tojson()
将 node buffer 转换为 json 对象
buf.tojson()
返回 buf 的 json 格式
var buf = buffer.from('test'); var json = buf.tojson(buf); console.log(json);//{ type: 'buffer', data: [ 116, 101, 115, 116 ] }
isencoding()
目前比较遗憾的是,node的buffer对象支持的编码类型有限,只有少数的几种编码类型可以在字符串和buffer之间转换。为此,buffer提供了一个isencoding()函数来判断编码是否支持转换
buffer.isencoding(encoding)
将编码类型作为参数传入上面的函数,如果支持转换返回值为true,否则为false。很遗憾的是,在中国常用的gbk、gb2312和big-5编码都不在支持的行列中
console.log(buffer.isencoding('utf8'));//true console.log(buffer.isencoding('gbk'));//false
buffer类方法
buffer.bytelength(string[, encoding])
buffer.bytelength()方法返回一个字符串的实际字节长度。 这与 string.prototype.length 不同,因为那返回字符串的字符数
string <string> | <buffer> | <typedarray> | <dataview> | <arraybuffer> 要计算长度的值
encoding <string> 如果 string 是字符串,则这是它的字符编码。 默认: 'utf8'
返回: <integer> string 包含的字节数
var str = '火柴'; var buf = buffer.from(str); console.log(str.length);//2 console.log(buf.length);//6 console.log(buf.bytelength);//6
buffer.compare(buf1, buf2)
该方法用于比较 buf1 和 buf2 ,通常用于 buffer 实例数组的排序。 相当于调用 buf1.compare(buf2)
buf1 <buffer>
buf2 <buffer>
returns: <integer>
var buf1 = buffer.from('1234'); var buf2 = buffer.from('0123'); var arr = [buf1, buf2]; var result = buffer.compare(buf1,buf2); console.log(result);//1 console.log(arr.sort());//[ <buffer 30 31 32 33>, <buffer 31 32 33 34> ]
buffer.concat(list[, totallength])
该方法返回一个合并了 list 中所有 buffer 实例的新建的 buffer
list <array> 要合并的 buffer 实例的数组
totallength <integer> 合并时 list 中 buffer 实例的总长度
返回: <buffer>
如果 list 中没有元素、或 totallength 为 0 ,则返回一个新建的长度为 0 的 buffer 。如果没有提供 totallength ,则从 list 中的 buffer 实例计算得到。 为了计算 totallength 会导致需要执行额外的循环,所以提供明确的长度会运行更快
var buf1 = buffer.alloc(10); var buf2 = buffer.alloc(14); var buf3 = buffer.alloc(18); var totallength = buf1.length + buf2.length + buf3.length; console.log(totallength);//42 var bufa = buffer.concat([buf1, buf2, buf3], totallength); console.log(bufa);//<buffer 00 00 00 00 ...> console.log(bufa.length);//42
buffer.isbuffer(obj)
如果 obj 是一个 buffer 则返回 true ,否则返回 false
var buf = buffer.alloc(5); var str = 'test'; console.log(buffer.isbuffer(buf));//true console.log(buffer.isbuffer(str));//false
实例方法
buf.slice([start[, end]])
该方法返回一个指向相同原始内存的新建的 buffer,但做了偏移且通过 start 和 end 索引进行裁剪
start <integer> 新建的 buffer 开始的位置。 默认: 0
end <integer> 新建的 buffer 结束的位置(不包含)。 默认: buf.length
返回: <buffer>
var buffer1 =buffer.from('test'); console.log(buffer1);//<buffer 74 65 73 74> var buffer2 = buffer1.slice(1,3); console.log(buffer2);//<buffer 65 73> console.log(buffer2.tostring());//'es'
[注意]修改这个新建的 buffer 切片,也会同时修改原始的 buffer 的内存,因为这两个对象所分配的内存是重叠的
var buffer1 =buffer.from('test'); console.log(buffer1);//<buffer 74 65 73 74> var buffer2 = buffer1.slice(1,3); console.log(buffer2);//<buffer 65 73> buffer2[0] = 0; console.log(buffer1);//<buffer 74 00 73 74> console.log(buffer2);//<buffer 00 73>
buf.copy(target[, targetstart[, sourcestart[, sourceend]]])
该方法用于拷贝 buf 的一个区域的数据到 target 的一个区域,即便 target 的内存区域与 buf 的重叠
target <buffer> | <uint8array> 要拷贝进的 buffer 或 uint8array
targetstart <integer> target 中开始拷贝进的偏移量。 默认: 0
sourcestart <integer> buf 中开始拷贝的偏移量。 当 targetstart 为 undefined 时忽略。 默认: 0
sourceend <integer> buf 中结束拷贝的偏移量(不包含)。 当 sourcestart 为 undefined 时忽略。 默认: buf.length
返回: <integer> 被拷贝的字节数
var buffer1 =buffer.from('test'); var buffer2 = buffer.alloc(5); var len = buffer1.copy(buffer2,1,3); console.log(buffer1);//<buffer 74 65 73 74> console.log(buffer2);//<buffer 00 74 00 00 00> console.log(len);//1
buf.compare(target[, targetstart[, targetend[, sourcestart[, sourceend]]]])
该方法比较 buf 与 target,返回表明 buf 在排序上是否排在 target 之前、或之后、或相同。 对比是基于各自 buffer 实际的字节序列
target <buffer> 要比较的 buffer
targetstart <integer> target 中开始对比的偏移量。 默认: 0
targetend <integer> target 中结束对比的偏移量(不包含)。 当 targetstart 为 undefined 时忽略。 默认: target.length
sourcestart <integer> buf 中开始对比的偏移量。 当 targetstart 为 undefined 时忽略。 默认: 0
sourceend <integer> buf 中结束对比的偏移量(不包含)。 当 targetstart 为 undefined 时忽略。 默认: buf.length
返回: <integer>
如果 target 与 buf 相同,则返回 0
如果 target 排在 buf 前面,则返回 1
如果 target 排在 buf 后面,则返回 -1
var buf1 = buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9]); var buf2 = buffer.from([5, 6, 7, 8, 9, 1, 2, 3, 4]); // 输出: 0(buf2中的1234对比buf2中的1234) console.log(buf1.compare(buf2, 5, 9, 0, 4)); // 输出: -1(buf2中的567891对比buf1中的56789) console.log(buf1.compare(buf2, 0, 6, 4)); // 输出: 1(buf2中的1对比buf2中的6789) console.log(buf1.compare(buf2, 5, 6, 5));
buf.equals(otherbuffer)
如果 buf 与 otherbuffer 具有完全相同的字节,则返回 true,否则返回 false
otherbuffer <buffer> 要比较的 buffer
返回: <boolean>
var buf1 = buffer.from('abc'); var buf2 = buffer.from('abc'); var buf3 = buffer.from('abc'); console.log(buf1.equals(buf2));//true console.log(buf1.equals(buf3));//false
buf.fill(value[, offset[, end]][, encoding])
value <string> | <buffer> | <integer> 用来填充 buf 的值
offset <integer> 开始填充 buf 的位置。默认: 0
end <integer> 结束填充 buf 的位置(不包含)。默认: buf.length
encoding <string> 如果 value 是一个字符串,则这是它的字符编码。 默认: 'utf8'
返回: <buffer> buf 的引用
如果未指定 offset 和 end,则填充整个 buf。 这个简化使得一个buffer的创建与填充可以在一行内完成
var b = buffer.allocunsafe(10).fill('h'); console.log(b.tostring());//hhhhhhhhhh
buf.indexof(value[, byteoffset][, encoding])
value <string> | <buffer> | <integer> 要搜索的值
byteoffset <integer> buf 中开始搜索的位置。默认: 0
encoding <string> 如果 value 是一个字符串,则这是它的字符编码。 默认: 'utf8'
返回: <integer> buf 中 value 首次出现的索引,如果 buf 没包含 value 则返回 -1
如果value是字符串,则 value 根据 encoding 的字符编码进行解析;如果value是buffer,则value会被作为一个整体使用。如果要比较部分 buffer 可使用 buf.slice();如果value是数值,则 value 会解析为一个 0 至 255 之间的无符号八位整数值
var buf = buffer.from('this is a buffer'); // 输出: 0 console.log(buf.indexof('this')); // 输出: 2 console.log(buf.indexof('is')); // 输出: 8 console.log(buf.indexof(buffer.from('a buffer'))); // 输出: 8 // (97 是 'a' 的十进制 ascii 值) console.log(buf.indexof(97)); // 输出: -1 console.log(buf.indexof(buffer.from('a buffer example'))); // 输出: 8 console.log(buf.indexof(buffer.from('a buffer example').slice(0, 8)));
buf.lastindexof(value[, byteoffset][, encoding])
与 buf.indexof() 类似,除了 buf 是从后往前搜索而不是从前往后
var buf = buffer.from('this buffer is a buffer'); // 输出: 0 console.log(buf.lastindexof('this')); // 输出: 17 console.log(buf.lastindexof('buffer')); // 输出: 17 console.log(buf.lastindexof(buffer.from('buffer'))); // 输出: 15 // (97 是 'a' 的十进制 ascii 值) console.log(buf.lastindexof(97)); // 输出: -1 console.log(buf.lastindexof(buffer.from('yolo'))); // 输出: 5 console.log(buf.lastindexof('buffer', 5)); // 输出: -1 console.log(buf.lastindexof('buffer', 4));
buf.includes(value[, byteoffset][, encoding])
该方法相当于 buf.indexof() !== -1
value <string> | <buffer> | <integer> 要搜索的值
byteoffset <integer> buf 中开始搜索的位置。默认: 0
encoding <string> 如果 value 是一个字符串,则这是它的字符编码。 默认: 'utf8'
返回: <boolean> 如果 buf 找到 value,则返回 true,否则返回 false
var buf = buffer.from('this is a buffer'); // 输出: true console.log(buf.includes('this')); // 输出: true console.log(buf.includes('is')); // 输出: true console.log(buf.includes(buffer.from('a buffer'))); // 输出: true // (97 是 'a' 的十进制 ascii 值) console.log(buf.includes(97)); // 输出: false console.log(buf.includes(buffer.from('a buffer example'))); // 输出: true console.log(buf.includes(buffer.from('a buffer example').slice(0, 8))); // 输出: false