也谈 Buffer 拼接——通用的 Buffer 拼接函数
发布于 13 年前 作者 xiongliding 10975 次浏览 最后一次编辑是 8 年前

事情要从一段代码说起

http.get(options, function(res){
  var data = '';
  res.on('data', function(chunk){
      data += chunk;
  });
  res.on('end'), function(){
      ...
  });
})

@jifeng 同学在一个回复里提醒我,“这样遇到中文时可能会遇到问题”,今天认真的看了 @朴灵 写的《小心buffer的拼接问题》,其中提到的 BufferHelper 已经很好的解决了这个问题。

不过这和我第一反应想到的解决方法还是略有不同的,所以我在这里也来谈谈 Buffer 的拼接。

第一反应

我的第一反应是实现一个让 Buffer 对象“相加”的方法。就像这样:

http.get(options, function(res){
  var data = new Buffer(0);
  res.on('data', function(chunk){
      data = concat1(data, chunk);
  });
  res.on('end'), function(){
      ...
  });
});

这么做,就能把拼接的问题留在 res.on('data', ...) 里。 concat1 函数也很简单。

var concat1 = function(buf1, buf2) {
  var tmp = new Buffer(buf1.length + buf2.length);
  buf1.copy(tmp, 0);
  buf2.copy(tmp, buf1.length);
  return tmp;
}

在 chunk 数量较多的情况下,这个方法在性能上应该会略逊于 @朴灵 的 BufferHelper,不过我想理解起来应该会更容易。

进一步扩展

上面也提到了,因为每次只能让两个 Buffer 对象 “相加”,所以对象数量较多时,就需要频繁的构建对象和复制内容,这会让性能下降,所以我本想扩展一下,实现多个 Buffer 相加的功能,不过在写的过程中,我发现比起提高性能,自己更希望让代码保持简单,于是就成了下面这样(你可以结合 @朴灵 的文章修改 concat2 来提高性能)。

下面这个函数实现了任意个 Buffer 相加:

var concat2 = function() {
  var tmp = arguments[0];
  for (var i = 1; i < arguments.length; i++) {
    tmp = concat1(tmp, arguments[i]);
  }
  return tmp;
}

在只有 2 个 Buffer 对象时,concat2 是完全可以代替 concat1 的。

现在要再进一步,实现用数组(其元素都是 Buffer 对象)作为参数:

var concat = function() {
  if (!Buffer.isBuffer(arguments[0])) {
    return concat2.apply(this, arguments[0]);
  }
  return concat2.apply(this, arguments);
}

当第一个参数不是 Buffer 对象时,函数把其认为是一个数组,并通过 apply 方法调用 concat2 来完成拼接。

当然,上面的函数都是没有上保险的,如果你不按预期输入参数,不会得到任何出错提示,只会得到不可预计的结果。

最终代码和简单测试

index.js:

var concat1 = function(buf1, buf2) {
  var tmp = new Buffer(buf1.length + buf2.length);
  buf1.copy(tmp, 0);
  buf2.copy(tmp, buf1.length);
  return tmp;
}

var concat2 = function() {
  var tmp = arguments[0];
  for (var i = 1; i < arguments.length; i++) {
    tmp = concat1(tmp, arguments[i]);
  }
  return tmp;
}

var concat = function() {
  if (!Buffer.isBuffer(arguments[0])) {
    return concat2.apply(this, arguments[0]);
  }
  return concat2.apply(this, arguments);
}

exports.concat1 = concat1;
exports.concat2 = concat2;
exports.concat  = concat;

测试完毕后,只要留下 exports.concat 就够了,正常使用下,功能上它是完全能替代 concat1 和 concat2 的。

test.js:

var bc = require('./index.js');

var buf = [
  new Buffer('ABC'),
  new Buffer('123'),
  new Buffer('DEF')
];

console.log(bc.concat1(buf[0], buf[1]).toString());
console.log('--------');

console.log(bc.concat2(buf[0], buf[1], buf[2]).toString());
console.log('--------');

console.log(bc.concat(buf[0], buf[1], buf[2]).toString());
console.log('--------');

console.log(bc.concat(buf).toString());
console.log('--------');

优化一下

响应一下大家的回复,给出一个优化后的版本:

exports.concat = function() {
  var args = (arguments.length === 1) ? arguments[0] : arguments;

  var i;
  var sumlen = 0;
  for (i = 0; i < args.length; i++) {
    sumlen += args[i].length;
  }

  var buf = new Buffer(sumlen);
  var pos = 0;
  for (i = 0; i < args.length; i++) {
    args[i].copy(buf, pos);
    pos += args[i].length;
  }

  return buf;
};
5 回复

这样似乎内存拷贝太多,可以写个buffer队列管理类,在需要的时候才拼接,也不限于两个拼接。

concat1 每次都需要重新new Buffer ,这样不好啊。

同意myy的说法,循环中不断new buffer是非常不理智的做法, 建议使用队列。 不断new buffer需要注意的是一定不要做太多计算, 太多计算会让进程没有时间GC, 造成内存飙升。 基本这样的进程是memory faults非常高。
Buffer继承于SlowBuffer, SlowBuffer默认会有个大小。

回到顶部