写点什么

深入浅出 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:0038163

评论

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

私有云统一管理定义以及好处简单说明

行云管家

云计算 私有云 云管理

龙蜥RISC-V SIG 2.0研讨会圆满举办,宋卓当选国际基金会Datacenter SIG主席

OpenAnolis小助手

AI 操作系统 高性能计算 龙蜥社区 OpenAnolis

全双工分轨语音数据集:让AI实现无缝对话

数据堂

人工智能 语音识别 语音交互 语音数据集 全双工分轨语音数据集

悖论:智驾难题为何在此刻无解?

脑洞汽车

AI

【轻量化】三个经典轻量化网络解读

地平线开发者

自动驾驶; 算法工具链 地平线征程6

Web3项目开发的测试

北京木奇移动技术有限公司

区块链技术 软件外包公司 web3开发

Metasploit Pro 4.22.7-2025040601 (Linux, Windows) - 专业渗透测试框架

sysin

Metasploit

Studio 3T 2025.6 (macOS, Linux, Windows) - MongoDB 的终极 GUI、IDE 和 客户端

sysin

mongodb

Amazon 最新语音模型 Nova Sonic:比 4o 便宜 80%,智能停顿和打断;a16z 发布 AI 数字人报告丨日报

声网

阿里巴巴 Druid 可观测性最佳实践

观测云

Druid

Spring项目开发的智能助手:通义灵码使用指南

阿里云云效

云计算 spring

CAD如何炸开参照图形

极客天地

CAD把PDF图纸插入为光栅图像

极客天地

Dinky 和 Flink CDC 在实时整库同步的探索之路

Apache Flink

大数据 flink 实时计算

Paragon NTFS与Tuxera NTFS有何区别 Mac NTFS 磁盘读写工具选哪个好

阿拉灯神丁

Mac 软件 Paragon NTFS NTFS 磁盘管理器 Tuxera NTFS2024 磁盘格式读写软件

龙蜥社区荣获 OS2ATC 2025 “最具影响力开源创新贡献奖”

OpenAnolis小助手

开源 操作系统 龙蜥社区 OpenAnolis

龙蜥社区两大委员会月度会议圆满召开

OpenAnolis小助手

开源 操作系统 龙蜥社区

2025慕尼黑上海电子展揭秘汽车电子新动能:智能电动化浪潮下,汽车半导体如何赋能行业未来发展?

极客天地

是时候重新审视“小米模式”了

脑洞汽车

智能电车

鸿蒙智行生态联盟爆款频出,四大车企差异化布局显成效

最新动态

休克的哪吒:一家新势力车企的失血日志

脑洞汽车

AI

运维堡垒机-开启IT安全运维利器!

行云管家

网络安全 堡垒机 数字安全

Metasploit Framework 6.4.55 (macOS, Linux, Windows) - 开源渗透测试框架

sysin

Metasploit

亮相2025全球分布式云大会,火山引擎边缘云落地AI新场景

火山引擎边缘云

边缘计算 IoT Edge AI 大底座 AI‘’ 边缘智能

Web3项目的开发

北京木奇移动技术有限公司

区块链技术 web3开发 软件外包开公司

​​AMS行政管理系统:数字化赋能人力资源精益管理​

秃头小帅oi

Invicti v25.4.0 发布,新增功能概览

sysin

invicti

Cloud Kernel SIG 季度动态:发布ANCK 6.6-003版本,支持一测多证

OpenAnolis小助手

操作系统 龙蜥社区 OpenAnolis 龙蜥社区SIG

基于Raft协议 + gRPC长连接实现集群间的服务发现、服务注册、元数据共享、元数据持久化

路 飞

CAD粘贴表格显示#怎么办

极客天地

2025慕尼黑上海电子展揭示技术密码:机器人行业蓬勃发展,半导体“芯脏”如何给予支撑?

极客天地

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