写点什么

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

评论

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

网站开发进阶(六十二)最强大的 CSS 布局——Grid 布局

No Silver Bullet

页面布局 2月月更 Grid

中国 PostgreSQL 分会加入龙蜥社区,携手共建基础软件开源新生态

OpenAnolis小助手

postgresql Linux 开源

web前端开发Nodejs的C++ 拓展开发_前端培训

@零度

node.js 前端开发

手把手教学电瓶车进电梯检测、多类别车辆追踪、异常行为检测产业级应用

百度大脑

低代码实现探索(三十三)前端脚本公式

零道云-混合式低代码平台

掉入成功的深渊

Shinta

ESLint-源码分析

Tone荣

前端 eslint

java培训:Netty的内存管理

@零度

Java Netty

MongoDB在信息资源共享建设的应用实践

MongoDB中文社区

mongodb

微服务从代码到k8s部署应有尽有系列(三、鉴权)

万俊峰Kevin

微服务 web开发 鉴权 go-zero Go 语言

全面解析湖仓一体与大数据演进历程|内含技术工具选型策略

云智慧AIOps社区

数据库 大数据 数据湖 Clickhouse 大数据运维

堡垒机哪家好?贵不贵?作用是什么?

行云管家

堡垒机 等级保护 过等保 等保2.0

Flink 新一代流计算和容错——阶段总结和展望

Apache Flink

大数据 flink 开源 编程 实时计算

向工程腐化开炮 | manifest治理

阿里巴巴终端技术

App 客户端开发 腐化治理 manifest

外包学生管理系统架构设计文档

Geek_36cc7c

震坤行工业超市研发效能提升之路

阿里云云效

云计算 阿里云 DevOps 云原生 研发

MongoDB 数据实时同步利器-Tapdata Cloud 免费上手指南

MongoDB中文社区

mongodb

容器化 | 在 KubeSphere 中部署 MySQL 集群

RadonDB

MySQL 数据库 高可用 RadonDB KubeSphere

Kaggle冠军解读:风电场短期风况预测任务方案

百度大脑

互联网应用开发如何搭上AI的快车?来厦门开发者Meetup一探究竟

百度大脑

Go学习笔记——复合数据类型

为自己带盐

Go 学习笔记 2月月更

等保2.0政策之物联网安全扩展要求包括哪四个?

行云管家

云计算 物联网 等保 等保2.0 扩展要求

网络标准之:永远是1.0版本的MIME

程序那些事

Java 网络协议 程序那些事 2月月更

如何更好的使用TypeScript

Tone荣

前端 js ts js 转 ts

网易数帆大数据场景下的DataOps实践

网易数帆

大数据 数据治理 DataOps

我提交了一个 pr,竟然是为了吃

AlwaysBeta

GitHub 开源 程序员 生活 程序员人生

StarRocks Contributor 人数破百!极速统一,你我协力!

StarRocks

数据库 数据分析 StarRocks

Flow vs Jenkins 实操对比,如何将Java应用快速发布至ECS

阿里云云效

Java 阿里云 cicd 云原生 ECS

如何应对数千微服务组件带来的挑战?

云智慧AIOps社区

php 架构 微服务 微服务架构 运维

快上车!第十七届全国大学生智能汽车竞赛百度创意组来啦

百度大脑

原生JavaScript灵魂拷问,你能答上多少(一)

战场小包

JavaScript 前端 2月月更

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