写点什么

深入浅出 Node.js(六):Buffer 那些事儿

  • 2012-04-16
  • 本文字数:2962 字

    阅读完需:约 10 分钟

作为前端的 JSer,是一件非常幸福的事情,因为在字符串上从来没有出现过任何纠结的问题。我们来看看 PHP 对字符串长度的判断结果:

复制代码
<? php
echo strlen("0123456789");
echo strlen(" 零一二三四五六七八九 ");
echo mb_strlen(" 零一二三四五六七八九 ", "utf-8");
echo "\n";

以上三行判断分别返回 10、30、10。对于中国人而言,strlen 这个方法对于 Unicode 的判断结果是非常让人疑惑。而看看 JavaScript 中对字符串长度的判断,就知道这个 length 属性对调用者而言是多么友好。

复制代码
console.log("0123456789".length); // 10
console.log(" 零一二三四五六七八九 ".length); /10
console.log("\u00bd".length); // 1

尽管在计算机内部,一个中文字和一个英文字占用的字节位数是不同的,但对于用户而言,它们拥有相同的长度。我认为这是 JavaScript 中 String 处理得精彩的一个点。正是由于这个原因,所有的数据从后端传输到前端被调用时,都是这般友好的字符串。所以对于前端工程师而言,他们是没有字 符串 Buffer 的概念的。如果你是一名前端工程师,那么从此在与 Node.js 打交道的过程中,一定要小心 Buffer 啦,因为它比传统的 String 要调皮一点。

你该小心 Buffer 啦

像许多计算机的技术一样,都是从国外传播过来的。那些以英文作为母语的传道者们应该没有考虑过英文以外的使用者,所以你有可能看到如下这样一段代码在向你描述如何在 data 事件中连接字符串。

复制代码
var fs = require('fs');
var rs = fs.createReadStream('testdata.md');
var data = '';
rs.on("data", function (trunk){
data += trunk;
});
rs.on("end", function () {
console.log(data);
});

如果这个文件读取流读取的是一个纯英文的文件,这段代码是能够正常输出的。但是如果我们再改变一下条件,将每次读取的 buffer 大小变成一个奇数,以模拟一个字符被分配在两个 trunk 中的场景。

复制代码
var rs = fs.createReadStream('testdata.md', {bufferSize: 11});

我们将会得到以下这样的乱码输出:

复制代码
事件循���和请求���象构成了 Node.js���异步 I/O 模型的���个基本���素,这也是典���的消费���生产者场景。

造成这个问题的根源在于 data += trunk 语句里隐藏的错误,在默认的情况下,trunk 是一个 Buffer 对象。这句话的实质是隐藏了 toString 的变换的:

复制代码
data = data.toString() + trunk.toString();

由于汉字不是用一个字节来存储的,导致有被截破的汉字的存在,于是出现乱码。解决这个问题有一个简单的方案,是设置编码集:

复制代码
var rs = fs.createReadStream('testdata.md', {encoding: 'utf-8', bufferSize: 11});

这将得到一个正常的字符串响应:

复制代码
事件循环和请求对象构成了 Node.js 的异步 I/O 模型的两个基本元素,这也是典型的消费者生产者场景。

遗憾的是目前 Node.js 仅支持 hex、utf8、ascii、binary、base64、ucs2 几种编码的转换。对于那些因为历史遗留问题依旧还生存着的 GBK,GB2312 等编码,该方法是无能为力的。

有趣的 string_decoder

在这个例子中,如果仔细观察,会发现一件有趣的事情发生在设置编码集之后。我们提到 data += trunk 等价于 data = data.toString() + trunk.toString()。通过以下的代码可以测试到一个汉字占用三个字节,而我们按 11 个字节来截取 trunk 的话,依旧会存在一个汉字被分割在两个 trunk 中的情景。

复制代码
console.log(" 事件循环和请求对象 ".length);
console.log(new Buffer(" 事件循环和请求对象 ").length);

按照猜想的 toString() 方式,应该返回的是事件循 xxx 和请求 xxx 象才对,其中“环”字应该变成乱码才对,但是在设置了 encoding(默认的 utf8)之后,结果却正常显示了,这个结果十分有趣。

在好奇心的驱使下可以探查到 data 事件调用了 string_decoder 来进行编码补足的行为。通过 string_decoder 对象输出第一个截取 Buffer(事件循 xx) 时,只返回事件循这个字符串,保留 xx。第二次通过 string_decoder 对象输出时检测到上次保留的 xx,将上次剩余内容和本次的 Buffer 进行重新拼接输出。于是达到正常输出的目的。

string_decoder,目前在文件流读取和网络流读取中都有应用到,一定程度上避免了粗鲁拼接 trunk 导致的乱码错误。但是,遗憾在于 string_decoder 目前只支持 utf8 编码。它的思路其实还可以扩展到其他编码上,只是最终是否会支持目前尚不可得知。

连接 Buffer 对象的正确方法

那么万能的适应各种编码而且正确的拼接 Buffer 对象的方法是什么呢?我们从 Node.js 在 github 上的源码中找出这样一段正确读取文件,并连接 buffer 对象的方法

复制代码
var buffers = [];
var nread = 0;
readStream.on('data', function (chunk) {
buffers.push(chunk);
nread += chunk.length;
});
readStream.on('end', function () {
var buffer = null;
switch(buffers.length) {
case 0: buffer = new Buffer(0);
break;
case 1: buffer = buffers[0];
break;
default:
buffer = new Buffer(nread);
for (var i = 0, pos = 0, l = buffers.length; i < l; i++) {
var chunk = buffers[i];
chunk.copy(buffer, pos);
pos += chunk.length;
}
break;
}
});

在 end 事件中通过细腻的连接方式,最后拿到理想的 Buffer 对象。这时候无论是在支持的编码之间转换,还是在不支持的编码之间转换(利用 iconv 模块转换),都不会导致乱码。

简化连接 Buffer 对象的过程

上述一大段代码仅只完成了一件事情,就是连接多个 Buffer 对象,而这种场景需求将会在多个地方发生,所以,采用一种更优雅的方式来完成该过程是必要的。笔者基于以上的代码封装出一个 bufferhelper 模块,用于更简洁地处理 Buffer 对象。可以通过 NPM 进行安装:

复制代码
npm install bufferhelper

下面的例子演示了如何调用这个模块。与传统 data += trunk 之间只是 bufferHelper.concat(chunk) 的差别,既避免了错误的出现,又使得代码可以得到简化而有效地编写。

复制代码
var http = require('http');
var BufferHelper = require('bufferhelper');
http.createServer(function (request, response) {
var bufferHelper = new BufferHelper();
request.on("data", function (chunk) {
bufferHelper.concat(chunk);
});
request.on('end', function () {
var html = bufferHelper.toBuffer().toString();
response.writeHead(200);
response.end(html);
});
}).listen(8001);

所以关于 Buffer 对象的操作的最佳实践是:

  • 保持编码不变,以利于后续编码转换
  • 使用封装方法达到简洁代码的目的

参考

关于作者

田永强,新浪微博 @朴灵,前端工程师,曾就职于 SAP,现就职于淘宝,花名朴灵,致力于 NodeJS 和 Mobile Web App 方面的研发工作。双修前后端 JavaScript,寄望将 NodeJS 引荐给更多的工程师。兴趣:读万卷书,行万里路。个人 Github 地 址: http://github.com/JacksonTian。


感谢崔康对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2012-04-16 00:0037759

评论

发布
暂无评论
发现更多内容

2021年抓住金三银四涨薪好时机,借花献佛

Java 面试 后端

2021年Java者未来的出路在哪里,Java开发校招面试题

Java 面试 后端

阿里巴巴首发:Java核心框架指导手册,1小时点击量破千万!

Java 程序员 架构 面试 计算机

如何画UML,几种简单的模型分析

编程 架构 面试 后端

一个简单的产品分析模型

石云升

产品经理 产品思维 9月日更 产品分析

2021年你与字节跳动只差这份笔记,大神码了2000页Spring全家桶笔记

Java 面试 后端

2021年冲刺年薪40w,Java从基础到高级知识点汇总

Java 面试 后端

2021年哔哩哔哩Java高级面试题及答案,Java实战视频下载

Java 面试 后端

IT运维和自动化运维以及运维开发有啥不同?能解释下吗?

行云管家

互联网 运维 IT运维 自动化运维 云运维

谁是中国最受赞赏的创投机构?

创业邦

2021年互联网大厂Java笔经,Java程序员如何有效提升学习效率

Java 面试 后端

2021年互联网大厂Java笔经,Java自学宝典电子书下载

Java 面试 后端

2021年Java面经分享,程序员必备技能:时间复杂度与空间复杂度的计算

Java 面试 后端

【等保知识】十个等保常见问题解答汇总

行云管家

网络安全 信息安全 等级保护 过等保 数据审计

2021年一起努力应对互联网寒冬吧,字节跳动Java高级工程师

Java 面试 后端

设计 | ClickHouse 分布式表实现数据同步

RadonDB

数据库 Clickhouse

2021年字节跳动、阿里等大厂最全Java面试题,1个月学会Java开发

Java 面试 后端

代码检查规则背景及总体介绍

百度开发者中心

最佳实践 代码规则

2021年Java高级面试题总结,2021最新大厂高频微服务面试总结

Java 面试 后端

2021年互联网大厂Java笔经,Java重点知识大全

Java 面试 后端

2021年大厂Java高级面试题分享,程序员Java基础入门

Java 面试 后端

2021年华为Java面试真题解析,大厂面试必问

Java 面试 后端

2021年哔哩哔哩Java高级面试题及答案,大牛手把手教你

Java 面试 后端

好看视频Android重构——围绕于播放器的重构实践

百度Geek说

性能优化 大前端 好看视频 播放器

2021年Java者未来的出路在哪里,让人抓狂的Nginx性能调优

Java 面试 后端

2021年Java面经分享,别再说你不会JVM性能监控和调优了

Java 面试 后端

2021年一起努力应对互联网寒冬吧,Java程序员面试笔试宝典答案

Java 面试 后端

Android音频架构| 社区征文

轻口味

android 音视频 新春征文

2021年Java面试心得,整理出这份8万字Java性能优化实战解析

Java 面试 后端

2021年京东Java岗面试必问,我在华为做Java外包的真实经历

Java 面试 后端

2021年抓住金三银四涨薪好时机,腾讯Java社招面试流程

Java 面试 后端

深入浅出Node.js(六):Buffer那些事儿_架构/框架_田永强_InfoQ精选文章