NodeJS Stream(可读流、可写流) API解读

栏目: Node.js · 发布时间: 7年前

内容简介:在 NodeJS 中,我们对文件的操作需要依赖核心模块

流的介绍

在 NodeJS 中,我们对文件的操作需要依赖核心模块 fsfs 中有很基本 API 可以帮助我们读写占用内存较小的文件,如果是大文件或内存不确定也可以通过 openreadwriteclose 等方法对文件进行操作,但是这样操作文件每一个步骤都要关心,非常繁琐, fs 中提供了可读流和可写流,让我们通过流来操作文件,方便我们对文件的读取和写入。

可读流

1、createReadStream 创建可读流

createReadStream 方法有两个参数,第一个参数是读取文件的路径,第二个参数为 options 选项,其中有八个参数:

r
null
null
0o666
true
64 * 1024

createReadStream 的返回值为 fs.ReadStream 对象,读取文件的数据在不指定 encoding 时,默认为 Buffer。

创建可读流

const fs = require("fs");

// 创建可读流,读取 1.txt 文件
let rs = fs.creatReadStream("1.txt", {
    start: 0,
    end: 3,
    highWaterMark: 2
});复制代码

在创建可读流后默认是不会读取文件内容的,读取文件时,可读流有两种状态,暂停状态和流动状态。

注意:本篇的可写流为流动模式,流动模式中有暂停状态和流动状态,而不是暂停模式,暂停模式是另一种可读流 readable

2、流动状态

流动状态的意思是,一旦开始读取文件,会按照 highWaterMark 的值一次一次读取,直到读完为止,就像一个打开的水龙头,水不断的流出,直到流干,需要通过监听 data 事件触发。

假如现在 1.txt 文件中的内容为 0~9 十个数字,我们现在创建可读流并用流动状态读取。

流动状态

const fs = require("fs");

let rs = fs.createReadStream("1.txt", {
    start: 0,
    end: 3,
    highWaterMark: 2
});

// 读取文件
rs.on("data", data => {
    console.log(data);
});

// 监听读取结束
rs.on("end", () => {
    console.log("读完了");
});

// <Buffer 30 31>
// <Buffer 32 33>
// 读完了复制代码

在上面代码中,返回的 rs 对象监听了两个事件:

  • data:每次读取 highWaterMark 个字节,触发一次 data 事件,直到读取完成,回调的参数为每次读取的 Buffer;
  • end:当读取完成时触发并执行回调函数。

我们希望最后读到的结果是完整的,所以我们需要把每一次读到的结果在 data 事件触发时进行拼接,以前我们可能使用下面这种方式。

错误拼接数据的方式

const fs = require("fs");

let rs = fs.createReadStream("1.txt", {
    start: 0,
    end: 3,
    highWaterMark: 2
});

let str = "";

rs.on("data", data => {
    str += data;
});

rs.on("end", () => {
    console.log(str);
});

// 0123复制代码

在上面代码中如果读取的文件内容是中文,每次读取的 highWaterMark 为两个字节,不能组成一个完整的汉字,在每次读取时进行 += 操作会默认调用 toString 方法,这样会导致最后读取的结果是乱码。

在以后通过流操作文件时,大部分情况下都是在操作 Buffer,所以应该用下面这种方式来获取最后读取到的结果。

正确拼接数据的方式

const fs = require("fs");

let rs = fs.createReadStream("1.txt", {
    start: 0,
    end: 3,
    highWaterMark: 2
});

// 存储每次读取回来的 Buffer
let bufArr = [];

rs.on("data", data => {
    bufArr.push(data);
});

rs.on("end", () => {
    console.log(Buffer.concat(bufArr).toString());
});

// 0123复制代码

3、暂停状态

在流动状态中,一旦开始读取文件,会不断的触发 data 事件,直到读完,暂停状态是我们每读取一次就直接暂停,不再继续读取,即不再触发 data 事件,除非我们主动控制继续读取,就像水龙头打开放水一次后马上关上水龙头,下次使用时再打开。

类似于开关水龙头的动作,也就是暂停和恢复读取的动作,在可读流返回的 rs 对象上有两个对应的方法, pauseresume

在下面的场景中我们把创建可读流的结尾位置更改成 9 ,在每次读两个字节并暂停一秒后恢复读取,直到读完 0~9 十个数字。

暂停状态

const fs = require("fs");

let rs = fs.createReadStream("1.txt", {
    start: 0,
    end: 9,
    hithWaterMark: 2
});

let bufArr = [];

rs.on("data", data => {
    bufArr.push(data);
    rs.pause(); // 暂停读取
    console.log("暂停", new Date());

    setTimeout(() => {
        rs.resume(); // 恢复读取
    }, 1000)
});

rs.on("end", () => {
    console.log(Buffer.concat(bufArr).toString());
});

// 暂停 2018-07-03T23:52:52.436Z
// 暂停 2018-07-03T23:52:53.439Z
// 暂停 2018-07-03T23:52:54.440Z
// 暂停 2018-07-03T23:52:55.442Z
// 暂停 2018-07-03T23:52:56.443Z
// 0123456789复制代码

4、错误监听

在通过可读流读取文件时都是异步读取,在异步读取中如果遇到错误也可以通过异步监听到,可读流返回值 rs 对象可以通过 error 事件来监听错误,在读取文件出错时触发回调函数,回调函数参数为 err ,即错误对象。

错误监听

const fs = require("fs");

// 读取一个不存在的文件
let rs = fs.createReadStream("xxx.js", {
    highWarterMark: 2
});

let bufArr = [];

rs.on("data", data => {
    bufArr.push(data);
});

rs.on("err", err => {
    console.log(err);
});

rs.on("end", () => {
    console.log(Buffer.concat(bufArr).toString());
});

// { Error: ENOENT: no such file or directory, open '......xxx.js' ......}复制代码

5、打开和关闭文件的监听

流的适用性非常广,不只是文件读写,也可以用在 http 中数据的请求和响应上,但是在针对文件读取返回的 rs 上有两个专有的事件用来监听文件的打开与关闭。

open 事件用来监听文件的打开,回调函数在打开文件后执行, close 事件用来监听文件的关闭,如果创建的可读流的 autoClosetrue ,在自动关闭文件时触发,回调函数在关闭文件后执行。

打开和关闭可读流的监听

const fs = require("fs");

let rs = fs.createReadStream("1.txt", {
    start: 0,
    end: 3,
    highWaterMark: 2
});

rs.on("open", () => {
    console.log("open");
});

rs.on("close", () => {
    console.log("close");
});

// open复制代码

在上面代码我们看出只要创建了可读流就会打开文件触发 open 事件,因为默认为暂停状态,没有对文件进行读取,所以不会关闭文件,即不会触发 close 事件。

暂停状态

const fs = require("fs");

let rs = fs.createReadStream("1.txt", {
    start: 0,
    end: 3,
    hithWaterMark: 2
});

rs.on("open", () => {
    console.log("open");
});

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

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

rs.on("close", () => {
    console.log("close");
});

// open
// <Buffer 30 31>
// <Buffer 32 33>
// end
// close复制代码

从上面例子执行的打印结果可以看出只有开始读取文件并读完后,才会关闭文件并触发 close 事件, end 事件的触发要早于 close

可写流

1、createWriteStream 创建可写流

createWriteStream 方法有两个参数,第一个参数是读取文件的路径,第二个参数为 options 选项,其中有七个参数:

w
utf8
null
0o666
true
16 * 1024

createWriteStream 返回值为 fs.WriteStream 对象,第一次写入时会真的写入文件中,继续写入,会写入到缓存中。

创建可写流

const fs = require("fs");

// 创建可写流,写入 2.txt 文件
let ws = fs.createWriteStream("2.txt", {
    start: 0,
    highWaterMark: 3
});复制代码

2、可写流的 write 方法

在可写流中将内容写入文件需要使用 wswrite 方法,参数为写入的内容,返回值是一个布尔值,代表 highWaterMark 的值是否足够当前的写入,如果足够,返回 true ,否则返回 false ,换种说法就是写入内容的长度是否超出了 highWaterMark ,超出返回 false

write 方法写入

const fs = require("fs");

let ws = fs.createWriteSteam("2.txt", {
    start: 0,
    highWaterMark: 3
});

let flag1 = ws.write("1");
console.log(flag1);

let flag2 = ws.write("2");
console.log(flag2);

let flag3 = ws.write("3");
console.log(flag3);

// true
// true
// false复制代码

写入不存在的文件时会自动创建文件,如果 start 的值不是 0 ,在写入不存在的文件时默认找不到写入的位置。

3、可写流的 drain 事件

drain 意为 “吸干”,当前写入的内容已经大于等于了 highWaterMark ,会触发 drain 事件,当内容全部从缓存写入文件后,会执行回调函数。

drain 事件

const fs = require("fs");

let ws = fs.createWriteStream("2.txt", {
    start: 0,
    highWaterMark: 3
});

let flag1 = ws.write("1");
console.log(flag1);

let flag2 = ws.write("2");
console.log(flag2);

let flag3 = ws.write("3");
console.log(flag3);


ws.on("drain", () => {
    console.log("吸干");
});

// true
// true
// false复制代码

4、可写流的 end 方法

end 方法传入的参数为最后写入的内容, end 会将缓存未写入的内容清空写入文件,并关闭文件。

end 方法

const fs = require("fs");

let ws = fs.createWriteStream("2.txt", {
    start: 0,
    highWaterMark: 3
});

let flag1 = ws.write("1");
console.log(flag1);

let flag2 = ws.write("2");
console.log(flag2);

let flag3 = ws.write("3");
console.log(flag3);

ws.on("drain", () => {
    console.log("吸干");
});

ws.end("写完了");

// true
// true
// false复制代码

在调用 end 方法后,即使再次写入的值超出了 highWaterMark 也不会再触发 drain 事件了,此时打开 2.txt 后发现文件中的内容为 “123写完了”。

常见报错

const fs = require("fs");

let ws = fs.createWriteStream("2.txt", {
    start: 0,
    highWaterMark: 3
});

ws.write("1");
ws.end("写完了");
ws.write("2");

// Error [ERR_STREAM_WRITE_AFTER_END]: write after end...复制代码

在调用 end 方法后,不可以再调用 write 方法写入,否则会报一个很常见的错误 write after end ,文件原有内容会被清空,而且不会被写入新内容。

可写流与可读流混合使用

可写流和可读流一般配合来使用,读来的内容如果超出了可写流的 highWaterMark ,则调用可读流的 pause 暂停读取,等待内存中的内容写入文件,未写入的内容小于 highWaterMark 时,调用可写流的 resume 恢复读取,创建可写流返回值的 rs 上的 pipe 方法是专门用来连接可读流和可写流的,可以将一个文件读来的内容通过流写到另一个文件中。

pipe 方法使用

const fs = require("fs");

// 创建可读流和可写流
let rs = fs.createReadStream("1.txt", {
    highWaterMark: 3
});
let ws = fs.createWriteStream("2.txt", {
    highWaterMark: 2
});

// 将 1.txt 的内容通过流写入 2.txt 中
rs.pipe(ws);复制代码

通过上面的这种类似于管道的方式,将一个流从一个文件输送到了另一个文件中,而且会根据读流和写流的 highWaterMark 自由的控制写入的 “节奏”,不用担心内存的消耗。

总结

这篇是关于读流和写流的基本用法,在平时的开发当中,大多数的 API 都用不到,只有最后的 pipe 用的最多,无论是在文件的读写还是请求的响应,其他的 API 虽然用的少,但是作为一个合格的 程序员 一定要有所了解。


以上所述就是小编给大家介绍的《NodeJS Stream(可读流、可写流) API解读》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Linux Command Line

The Linux Command Line

William E. Shotts Jr. / No Starch Press, Incorporated / 2012-1-17 / USD 39.95

You've experienced the shiny, point-and-click surface of your Linux computer-now dive below and explore its depths with the power of the command line. The Linux Command Line takes you from your very ......一起来看看 《The Linux Command Line》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换