Node闲谈之Buffer

在刚接触Nodejs的时候,有些概念总让学前端的我感到困惑(虽然大学的时候也是在搞后端,世界上最好的语言,you know)。我可以很快理解File System,Path等带有明显功能的模块,却一下子不能理解Buffer这个玄而又玄的东西。因为,在前端的js实践中,我很少去考虑什么编码方式,字符集之类的东西。二进制的理解仅限于大学课堂而已。本文与其说是在探讨Node的Buffer模块,倒不如说是来探讨下如何从字符集,编码的角度来理解Buffer这个模块设立的意义。Node闲谈系列不涉及具体的API讲解,只会勾勾画画一些自己认为比较重要的特性。

一、基本知识

正如我们学习编程的第一节课一样,我们明白,计算机就是一个二进制生物。它只能理解1和0,或者说有和没有。“太极生两仪,两仪生四象,四象生八卦”。我们在计算上看到的无论是任何东西,都是通过某种特殊的编码方式,或者说是约定展现出来的。

我们很熟悉的ASCII码就是这样一种规范,和摩尔斯码一样,固定的值表示固定的含义。ASCII码用1个字节8位来表示2^8=256种状态。比如大写字母A对应的是65,B对应66,是不是很简单。ASCII码并不是占满了所有的256个位置,只用到了一半128位。在一个字节中,只占用后面7位,最前面一位统一标识为0。

但是我们很容易就发现,ASCII码远远是不够的,最起码我们汉字就表示不了。后面考虑了许多其他解决方案,我们在此不多叙述,直接说最终解决方案——Unicode。Unicode想法很直接,就是想把全世界所有的字符都囊括进去。我们一般认为Unicode用两个字节16位表示,并且完全囊括了ASCII字符集。比如汉字“好”在unicode里面二进制表示是 0101 1001 0111 1101。将其转换成16进制就是U597D(U只是表示它们是unicode码)。之所以我前面说是“一般认为”,是因为这种想法是不准确的。Unicode一个平面(plane)是两个字节。我们经常谈论的是它的一个基本平面,编码是U+0000到U+FFFF,常见字符都在这个平面。Unicode还有16个辅助平面,码点范围是U+010000一直到U+10FFFF。一般而言,我们只需要把关注点放在基本平面就好,并且要习惯Unicode的表示方式。因为,这是毕竟在各种编码方式间转化的“硬通货”。

我们常常谈到的utf-8,utf-16这些是什么呢?这些都是具体的编码方式,而Unicode是个字符集。以utf-8为例,它在unicode码的基础上,进行重新编码,把一些本身不需要占满2个字节的转化为1个字节。比如ASCII里面的那些字符,在unicode里面,第一个字节全是0,简直是空间的浪费,也会把汉字编码城3个字节。你尽可以在控制台试下Buffer.from('我','utf8')看下编码后占的字节数。

javascript使用哪种编码方式?

javascript采用Unicode字符集,但是只支持一种编码方式。那就是USC-2。是不是没有听说过?你可以把它理解成utf-16。但它和utf-16到底是什么关系呢?

两者的关系简单说,就是UTF-16取代了UCS-2,或者说UCS-2整合进了UTF-16。所以,现在只有UTF-16,没有UCS-2。

UCS-2只支持两个字节,而在它后面才出来的UTF-16在UCS-2的基础上,利用辅助平面可以支持4个字节。既然是UCS-2整合进UTF-16,那就存在有的字符UTF-16有,而UCS-2不存在的情况。出现这种情况怎么办?大家可以参考下参考资料里面阮老师的讲述。

二、Buffer的生成

相关重要的API

  • Buffer.alloc(size[, fill[, encoding]])
  • Buffer.allocUnsafe(size)
  • Buffer.allocUnsafeSlow(size)
  • Buffer.from(array)
  • buf.fill(value[, offset[, end]][, encoding])

在Buffer生成的过程中,最大的关注点就是内存的申请和分配。原先new Buffer()生成Buffer的方法已经不建议再次使用,它和Buffer.allocUnsafe()方法一样,可能包含敏感数据。

为什么会包含敏感数据呢?在生成buffer的过程中,不是一步到位,要分为两步走,1,申请内存空间,2,申请的内存空间进行填充。Buffer.allocUnsafe()方法只完成了第一步。不完成第二步的后果就是,申请的空间可能“残留了”以前内存上的数据。毕竟一块儿内存在计算机中总是申请了再释放,释放了再申请,难免就会导致一些数据没有被及时清理干净。当然,由于少了第二步操作,速度自然快了不少。

const a = Buffer.allocUnsafe(10);
console.log(a)
//<Buffer f0 4e a8 6f 01 02 00 00 00 20> 打印结果总是不一样的,但我们发现每一位上很可能不是00,这些数据就属于敏感数据。

可以使用buf.fill(0)进行后期的填充。但为了避免漏洞产生,应该避免使用Buffer.allocUnsafe()来分配内存。

Buffer.alloc()Buffer.allocUnsafe()安全的原因在于它在第二步会把所有的旧数据清除掉,填充成0。

const a = Buffer.alloc(10);
console.log(a)
//<Buffer 00 00 00 00 00 00 00 00 00 00> 打印结果每一位都是0。

看到这里,你是不是以为Buffer.alloc()Buffer.allocUnsafe()的区别仅限于有没有填充数据?其实并不是的。真正与Buffer.alloc()差别在是否填充数据的是Buffer.allocUnsafeSlow()。原来,使用Buffer.allocUnsafe()分配内存需要借助共享内存池(shared internal memory pool)。而Buffer.alloc()Buffer.allocUnsafeSlow()是直接在内存空间上开辟相应大小的内存空间。

Buffer.allocUnsafe() (与之前的 new Buffer(size) 机制类似)是三者中分配内存速度最快的方式,它采用了共享内存池(shared internal memory pool)这一方式,通过预先分配一定大小的一段内存,从中再向 JavaScript 分配相应大小的片段,避免频繁的向系统申请内存分配,来达到较高的效率。共享内存池的默认值 poolSize 为 8KB(可重新赋值),只有当需要分配的内存小于等于 poolSize 的一半时,Buffer.allocUnsafe() 才会从共享内存池从分配空间。

这里的知识点到为止,详细的探讨以后可以可以考虑专门写一篇来说明,参考资料的内容也是相当不错,建议阅读。

三、Buffer的读取和写入

相关重要的API

  • buf.readInt8(offset[, noAssert])
  • buf.readDoubleBE(offset[, noAssert])
  • buf.readDoubleLE(offset[, noAssert])
  • buf.writeUInt8(value, offset[, noAssert])
  • buf.writeUInt16BE(value, offset[, noAssert])
  • buf.writeUInt16LE(value, offset[, noAssert])
  • ...

buffer只有能够读写,才能够显示其存在的价值。查看Buffer的文档,Buffer的读写方法中有非常多以“BE”和“LE”结尾的方法。他们分别代表什么呢?

大字序和小字序

“BE”表示的是“big endian”大端字节序,而“LE”自然表示“little endian”小端字节序。字节序是干什么的呢?我们在描述一个字符的unicode码的时候,习惯性地从左到右去写。为什么不可以从右右往左写呢?多个字节无论是读取还是写入,总要有一个顺序,这就是“字节序”。大端序就是我们常看到的高位字节在前,低位字节在后,小端序恰好相反。

为什么要区分大端序和小端序呢?不能都统一从一个方向读取,写入么?

计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。
但是,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。

字节序的处理,就是一句话:"只有读取的时候,才必须区分字节序,其他情况都不用考虑。"

好,下面我们举个实际的例子。

var buf = Buffer.from([1,3,5,7]);
//<Buffer 01 03 05 07>

buf.readInt16BE(0)
//259    
从buf中读取16位的整数,所以读取的第一个字符对应的码点是 01 03转化成10进制就是1*16^2+3 = 259。
buf.readInt16LE(0)
//756
// 小字序从右往左读取,第一个字符对应的码点是 03 01 转化成10进制就是3*16^2+1 = 756 

读取和写入是一个相反的过程,道理是一样的。

//官方示例
const buf = Buffer.allocUnsafe(4);

buf.writeUInt8(0x3, 0);
buf.writeUInt8(0x4, 1);
buf.writeUInt8(0x23, 2);
buf.writeUInt8(0x42, 3);

// Prints: <Buffer 03 04 23 42>
console.log(buf);

Buffer还有很多很有意思的方面需要进一步学习,待以后再进一步补充本文或者写几篇相关更为深入的文章。

未完待续。。。

参考资料

posted @ 2017-11-06 16:19  杰枫Jeff  阅读(727)  评论(0编辑  收藏