前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >第七十期:Node中的I/O操作(标准I/O)

第七十期:Node中的I/O操作(标准I/O)

作者头像
terrence386
发布2022-07-15 10:44:12
6290
发布2022-07-15 10:44:12
举报

Node中的I/O操作 (标准I/O)

从某种意义上讲,Node其实是在C++的基础上又包了一层。和其他语言一样,Node和操作系统的交互也是通过I/O。

Node的I/O操作包括哪些内容

Node的I/O操作具体包括哪些内容呢?有这么几个:

  • 标准I/O
  • 文件处理 fs
  • 元数据处理 metadata
  • 文件和目录的监听
  • sockets通信

标准I/O

标准I/O我们可以理解为Node 中一些事先定义好的输入,输出,以及一些为了显示在终端中的错误数据。比如常见的(STDIN),(STDOUT)标准输出,以及(STDERR)标准错误等,这些都可以被重定向并通过管道传输到其他程序,以便进一步处理、存储等。

Node 通过全局的process对象,提供了操作标准I/O的能力。

我们还是用一个简单的例子来体验一下:

代码语言:javascript
复制
process.stdin.on('data', (data) => {
  process.stderr.write(`my name is:${data}` + '\n')
  process.stdout.write(data.toString('base64') + '\n')
})

运行程序:

代码语言:javascript
复制
node index.js

然后在终端输入名字,可以看到如下结果

这是怎么实现的呢?

标准I/O是如何实现的

Node标准I/O通道,其实是是用了Node的streams流实现的。

可以说,Node的streams流实例(从stream核心流模块实例化而来)继承自EventEmitter,并为接收到的每个数据块发出一个数据(data)事件。

当处于交互模式时,每一行都代表了一个数据块儿(data chunk)。当通过process输送数据时,每个数据块儿都由streams流允许消耗的最大内存决定。

我们对data事件进行监听,它实际上提供了一个二进制表示形式的数据,用来存储输入的数据。

当接受到data事件时,调用process.stdout 的 out方法就可以了。

但是怎么判断I/O已经和终端建立了链接呢?

终端检测

虽然一般情况下,标准I/O是跟终端分离的。但是如果我们知道我们的程序是否直接连到终端,或者有没有被重定向,这对我们很有帮助。

我们可以用isTTY这个属性做判断。

但是当我们执行下面的命令时,这个属性会返回false。

代码语言:javascript
复制
echo "terrence" | node -p "process.stdin.isTTY"

如图:

这是因为程序是在shell的管道中执行。需要注意的是,isTTY是defined,并且不是false,因为这可能会导致错误。

因为标准I/O通道是根据场景从不同的构造函数内部初始化的。所以当进程直接连接到终端时,process.stdin是使用核心TTY模块的ReadStream构造函数创建的,该构造函数具有isTTY属性。然而,当I/O被重定向时,通道是从网络模块的套接字构造函数创建的,它没有isTTY属性。

文件处理 fs

文件处理能力是服务端编程的一个基本能力,Node通过fs模块提供了这种能力。

我们可以通过下面的语句生成一个1M的文件。

代码语言:javascript
复制
node -p "Buffer.allocUnsafe(1e6).toString()" > file.dat

然后修改我们的index.js

代码语言:javascript
复制
const fs = require('fs')
const path = require('path')
const cwd = process.cwd()
const bytes = fs.readFileSync(path.join(cwd, 'file.dat'))
const clean = bytes.filter((n) => n)
fs.writeFileSync(path.join(cwd, 'clean.dat'), clean)
// 添加日志
fs.appendFileSync(
  path.join(cwd, 'log.txt'),
  new Date() + '-' + (bytes.length - clean.length) + 'bytes removed \n'
)

运行index.js,我们可以得到log.txt文件。

这个过程是什么样的呢

fs和path是Node的两个核心模块。

path.join()这个方法可以将跨平台的路径格式化,windows上用反斜杠‘\’,其他的用斜杠‘\’。

代码中用了三次,和path.cwd()一起,用来获取当前的工作目录的路径。

这意味着,在读取整个文件之前,任何队列中的逻辑都会被阻塞,从而破坏任何并发操作(例如服务web请求)的容量。

这也是为什么在Node中同步操作通常是显式的原因。但是在这个demo中,这些情况无关紧要。

我们先读取了file.dat中的内容,然后通过filter方法删除了0字节的内容。fs.readFileSync返回的是一个Buffer 对象,里面存储的是二进制数据。它是从Unit8Arrat集成而来的。

filter方法中有个函数,这个函数只返回传递给它的值。如果值为0,则字节将从字节数组中删除。

最后,我们使用fs.appendFileSync方法记录删除到日志中的日期和字节数写到log.txt文件。如果是log.txt文件不存在,将自动创建一个log.txt并写入内容。

异步文件操作

假如我们需要一些信息来表示我们的程序真正处理一些问题。

我们也许可以使用定时器,来写一个东西。比如:每10ms输出一个s到终端。

代码语言:javascript
复制
setInterval(()=>process.stdout.write('s'),10).unref()

然后我们修改index.js

代码语言:javascript
复制
setInterval(() => {
  process.stdout.write('s')
}, 10).unref()

const fs = require('fs')
const path = require('path')
const cwd = process.cwd()

fs.readFile(path.join(cwd, 'file.dat'), (err, bytes) => {
  if (err) {
    console.log(err)
    process.exit(1)
  }
  const clean = bytes.filter((n) => n)
  fs.writeFile(path.join(cwd, 'clean.dat'), clean, (err) => {
    if (err) {
      console.log(err)
      process.exit(1)
    }
    fs.appendFile(
      path.join(cwd, 'log.txt'),
      new Date() + '-' + (bytes.length - clean.length) + 'bytes removed \n',
      (err) => {
        console.log(err)
      }
    )
  })
})

浏览器中的定时器返回的是数字ID,用来清除定时器。Node中的定时器返回的是对象,作用也是清除定时器,但是使用的unref()方法。

我们直接运行单独的定时器函数,终端并不会输出s字符。因为同步操作发生在事件循环的同一个Tick中,如果这个tick中没有别的操作,程序就退出。

但是异步操作有可能发生在好几个tick中,同时对时间有一定的延迟。

我们执行index.js可以看到它打印出来了字符串。

增量处理

我们要如何减轻密集的字节剥离操作对其他重要并发逻辑的阻塞?这个是一个问题。

没的说,肯定需要用streams。

执行下面的代码:

代码语言:javascript
复制
setInterval(() => {
  process.stdout.write('s')
}, 10).unref()

const fs = require('fs')
const path = require('path')
const cwd = process.cwd()
const sbs = require('strip-bytes-stream')

fs.createReadStream(path.join(cwd, 'file.dat'))
  .pipe(sbs((n) => n))
  .on('end', function () {
    console.log(this.total)
  })
  .pipe(fs.createWriteStream(path.join(cwd, 'clean.dat')))

function log(total) {
  fs.appendFile(
    path.join(cwd, 'log.txt'),
    new Date() + '-' + total + 'bytes removed \n'
  )
}

我们会看到更多的字符s。

这是因为文件是以块的形式读入进程的。每个区块都会被剥离空字节并写入文件,旧区块和剥离结果会被丢弃,而下一个区块会进入进程内存。这一切都发生在事件循环的多个tick中,从而为计时器队列的处理留出了空间。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-02-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 JavaScript高级程序设计 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Node的I/O操作包括哪些内容
  • 标准I/O
  • 标准I/O是如何实现的
  • 终端检测
  • 文件处理 fs
  • 这个过程是什么样的呢
  • 异步文件操作
  • 增量处理
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档