原来来自 我的个人博客
1. Buffer 缓冲区
我们会发现,对于前端开发来说,通常很少会和二进制直接打交道,但是对于服务器端为了做很多的功能,我们必须直接去操作其二进制的数据;
所以 Node
为了可以方便开发者完成更多功能,提供给了我们一个 Buffer
类用来创建一个专门存放二进制数据的缓存区,并且它是全局的。
我们可以将 Buffer
看成是一个存储二进制的数组,这个数组中的每一项,可以保存 8
位二进制:0000 0000
1. Buffer 和字符串
Buffer
相当于是一个字节的数组,数组中的每一项对应一个字节的大小:
如果我们希望将一个字符串放入到 Buffer
中,是怎么样的过程呢?
const buffer = new Buffer("hello");
console.log(buffer);
打印结果如下:
可以看到打印出的 Buffer
对象包含了五个 由两位十六进制组成的数字。而我们知道 2
位十六进制正好对应 8
位二进制也就是一个字节
而如果你去查询 ASCII
表的话会发现,这五个十六进制数正好对应着英文字母的 h
e
l
l
o
也就是说,我们可以得出结论:Buffer
存储字符串时默认是以 ASCII
将字符串编码存储的
1.2 Buffer 与中文
Buffer
默认是以 utf-8
为默认编码存储中文的
在上面一小节的例子中,我们使用 new Buffer()
创建了一个 Buffer
对象,事实上由于安全性和可用性的问题,Node
已经废弃了这种方法,推荐使用 Buffer.alloc()
、Buffer.allocUnsafe()
或者 Buffer.from()
这三种方法创建 Buffer
对象
接下来我们试着用 Buffer
存储中文:
const buf = Buffer.from("你好");
console.log(buf);
打印结果如下:
我们知道在 UTF-8
中,一个中文等于三个字节。而在上面的打印结果也能看出 “你好”
两个字正好存储了六个两位十六进制的数字,证实了我们的结论。
乱码的问题:
- 一般乱码的问题都是因为编码与解码使用了不同的方法而导致的。
- 在
Buffer.from()
方法中,我们还可以添加第二个参数来规定使用哪种编码方式
const fs = require("fs");
const buf = Buffer.from("你好", "utf16le");
console.log(buf.toString("utf16le"));
console.log(buf.toString("utf8"));
打印结果:
可以看到,同时以 utf-16
解码打印出来没有出现乱码,而以 utf-8
解码打印出来出现了乱码。
1.3 Buffer 的其他创建方式
关于 Buffer
的创建方式还有很多,见下图
这里主要再讲一下 alloc
,如果有学习 c
语言的应该是很熟悉的。它的意思是 承认、同意。Buffer.alloc
的作用为向内存申请一个多长的 Buffer
,里面的默认数据是 00
。
const buf = Buffer.alloc(8);
console.log(buf);
打印结果如下
我们也可以对其进行操作
buf[0] = "w".charCodeAt();
buf[1] = 100;
buf[2] = 0x66;
console.log(buf); // <Buffer 77 64 66 00 00 00 00 00>
console.log(buf[0]); // 119
console.log(buf[2].toString()); // 102
1.4 Buffer 和文件读取
在上一章 fs
模块中介绍中,我们提到了 readFile
的回调是一个 Buffer
,现在大家应该也能理解了吧
- 文本的读取
const fs = require("fs");
fs.readFile("./test.txt", (err, data) => {
console.log(data); // <Buffer 48 65 6c 6c 6f 20 57 6f 72 6c 64>
console.log(data.toString()); // Hello World
});
- 图片的读取
const fs = require("fs");
fs.readFile("./image.png", (err, data) => {
console.log(data); // <Buffer 89 50 4e 47 0d 0a 1a 0a 00 00 ... 1878012 more bytes>
});
2. Stream 流
Stream 意为 小溪、小河的意思,在编程中通常翻译为流。
我们可以想象当我们从一个文件中读取数据时,文件的二进制数据会源源不断的被读取到我们程序中,而这个一连串的字节,就是我们程序中的流。
所以我们可以这样理解流:
- 它是连续字节的一种表现形式和抽象概念
- 流应该是可读的,也是可写的
再上一章讲文件的读写时,我们已经可以直接通过 readFile 和 writeFile 的方式读写文件了,为什么还需要流呢?
- 直接读写的方式,虽然简单,但是无法控制一些细节的操作
- 比如从什么位置开始读、读到什么位置、一次性读取多少个字节
- 读到某个位置后,暂停读取,某个时刻恢复继续读取等等
- 或者这个文件非常大,比如一个视频文件,一次性全部读取并不合适
Node.js
中有四种基本流类型:
Writable
:可以向其写入数据的流(例如fs.createWriteStream()
)。Readable
:可以从中读取数据的流(例如fs.createReadStream()
)。Duplex
:同时为Readable
和Writable
(例如net.Socket
)。Transform
:Transform
可以在写入和读取数据时修改或转换数据的流(例如zlib.createDeflate()
)。
这里我们通过 fs
的操作,讲解一下 Writable
、Readable
,另外两个大家可以自行学习一下。
2.1 Readable
- 之前我们读取一个文件的信息
fs.readFile("./test.txt", (err, data) => {
console.log(data.toString()); // Hello World
});
这种方式是一次性将一个文件中所有的内容都读取到程序(内存)中,但是这种读取方式就会出现我们之前提到的很多问题:
- 文件过大、读取的位置、结束的位置、一次读取的大小;
这个时候,我们可以使用 createReadStream
,我们来看几个参数,更多参数可以参考官网:
start
:文件读取开始的位置;end
:文件读取结束的位置;highWaterMark
:一次性读取字节的长度,默认是64kb
;
通过
Readable
读取一个文件信息- 创建文件的Readable
const read = fs.createReadStream('./test.txt', { start: 3, end: 8, highWaterMark: 4 })
- 通过监听 data 时间,获取读取到的数据
read.on("data", (data) => { console.log(data); });
打印结果如下:
- 也可以做一些其他的操作:监听其他事件、暂停或恢复
read.on("open", (fd) => { console.log("文件被打开", fd); }); read.on("end", () => { console.log("文件读取结束"); }); read.on("close", () => { console.log("文件被关闭"); }); // 暂停读取,两秒后再读取 read.pause(); setTimeout(() => { read.resume(); }, 2000);
2.2 Writeable
- 之前我们写入一个文件的方式是这样的
fs.writeFile('./test.txt',"内容",(err)=>{
})
这种方式相当于一次性将所有的内容写入到文件中,但是这种方式也有很多问题:
- 比如我们希望一点点写入内容,精确每次写入的位置等;
这个时候,我们可以使用 createWriteStream
,我们来看几个参数,更多参数可以参考官网:
flags
:默认是w
,如果我们希望是追加写入,可以使用a
或者a+
;start
:写入的位置;
- 使用流的方式写入文件
const writer = fs.createWriteStream("./test.txt", {
flags: "a+",
start: 8,
});
writer.write("你好啊", (err) => {
console.log("写入成功");
});
writer.on("open", () => {
console.log("文件打开了");
});
writer.on("finish", () => {
console.log("文件写入结束");
});
writer.on("close", () => {
console.log("文件关闭");
});
我们会发现,我们并不能监听到 close
事件:
- 这是因为写入流在打开后是不会自动关闭的;
- 我们必须手动关闭,来告诉
Node
已经写入结束了; - 并且会发出一个
finish
事件的;
另外一个非常常用的方法是 end
:end
方法相当于做了两步操作: write
传入的数据和调用close
方法;
writer.close()
writer.end('Hello World')
2.3 pipe 方法
正常情况下,我们可以将读取到的 输入流,手动的放到 输出流中进行写入:
const reader = fs.createReadStream("./test.txt");
const writer = fs.createWriteStream("./bar.txt");
reader.on("data", (data) => {
console.log(data);
writer.write(data, (err) => {
console.log(err);
});
});
我们也可以通过 pipe
来完成这样的操作:
reader.pipe(writer)