把node捡起来之buffer


最近感觉很痛苦,再别人学习新语言新框架的时候,自己却在原地踏步,实在是对自己感到可悲,于是开始着手学习一门后台语言,学习后天语言并不是一时兴起想去学的,而是看现在的趋势,还有是自己的兴趣。
js其实我觉得是一门很友好的语言,在踩过基础坑之后,觉得自己应该取扩展一些强数据类型的语言,一是想多了解,二是可以把自己道路拓宽,于是就在想,我应该适合学什么语言,公司现在后台有用java和php,虽然java是使用率最高的语言而php有一句话不是说么”世界上最好的语言是PHP”,但是也许是个人原因,我对php和java都不会感兴趣,java不感兴趣是因为当初找工作的时候总有java打电话,问有没有兴趣培训java的,所以我觉的会不会学java的人太多了?当然都是我自己偏见,而php呢,是因为身边很多用php的码农,我觉的用php好像很”随便”,这在我的理解就是随便的编码格式,随便的…好吧,其实都是我得偏见,最终选择Go语言是因为我在一年前就对它有一些了解,然后机缘巧合,找到了一些学习资料,然后就开始着手学习起来

…跑题了

为啥又捡起来node了呢,node这个作为js写的”后台”首先js的基础学习成本减少了,而且使用各种包又算式比较得心应手,上周看一篇js爬虫抓取数据的例子,然后自己正好有这个需求,于是就想再重开始把node过一遍。
–当然还有最主要的是我并不满足只满足现在公司的需求“只对str与DOM做一些操作”

buffer


像之前说的,js是很友好的,比如字符串

1
2
console.log("123".length)//3
console.log("你的名字".length)//4

在大多数语言中的汉字都是占3个字节的,而js只占1个

因为在node中,应用需要处理网络协议.操作数据库.处理图片.接收上传文件等.还要处理大量的二进制数据,js中的str不能满足需求,所以就应运而生了Buffer对象;

结构

Buffer是一个像Array结构的对象,它的元素为16进制的两位数(0~255),但是它主要用于操作字节;

Buffer对象

我们可以分配一个Buffer对象,指定它的字节;

1
2
var buf = new Buffer(100)
console.log(buf.length)

当然我们也可以像“数组”一样继续操作它,如:
获取它第10个字节

1
console.log(buf[10])

或者给它第10个字节赋值

1
2
buf[10] = 100
console.log(buf)

这里有一个很重要的一点,当赋值范围不在0~255之间的时候,该值就会逐次加256,直到得到0~255之间的整数,当大于的时候逐次减256,如果是小数舍弃小数部分只保留整数部分

Buffer内存分配机制

Buffer对象的内存分配不是在V8的堆内存中,而是在Node的C++层面实现内存的申请的,也就是C++先去想系统内存申请一些内存给Node用,每次Node需要用的时候再由C++分配给Node,而不是Node每次需要用内存就去找系统要,这样系统压力很大的。(这里废话好像多,可以略过)

总之
1
2
3
4
5
var pool
function allocPool(){
pool = new SlowBuffer(Buffer.poolSize);
pool.used = 0;
}


<-used:08 BK的pool

Node采用的是slab内存分配机制,新建一个局部变量pool,处于分配状态的slab都指向它,slab具有三种状态

  • full:完全分配状态
  • partial:部分分配状态
  • empty:没有分配状态

    1
    var buf = new Buffer(8*1024)

Node以8KB为界限来区分是大对象和小对象(<8KB就按照小对象的方式分配)

  • 小对象:会记录起始长度(offset),以及当前pool使用的长度。(如果第二个Buffer大于剩下的空间,则再新建一个pool)


    <-offset:0Buffer1<-used:20488 BK的pool


  • 大对象:将会直接分配一个SlowBuffer作为slab单元,这个单元将会被这个大Buffer独占;

Buffer的转换


支持转换的类型


  • ASCII
  • UTF-8
  • UTF-16LE/UCS-2
  • Base64
  • Binary
  • Hex

    通过
    new Buffer(str, [encoding])创建
    buf.write(string,[offset],[length],[encoding])写入(就是修改,不能新增)
    buf.toString([encoding],[start],[end])转换
    1
    2
    3
    var buf = new Buffer("我是","utf-8");
    console.log(buf);
    console.log(buf.toString("utf-8",0,6));

    比较遗憾的是Node的Buffer对象支持的编码类型有限,于是提供Buffer.isEncoding([encoding])的方法判断是否支持转换

    1
    2
    console.log(Buffer.isEncoding("utf-8"));//true
    console.log(Buffer.isEncoding("utf-16"));//false [GBK.GB2312.BIG-5]等都不支持

    当然在Node中不可转换的可以借助其他模块来完成转换。
    iconv(C++调用libiconv库)iconv-lite(纯javascript)两个模块可以支持更多的编码转换

    如:iconv-lite
    1
    2
    3
    4
    5
    var iconv = require("iconv-lite");
    //Buffer 转字符串
    var str iconv.decode(buf,"win1251")
    //字符串 转Buffer
    var buf = iconv.encode("im string","win1251")

Buffer的拼接


先来一段从输入流中读取内容的示例

1
2
3
4
5
6
7
8
9
10
11
var fs = require("fs");
var rs = fs.createReadStream('demo.go');
var data = '';
rs.on("data",function (chunk) {
console.log(chunk);
//等价于 data = data.toString() + chunk.toString()
data+=chunk
});
rs.on("end",function () {
console.log(data);
})

这样子写没有问题,会输出demo.go中的Go语言的代码,但是如果换成不能解析的就会乱码,像中文,图片,视频等,因为这里的Buffer已经当字符串处理了,我们用的+=,等同于data = data.toString() + chunk.toString(),因为在中文场景下Buffer的长度受到了影响,必须还是刚才的代码,我们重现一下报错

1
var rs = fs.createReadStream('demo.go',{highWaterMark:11});

限制了一下每次读取的长度就会产生乱码
因为中文在utf-8中显示三个字节,而截断之后只要遇到一个汉字被截断之后每个字节就会以�来显示

setEncoding()中的string_decoder()

上面的Buffer拼接出现乱码确实令人头痛,当然Buffer长度越大也就越不会出现上面的问题,但是这并不是解决办法,这个问题是不可以忽视的。

于是想到了setEncoding()方法,
1
2
var rs = fs.createReadStream('main.go',{highWaterMark:11});
rs.setEncoding("utf-8")

这下就可以完整输出中文了


1
2
3
4
5
6
7
var StringDecoder = require('string_decoder').StringDecoder;
var decoder = new StringDecoder('utf-8');

var buf1 = new Buffer([0xE5, 0xBA, 0xBA, 0xBA, 0xBA, 0xBA, 0xBA])//这个buffer请忽略
console.log(decoder.write(buf1));
var buf2 = new Buffer([0xE5, 0xBA, 0xBA, 0xBA])//这个buffer请忽略
console.log(decoder.write(buf2));

原理:在调用setEncoding方法时,可读流内部设置了一个decoder对象,该对象来于string_decoder模块StringDecoder的实例对象,它神奇的地方就在于它知道宽字节字符串在utf-8中是以3个字节的方式储存,所以在第一次write()的时候只把转成的字符,而“半个”字符中的字节保存在StringDecoder的实例内部,第二次write()的时候将剩余的字节与第二次开始的字节合并成一个完整的字节在第二次输出。


虽然string_decoder模块很奇妙,但是它并不是万能的,它目前只能处理utf-8.Base64UCS-2/UTF-16LE这三种编码,虽然他能解决大部分编码问题,但不能从根本上解决该问题


正确的拼接方式


舍弃了setEncoding之后,解决方法是把Buffer小对象拼接橙一个Buffer大对象。然后用iconv-lite一类的模块进行转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var fs = require("fs");
var iconv = require("iconv-lite");
var rs = fs.createReadStream('sh.go',{highWaterMark:11});
var size = 0;
var chunks = [];
rs.on("data",function (chunk) {
chunks.push(chunk);
size+=chunk.length
});
rs.on("end",function () {
var buf = Buffer.concat(chunks,size);
var str = iconv.decode(buf,"utf-8");
console.log(str);
});

Buffer.concat方法和js数组的拼接.concat方法不相同,前者是封装了从小Buffer对象向大Buffer对象复制的过程。源码确实写得很细腻,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Buffer.concat = function(list,length){
if(!Array.isArray(list)){//判断传入list是否是数组
throw new Error("Usage: Buffer.concat(list,[length])");
}

if(list.length == 0){
return new Buffer(0);//传入数组为空时创建一个空Buffer
}else if(list.length === 1){
return list[0];//传入数组为1的时直接返回第一个Buffer作为最终Buffer
}

if(typeof length !== 'number'){//如果未传入length,或者传入的不是'number'类型,自循环获取
length = 0;
for(var i = 0; i <list.length; i++){
var buf = list[i];
length += buf.length;
}
}

var buffer = new Buffer(length);//创建一个所有子Buffer总长的Buffer;
var pos = 0;
for(var i = 0; i < list.length; i++){
var buf = list[i];
buf.copy(buffer,pos);//*使用copy方法复制子项插入到总Buffer中,并且制定插入的位置
pos+=buf.length;//*每次插入之后把刚才copy子项的长度记录,保证下次插入位置正确
}
return buffer;
}

总结,摆脱string的思维定势,注意Buffer与Sting之间的差异,后续还有Buffer与性能,暂时还没有整理;

|