buffer的理解以及文件上传的应用

Buffer(缓冲区)

在Node中,应用需要处理网络协议、操作数据库、处理图片、接收上传的文件等,在网络流和文件的操作中,还要处理大量的二进制数据,而Js自有的字符串远远无法满足这些需求,于是Buffer对象应运而生

Buffer类在全局作用域中,因此,我们无需使用require(‘buffer’).Buffer来进行使用

什么是Buffer(缓冲区)

我们知道数据的移动是以流的方式进行的。当我们从文件或网络读取数据的时候,就需要一个输入流来进行数据的读取;而当我们要写入一些数据的时候,就需要开启一个输出流来进行数据的移动。

但是,Node并无法控制数据流的速度以及数据到达目的地的时间。因此,如果数据到达的速度比进程消耗的速度快,那么少数早到达的数据会处于等待区等候被处理。反之,如果数据到达的速度比进程消耗的数据慢,那么早先到达的数据需要在等待区中等待一定量的数据到达之后才能被处理。

这个等待区也就是我们要提的Buffer(缓冲区)

Buffer对象

Buffer对象类似与一个数组,它的元素为16进制的两位数,即0到255的数值。

我们可以这样来创建Buffer对象

1
2
const buf =  Buffer.from('理解Buffer')
console.log(buf);

我们输出一下这个buf对象长度

1
console.log(buf.length); // 12

我们可以看到buf对象的长度与给定的字符串长度不一样。因此,我们可以得出不同编码的字符串占用的元素个数各不相同。上面的代码中的中文字在UTF-8的编码下占用3个元素,而字母和半角标点符号占用1个元素。

另外,以下是创建Buffer对象时常用的API:

  1. Buffer.from(array) 返回一个新的 Buffer,其中包含提供的八位字节数组的副本。
  2. Buffer.from(arrayBuffer[, byteOffset [, length]]) 返回一个新的 Buffer,它与给定的 ArrayBuffer 共享相同的已分配内存。
  3. Buffer.from(buffer) 返回一个新的 Buffer,其中包含给定 Buffer 的内容的副本。
  4. Buffer.from(string[, encoding]) 返回一个新的 Buffer,其中包含提供的字符串的副本,encoding为给定的string的编码格式。
  5. Buffer.alloc(size[, fill[, encoding]]) 返回一个指定大小的新建的的已初始化的 Buffer。 此方法比 Buffer.allocUnsafe(size) 慢,但能确保新创建的 Buffer 实例永远不会包含可能敏感的旧数据。 如果 size 不是数字,则将会抛出 TypeError。
  6. Buffer.allocUnsafe(size) 和 Buffer.allocUnsafeSlow(size) 分别返回一个指定大小的新建的未初始化的 Buffer。 由于 Buffer 是未初始化的,因此分配的内存片段可能包含敏感的旧数据。

如果 size 小于或等于 Buffer.poolSize 的一半,则 Buffer.allocUnsafe() 返回的 Buffer 实例可能是从共享的内部内存池中分配。 Buffer.allocUnsafeSlow() 返回的实例则从不使用共享的内部内存池。

示例:

1
2
3
4
5
6
7
// 创建一个给定Array的Buffer,其中Array的元素为10进制整数,它会被转化为16进制的二进制数
let buffer = Buffer.from([53,198,255]);
console.log(buffer); // <Buffer 35 c6 ff>

// 分配一个指定大小的新建的的已初始化的 Buffer,该Buffer永远不会包含旧数据
buffer = Buffer.alloc(10);
console.log(buffer); // <Buffer 00 00 00 00 00 00 00 00 00 00>

Buffer的内存分配

我们在V8的垃圾回收机制中了解到,一般的基本类型变量会存储在栈中,而复杂引用类型会存储在v8的堆内存中。

然而,Buffer对象的内存分配不是在v8的堆内存中进行的,而是在Node的C++层面进行内存分配的。

因为处理大量的字节数据不能采用需要一点内存就向系统申请内存的方式(v8的内存管理就采用这种方式,如果堆内存不够就继续申请堆内存直到超过系统限制),这样的话会造成操作系统层面的压力

Node采用slab分配机制来进行Buffer对象的内存分配。

slab是一块申请号的固定大小的内存区域,它的大小为8kb。Node通常也以8Kb为分界来区分小Buffe对象和大Buffer对象

1
Buffer.poolSize = 8*1024;

之前提过,我们可以使用Buffer.alloc(size)来分配指定大小的Buffer对象

当size<8*1024时,该Buffer对象为小对象,否则,则是大对象

当在进行slab内存分配的时候,其符合以下的规则:

  1. 如果要分配的Buffer内存大小小于slab内存大小,那么在slab中分配内存给Buffer
  2. 如果slab剩余空闲内存小于需要分配的Buffer内存大小,那么重新会新建一个slab来分配该Buffer对象的内存
  3. 如果Buffer对象为大对象,即需要分配的内存大于8kb,那么,系统将会直接分配一个SlowBuffer对象作为slab单元,这个slab单元将会被该Buffer对象独占

Buffer与字符串的相互转换

当字符串存储入一个Buffer实例或者从Buffer实例中提取时,我们可以指定一个字符编码

例如:

1
2
3
4
5
6
7
8
9
// 以utf-8编码将字符串存储入Buffer中
let buf = Buffer.from('理解Buffer','utf-8');
console.log(buf);
// 以base64编码取出该字符串
console.log(buf.toString('base64')); // 55CG6KejQnVmZmVy

// 以ascii编码将字符串存入Buffer中
buf = Buffer.from('理解Buffer','ascii');
console.log(buf);

Buffer的拼接

直接拼接Buffer时遇到的问题

我们知道数据的移动是以流的方式进行的。那么,使用了Buffer作为缓冲区后,我们得到一个流数据就是一段一段的进行获取

现在我们来试着使用读取流来读取一个文件内容:

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const fs = require('fs');

// 该fs.createReadStream()方法会返回一个新的fs.ReadStream对象,该对象也就是读取流
let rs = fs.createReadStream('./a.ja');

let data = '';

// 当流将数据块传送给消费者后触发data事件
rs.on('data',(chunk)=>{
data+=chunk;
})

// 'end' 事件只有在数据被完全消费掉后才会触发
rs.on('end',()=>{
console.log(data);
})

此时a.js文件内容如下:

1
console.log('hello');

那么,输出结果如下:

1
console.log('hello');

我们发现读取似乎没有什么问题。但其实,这是有问题的。上述程序中有一行语句是

1
data+=chunk;

我们知道流的读取时,传递的应该是Buffer对象,我们试着输出一下这个chunk

1
2
3
4
rs.on('data',(chunk)=>{
console.log(chunk); // <Buffer 63 6f 6e 73 6f 6c 65 2e 6c 6f 67 28 27 68 65 6c 6c 6f 27 29 3b>
data+=chunk;
})

的确,这个chunk确实是Buffer对象,那么为什么最后输出的是正确的字符串呢?

我们知道在字符串拼接的时候,如果有一方不是字符串,那么就会将其转化为字符串之后再进行拼接。因此,那行代码又可以等价为

1
data+=chunk.toString();

这样似乎都可以说的通了,但是如果我们读取的内容不全是英文,而是包括有宽字节的中文呢?

我们将a.js文件内容改为如下

1
console.log('你好呀,我很好');

同时,我们利用highWaterMark属性将缓冲区的大小限制为5。

此时,完整的程序如下:

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
const fs = require('fs');

// 将每次读取的Buffer长度限制为5
let rs = fs.createReadStream('./a.js',{highWaterMark:5});

let data = '';


rs.on('data',(chunk)=>{
console.log(chunk);
data+=chunk;
})

rs.on('end',()=>{
console.log(data);
})

/* 输出结果
<Buffer 63 6f 6e 73 6f>
<Buffer 6c 65 2e 6c 6f>
<Buffer 67 28 27 e4 bd>
<Buffer a0 e5 a5 bd e5>
<Buffer 91 80 ef bc 8c>
<Buffer e6 88 91 e5 be>
<Buffer 88 e5 a5 bd 27>
<Buffer 29 3b>
console.log('��好���,我��好');
*/

我们可以看到,每次的读取Buffer长度的确为5。此时,我们惊奇的发现,文件内容出现了乱码。

这是为什么呢?我们知道,中文字符为宽字节字符,在utf-8模式下,其占3个字节。因此,我们用每次5个字节进行读取时,就会遇到,有些中文字符会被拆分到两次读取中,因此,就会显示出乱码

利用setEncoding来解决

readable.setEncoding() 方法为从可读流读取的数据设置字符编码。

默认情况下没有设置字符编码,流数据返回的是 Buffer 对象。 如果设置了字符编码,则流数据返回指定编码的字符串。 例如,调用 readable.setEncoding(‘utf-8’) 会将数据解析为 UTF-8 数据,并返回字符串,调用 readable.setEncoding(‘hex’) 则会将数据编码成十六进制字符串。

因此,我们可以这样修改上述的程序:

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
29
const fs = require('fs');

const rs = fs.createReadStream('./a.js',{highWaterMark:5});

// 设置字符编码,将流Buffer对象转化为字符串
rs.setEncoding('utf-8');

let data = '';
rs.on('data',(chunk)=>{
// 此时chunk为字符串形式
console.log(chunk);
data+=chunk;
})

rs.on('end',()=>{
console.log(data);
})

/* 输出结果
conso
le.lo
g('
你好
呀,

很好'
);
console.log('你好呀,我很好');
*/

虽然使用setEncoding可以解决目前的问题,但是它目前只能处理utf-8、Base64等部分编码,因此,它并不是完美的。

使用Buffer.concat()来解决

相较setEncoding方法在接收时并将buffer对象进行编码转换的不同,Buffer.concat()方法的思想是先接收到所有的小Buffer对象,然后将所有的小Buffer对象进行合并成一个大对象然后再进行字符串输出

Buffer.concat(list[,totalLength])方法接受一个要合并的Buffer数组和合并后list中的Buffer实例的总长度,然后返回一个合并了list中所有Buffer实例的新Buffer

如果没有提供 totalLength,则计算 list 中的 Buffer 实例的总长度。 但是这会导致执行额外的循环用于计算 totalLength,因此如果已知长度,则明确提供长度会更快

因此,最完美的解决方案应该如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const fs = require('fs');

const rs = fs.createReadStream('./a.js',{highWaterMark:5});

let list = [];
let length = 0;
rs.on('data',(chunk)=>{
list.push(chunk);
length+=chunk.length;
})

rs.on('end',()=>{
let newBuf = Buffer.concat(list,length);
console.log(newBuf);
console.log(newBuf.toString('utf-8'));
})

/* 输出结果
<Buffer 63 6f 6e 73 6f 6c 65 2e 6c 6f 67 28 27 e4 bd a0 e5 a5 bd e5 91 80 ef bc 8c e6 88 91 e5 be 88 e5 a5 bd 27 29 3b>
console.log('你好呀,我很好');
*/

我们可以看到,结果是可以正确输出的。

BUffer与网络传输

网络传输一般使用字节流来进行传输,因此,无论我们在传输之前什么类型的值,在传输的过程中都会转化为Buffer对象来进行网络传输

例如:当客户端想要通过post方式传递一些数据的时候,这些数据就会被转化为Buffer对象,一点一点地传递到服务器端

注意:

网络传输中,请求与响应都是流对象,req为可读流,res为可写流

因此,我们可以利用以下的方式来获取post请求的携带的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const http = require('http');

let list = [];
let length = 0;
http.createServer((req,res)=>{
req.on('data',(chunk)=>{
// console.log(chunk);
list.push(chunk);
length+=chunk.length;
})

req.on('end',()=>{
let buf = Buffer.concat(list,length);
// console.log(buf);
console.log(buf.toString('utf-8'));

res.setHeader('Content-Type','application/JSON');
res.end(buf.toString('utf-8'))
})
})

因此,如果我们直接返回Buffer类型,cpu就不需要进行类型转换工作,可以有效地减少cpu的重复使用,节省服务器资源

文件上传

首先我们要在前端选取文件,并得到文件的base64编码

前端部分代码,通过FileReader对象来将获取的文件进行base64编码,通过axios来进行文件上传

前端部分

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
const axios = require('axios');

let fileSelect = document.getElementsByTagName('input')[0];
let submit = document.getElementsByTagName('input')[1];


submit.addEventListener('click',()=>{
let file = fileSelect.files[0];
let fr = new FileReader(file);
// 处理load事件。该事件在读取操作完成时触发。
fr.addEventListener('load',()=>{
console.log(fr.result);
axios.post('/api/hello',{
params: {
value: fr.result
}
}).then(v=>{
console.log(v);
}).catch(err=>{
console.log(err);
})
})
// 开始读取指定的Blob中的内容。一旦完成,result属性中将包含一个data: URL格式的Base64字符串以表示所读取文件的内容。
fr.readAsDataURL(file);
})

Node端处理文件上传

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
29
30
31
32
33
const http = require('http');
const fs = require('fs');
const queryString = require('queryString');

let list = [];
let length = 0;
http.createServer((req,res)=>{
req.on('data',(chunk)=>{
list.push(chunk);
length+=chunk.length;
})

req.on('end',()=>{
// 获取得到的是utf-8编码的Buffer对象
let buf = Buffer.concat(list,length);
// 将该Buffer对象转化为utf-8编码的字符串,并且取出包含在该字符串中的base64编码的文件内容
let data = JSON.parse(buf.toString('utf-8')).params.value.replace(/^data:text/javascript;base64,/,'');
// 接下来就需要将base64编码转化为utf-8编码,我们可以间接地通过Buffer对象来转换

// 创建以base64编码的Buffer对象
let newBuf = Buffer.from(data,'base64');
// 将该Buffer对象转化为base64编码的字符串
let fileContent = newBuf.toString('utf-8');
// 将文件内容写进get.js文件
fs.writeFile('get.js',fileContent,()=>{
console.log('文件已保存');
})
res.setHeader('Content-Type','application/JSON');
res.end(JSON.stringify({value:'收到'}));
})
}).listen(3000,()=>{
console.log('Port 3000 is listenging');
})

此时,我们就可以实现一个js文件的上传。

当然,为了能够上传更多格式的文件,我们修改以上的正则表达式就OK了

Node端处理图片上传

因为图片的编码貌似都是base64编码,因此,我们在获得图片的base64编码后不需要像文件一样转化为utf-8编码,只需直接存储即可

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
29
30
31
32
33
34
const http = require('http');
const fs = require('fs');
const queryString = require('queryString');

let list = [];
let length = 0;
http.createServer((req,res)=>{
req.on('data',(chunk)=>{
list.push(chunk);
length+=chunk.length;
})

req.on('end',()=>{
// 获取得到的是utf-8编码的Buffer对象
let buf = Buffer.concat(list,length);
// 将该Buffer对象转化为utf-8编码的字符串,并且取出包含在该字符串中的base64编码的文件内容
let data = JSON.parse(buf.toString('utf-8')).params.value.replace(/^data:((text/(javascript|plain))|(image/(png|jpg|jpeg|gif)));base64,/,'');
// 接下来就需要将base64编码转化为utf-8编码,我们可以间接地通过Buffer对象来转换
console.log(data);
// 创建以base64编码的Buffer对象
let newBuf = Buffer.from(data,'base64');
// 不需要将该Buffer对象转化为base64编码的字符串
//let fileContent = newBuf.toString('utf-8');

// 生成a.jpeg
fs.writeFile('a.jpeg',newBuf,()=>{
console.log('文件已保存');
})
res.setHeader('Content-Type','application/JSON');
res.end(JSON.stringify({value:'收到'}));
})
}).listen(3000,()=>{
console.log('Port 3000 is listenging');
})