上周同事用我们的工具写了一个关于全国比较出名的景点介绍的APP,地址在这里。整个APP中文字符比较多,文件也比较大,计算了下整个APP描述文件大小在4M左右。整个描述文件要放到第三方CDN。在这中间,我们的服务器主要负责获取前端传上来的数据,做一些加工处理并压缩后抛给第三方CDN服务器。
Node接受POST数据通常的写法:
var load = function(stream, onDone) {
var data = '';
stream.on('data', function(chunk) {
data += chunk;
});
stream.on('end', function() {
onDone(null, data);
});
stream.on('error', function(err) {
onDone(err);
})
};
好像看不出什么问题,我们看到很多英文资料都是这样写的。如果我们始终没有用到宽字符,上面的load操作始终是没问题的。但是如果输入流有宽字符的时候就没那边简单了。
首先看 data += chunk 这句做了什么,当触发了'data'事件的时候,chunk是一个buffer对象。data是一个string字符串所以data += chunk 这句隐式转换为:data = data.toString() + chunk.toString(); 看了下toString的源码,发现默认toString是转换为'utf-8'编码格式的字符串。中文在utf-8的编码方式下占用3个字节,所以如果chunk不足以放下完整的一个宽字符的时候就有可能出现字符被截断的风险。
所以如果你在NODE里面看到有涉及到数据流的乱码,多半是由于这里处理的不好。
问题是找到了,怎么解决呢?上网找了下,主要有两个方案:
1. 让'data'事件传输的不是Buffer对象而是编码后的字符串,即个体stream设置编码:
stream.setEncoding('utf8');
这样在'data'事件接受到的是一个utf8编码的字符串。setEncoing之后可读流变得非常智能,如果遇到宽字符被截断的情况下,它在内部会暂存被截断的宽字符,等待下次事件触发,合并之前暂存的字符,这样就解决了乱码的问题。但是其处理编码方式有限,只能处理Base64/utf-8/utf-16LE/UCS-2.在需要使用其他字符编码的时候就不得不放弃了。
2. 使用第三方模块来完成编解码。
前面提到,出现乱码的原因是由于Buffer缓冲区大小不足,也不够智能。针对这个缺陷,提出第二种解决方案:
var load = function(stream, onDone) {
var buffers = '';
stream.on('data', function(chunk) {
buffers.push(chunk);
});
stream.on('end', function() {
onDone(null, Buffer.concat(buffers));
});
stream.on('error', function(err) {
onDone(err);
})
};
全局的Buffer对象提供了一个concat对象用于把多个Buffer合并为一个大的Buffer对象。这样就解决了缓冲区不足 出现截断的问题。后面怎么解码这个大的Buffer就看具体需求了,由于和Buffer对象之间可以互相转换的编码格式有限。所以:
如果需要的是一个utf8字符串:
buffer.toString('utf8')
如果是需要gbk编码的字符串,借助iconv-lite模块:
var gbkStr = iconv.decode(buffer, 'gbk');
反之,gbk字符串转Buffer:
var buffer = iconv.encode(gbkStr, 'gbk');