UDN-企业互联网技术人气社区

板块导航

浏览  : 991
回复  : 2

[Nodejs] 理解nodejs中的Buffer

[复制链接]
独领风骚的头像 楼主
发表于 2017-2-4 10:24:13 | 显示全部楼层 |阅读模式
  nodejs的优势在于编写高性能的网络服务,而网络请求中,Stream和Buffer是其基础,因此理解这两个概念至关重要。

  而Buffer又是Stream的基础,所以,先来看看Buffer吧。然后再去搞Steam。

  Buffer是什么

  Buffer是一个类数组对象,里面存储的是字节,有点类似于字节数组,主要用于操作字节的。

  来个例子:
  1. let str = 'Hello World!';
  2. let buffer = Buffer.from(str);
  3. console.log(buffer);
复制代码

  将输出 <Buffer 48 65 6c 6c 6f 20 57 6f 72 6c 64 21>

  我们可以看出其是十六进制的字节编码。Buffer.from(string[,encoding])方法接收两个参数,第二个是编码方法,如果不传,默认将使用UTF-8编码。

  要想了解更多关于编码的内容,推荐阮老师的文章: 《字符编码笔记:ASCII,Unicode和UTF-8》

  为什么说Buffer是一个类数组对象呢?

  因为Buffer有几个和数组相似的属性和方法,比如length属性,indexOf()方法,includes()方法等等。最重要的是它有一个length属性,以及可以直接使用下标访问,例如:
  1. console.log(buffer.length); //12
  2. console.log(buffer[0]);     //72
复制代码

  上面例子中,buffer的长度是12,有12个字节。位置0的字节值为72,对应16进制的 48 。

  Buffer中的每一个元素(Buffer不是数组,因此严格意义上说,‘每一个元素’这种描述是错的,这里方便描述)都是一个16进制的二位数,因此它的大小在0~255之间。

  为什么呢?刚才说过了Buffer存的是字节,一个字节是由8个位组成的,因此它的大小是0~255之前,而使用16进制表示出来就是2位。

  nodejs的Buffer有丰富的接口供使用,例如分配空间,填充Buffer,赋值等等,具体可参阅 官方文档 。

  Buffer的每个元素都是0~255的整数,那么如果赋值给小数甚至负数会怎样呢?例如:
  1. let bf2 = Buffer.alloc(2);
  2. bf2[0] = 72.5;  
  3. bf2[1] = -72;
  4. console.log(bf2); //<Buffer 48 b8>
复制代码

  它的两个元素分别是 0x48 和 0xb8 ,对应十进制是 72,184。可以看出,如果赋值小数,则直接截断为整数,负数的话,184和-72啥关系呢?哦,-72+256=184。

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

  Buffer与字符串的转换

  字符串可以通过Buffer.from(string[,encoding])来转换为Buffer,如果不指定encoding,默认使用utf-8编码。

  目前支持的编码有如下几种:

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

  由于nodejs内置的转换编码并不支持GBK,因此如果要处理编码为GBK的文档,要借助第三方的插件,推荐 iconv-lite 。
  1. let str = 'Hello,你好';

  2. console.log(Buffer.from(str,'utf-8'));
  3. console.log(Buffer.from(str,'ascii'));
  4. console.log(Buffer.from(str,'utf-16le'));
  5. console.log(Buffer.from(str,'base64'));
  6. console.log(Buffer.from(str,'binary'));
  7. console.log(Buffer.from(str,'hex'));

  8. //<Buffer 48 65 6c 6c 6f 2c e4 bd a0 e5 a5 bd>
  9. //<Buffer 48 65 6c 6c 6f 2c 60 7d>
  10. //<Buffer 48 00 65 00 6c 00 6c 00 6f 00 2c 00 60 4f 7d 59>
  11. //<Buffer 1d e9 65 a3>
  12. //<Buffer 48 65 6c 6c 6f 2c 60 7d>
  13. //<Buffer >
复制代码

  注意最后一个,hex出来怎么是空呢?因为hex只支持十六进制的字符串。
  1. console.log(Buffer.from('48656c6c6f2ce4bda0e5a5bd','hex').toString('utf-8')); //Hello,你好
复制代码

  Buffer的拼接

  上面例子我们看出了,字符串和Buffer的转换和编码息息相关。而且即使是相同编码,如果Buffer被截断,那么也有可能出现乱码。
  1. let str = 'Hello,你好';
  2. let bf = Buffer.from(str);
  3. console.log(bf.slice(0,7).toString());  //Hello,��
复制代码

  从上面例子中我们看出,’你好’两个汉字,分别占用3个字节,这里我取bf的前8个字节,很明显是将’你’字的3个字节给分开了,只取了前2个字节,那么,在转换为字符串时,由于不能正确识别出现乱码。

  我们在做网络请求或者使用流读取文件时,由于可能会读取多次,不可避免的出现这种情况,一个汉字的字节被截断,导出出现乱码:
  1. 'use strict';
  2. const fs = require('fs');

  3. //test.txt
  4. //Hello,你好

  5. let rs = fs.createReadStream('./test.txt', {
  6.     highWaterMark: 7
  7. });
  8. let txt = '';
  9. rs.on('data',(chunk) => {
  10.     txt += chunk;
  11. });

  12. rs.on('end',() => {
  13.     console.log(txt);
  14. });

  15. //Hello,���好
复制代码

  这里出现乱码了,是因为 txt += chunk 隐含了一个操作,即 chunk.toString() ,因为txt是String类型的。因此相当于是 txt += chunk.toString() 。

  由于每次限定只读取7个字节,因此’你’字被截断,解析时成乱码。

  使用http发送网络请求时,也是同样的原理。

  不要紧,这里我们可以直接将chunk拼接成一个大的Buffer,然后再转换成字符串。
  1. let bfs = [];
  2. rs.on('data',chunk => {
  3.     bfs.push(chunk);
  4. });
  5. rs.on('end',() => {
  6.     console.log(bfs.toString());
  7. });
复制代码

  如果是网络请求这样做还好,因为每次请求的数据不会太大,不至于出现内存不够的情况。但是如果是读取一个大的文件,比如几百M或者几个G的情况下,很明显不能这样拼接了,因为内存可能不够。

  例如,有一个很大的纯文本文件,utf-8编码,如何正确读取其内容然后显示在终端?

  这个问题只能分段读取,然后分段显示。那么,问题又来了,上面的乱码问题该如何解决?

  nodejs有一个神奇的string_decoder模块,神奇在哪,来个实验:
  1. const StringDecoder = require('string_decoder').StringDecoder;
  2. const decoder = new StringDecoder('utf-8');
  3. let str = 'Hello,你好';
  4. let bf = Buffer.from(str);
  5. let bf1 = bf.slice(0,7);
  6. let bf2 = bf.slice(7,bf.length);

  7. console.log(bf1.toString());
  8. console.log(bf2.toString());
  9. console.log(decoder.write(bf1));
  10. console.log(decoder.write(bf2));

  11. //Hello,�
  12. //��好
  13. //Hello,
  14. //你好
复制代码

  bf1和bf2分别是bf的前后两段,但是直接toString输出乱码,使用string_decoder输出则正常?何解?

  将Buffer传给StringDecoder解析写入时,前面 Hello, 能够正常解析,最后一个字节不能解析,留在decoder里,第二次解析bf2时,会和bf2拼接起来一起解析,因此’你’在第二次解析中输出。

  但是string_decoder有个问题,只能处理utf-8,base64,和UCS-2/UTF-16LE三种编码。对于其他编码无能为力。因此,如果是其他编码的,不能直接使用它。

  对于GBK编码的文件或者网络请求,我们该如何处理呢?上面提到一个第三方的转换插件iconv-lite模块,我们可以使用这个模块进行转换。

  例如,再有一个很大的文本文件,编码是GBK的,如何正确读取并显示在终端?
  1. 'use strict';
  2. const fs = require('fs');
  3. const iconv = require('iconv-lite');

  4. let rs = fs.createReadStream('./中文测试.md', {
  5.     highWaterMark: 7
  6. });
  7. let StringDecoder = require('string_decoder').StringDecoder;
  8. let decoder = new StringDecoder('utf-8');

  9. let readable = rs.pipe(iconv.decodeStream('GBK'))
  10.     .pipe(iconv.encodeStream('utf-8'));

  11. readable.on('readable', () => {
  12.   var chunk;
  13.   while (null !== (chunk = readable.read())) {
  14.     console.log(decoder.write(chunk));
  15.   }
  16. });
复制代码

  中文测试.md是一个GBK编码的纯文本文件,我们创建一个读取流rs,然后通过管道将该流扔给iconv,iconv使用GBK编码解码流,并将其转换成utf-8编码的流,最后通过string_decoder输出。

  小结

  对于Buffer的操作,一定要注意几点:

  • 编码一定要统一。不管是网络服务还是文件读取,一定要统一编码。如果编码不统一,则先进行转码。
  • 注意字节完整性。对于多字节的字符,千万不要出现截断字节的情况。否则会乱码。

相关帖子

发表于 2017-2-4 10:24:49 | 显示全部楼层
无论是不是沙发都得回复下
使用道具 举报

回复

发表于 2017-2-4 10:24:50 | 显示全部楼层
有空一起交流一下
使用道具 举报

回复

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部