Buffer与中文乱码问题

Posted by Ray on 2018-02-02

Buffer

Buffer 对象类似于数组,每一个元素都是16进制的两位数,即每一个元素可以表示一个 0-255 的值。在不同的编码下,字符串的每一个字符占用的元素个数不相同,在 UTF-8 编码下,每一个中文字占 3 个元素,字母和半角标点符号占1个元素。

Buffer 的实例可以先指定大小后赋值,也可以像数组那样使用 index 访问该值,返回一个十进制的数字。

1
2
3
4
5
6
7
8
let str = 'ray is handsome'

let buf = new Buffer(str,'utf-8')
console.log(buf);//<Buffer 72 61 79 20 69 73 20 68 61 6e 64 73 6f 6d 65>
console.log(str.length);//15
console.log(buf.length);//15

console.log(buf[10]);//100 对应的16进制即第九位上的 64

此外如果对 buf 某一位赋值不是在0-255之间的数字,那么会根据大小,依次 加/减 256 直到满足是在0-255区间,如果赋值是小数,那么取整。

1
2
3
4
5
6
7
8
9
10
let buf2 = new Buffer(100)

buf2[10] = -100
console.log(buf2[10]);//156

buf2[11] = 1000
console.log(buf2[11]);//232

buf2[12] = 3.14
console.log(buf2[12]);//3

一个buffer可以和字符串相互转化,如果没有指定编码,默认编码规则为‘utf-8’。

1
2
let buf3 = new Buffer('ray')//let buf = new Buffer(str,[encoding])
console.log(buf3.toString());

此外,可以指定buffer的不同段使用不同的编码方式,但是需要注意的是每种编码所用的buffer字节数不一致,且在转回字符串是要分别对每一段指定不同的编码

Buffer 支持的编码格式有限(不支持GBK):

  • UTF-8
  • ASCII
  • Base64
  • Binary
  • Hex
  • UTF-16LE

Buffer 转中文出现乱码的问题

前面提到过,中文在 utf-8 编码之下是三个buffer元素表示一个中文字,在一个读取流操作中,Buffer是被分成一段一段进行传输和读取的,有可能在一个很大的文字文件中会出现一种情况,就是被截取传输的一段 <Buffer >不能够被 3 整除那么就会出现中文乱码的问题了

1
2
3
4
5
6
7
8
9
10
11
12
const fs = require('fs')

let rs = fs.createReadStream('./test.md',{highWaterMark:4})
let data = ''
rs.on('data',(chunk) => {
console.log(chunk)
data += chunk
})

rs.on('end',(params) => {
console.log(data);
})

打印结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
<Buffer e8 8b 9f e5>
<Buffer 88 a9 e5 9b>
<Buffer bd e5 ae b6>
<Buffer e7 94 9f e6>
<Buffer ad bb e4 bb>
<Buffer a5 2c e5 b2>
<Buffer 82 e5 9b a0>
<Buffer e7 a5 b8 e7>
<Buffer a6 8f e9 81>
<Buffer bf e8 b6 8b>
<Buffer e4 b9 8b 21>
苟�����家生�����,��因祸�����趋之!

在创建可读流的时候通过传入选项 highWaterMark 限定一个chunk 四个buffer字节的方式模拟大文件有可能的截取的不是3的倍数,可见出现了 乱码。

Tips :这个highWaterMark的其实是很有讲究的,node默认是 64KB。首先应该说明的是 fs.createReadStream()的工作方式:创建可读流内部是在内存中先准备一段 Buffer 然后通过执行 fs.read()进行系统调用读取字节之后复制进准备好的Buffer对象中,Buffer再进行分割成一个一个小的chunk,触发“data”事件。那个在内存中先准备的Buffer就是和highWaterMark的设置有很大的关系,读取的数据比准备好的 Buffer 大,那就要再一次分配一个新的 Buffer ,并且触发“data”事件。所以把highWaterMark设置的太小就会相应的影响读取速度。

应对策略

  1. 创建可读流的时候通过设置编码,让可读流内部调用,decoder 对buffer 先进行解码,之后在监听 data 事件里的chunk 是已经解码好的字符串,decoder 设置了编码规则之后就会知道对于 utf-8编码规则下,是三个字组成一个中文,在不能构成3的倍数的时候,会把字节顺延至下一个buffer对象,拼接之后达到满足条件的情况之后再传给 chunk
1
2
3
4
5
6
7
8
9
10
11
12
13
const fs = require('fs')

let rs = fs.createReadStream('./test.md',{highWaterMark:4})
let data = ''
rs.setEncoding('utf-8')
rs.on('data',(chunk) => {
console.log(chunk);
data += chunk
})

rs.on('end',(params) => {
console.log(data);
})

打印结果:

1
2
3
4
5
6
7
8
9
10
11
12


国家


以,
岂因


避趋
之!
苟利国家生死以,岂因祸福避趋之!

总的触发 data 事件的次数不变,但是内部调用了 decoder 之后就会更加的 “智能” 一点。

  1. Buffer.concat(list[, totalLength])

Buffer 类提供了一个方法,用来拼接几个 buffer 实例,其中 list是几个buffer实例组成的数组, totalLength 是可选参数,表示数组中每个buffer实例的总和,虽然是可选参数,但是如果不传的话,concat 内部实现时会再一次循环每一个buffer实例计算总长度,所以能传还是尽量传。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const fs = require('fs')

let chunks = []
let totalLength = 0
let rs = fs.createReadStream('./test.md',{highWaterMark:4})

rs.on('data',(chunk) => {
chunks.push(chunk)
totalLength += chunk.length
})
rs.on('end',() => {
let str = Buffer.concat(chunks,totalLength).toString()
console.log(str);
})

用这种拼接buffer实例之后,再进行解码,避免了直接凭借字符串时,一个字被截断的现象。

后记

在《深入浅出Nodejs》中Buffer章节做了一个测试,在网络IO中直接传字符串和转化成Buffer之后再传的压力测试做比较,测试结果表明把那些不需要改动的静态文件转化成 Buffer 的形式传输能够明显提升性能。

参考:

NodeJs V8.9.4官方文档
《深入浅出NodeJS》–朴灵

本文为原创文章作为学习交流笔记,如有错误请您评论指教
转载请注明来源:https://isliulei.com/article/node-Buffer/