Node中的I/O操作 (标准I/O)
从某种意义上讲,Node其实是在C++的基础上又包了一层。和其他语言一样,Node和操作系统的交互也是通过I/O。
Node的I/O操作具体包括哪些内容呢?有这么几个:
标准I/O我们可以理解为Node 中一些事先定义好的输入,输出,以及一些为了显示在终端中的错误数据。比如常见的(STDIN),(STDOUT)标准输出,以及(STDERR)标准错误等,这些都可以被重定向并通过管道传输到其他程序,以便进一步处理、存储等。
Node 通过全局的process对象,提供了操作标准I/O的能力。
我们还是用一个简单的例子来体验一下:
process.stdin.on('data', (data) => {
process.stderr.write(`my name is:${data}` + '\n')
process.stdout.write(data.toString('base64') + '\n')
})
运行程序:
node index.js
然后在终端输入名字,可以看到如下结果
这是怎么实现的呢?
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。
echo "terrence" | node -p "process.stdin.isTTY"
如图:
这是因为程序是在shell的管道中执行。需要注意的是,isTTY是defined,并且不是false,因为这可能会导致错误。
因为标准I/O通道是根据场景从不同的构造函数内部初始化的。所以当进程直接连接到终端时,process.stdin是使用核心TTY模块的ReadStream构造函数创建的,该构造函数具有isTTY属性。然而,当I/O被重定向时,通道是从网络模块的套接字构造函数创建的,它没有isTTY属性。
文件处理能力是服务端编程的一个基本能力,Node通过fs模块提供了这种能力。
我们可以通过下面的语句生成一个1M的文件。
node -p "Buffer.allocUnsafe(1e6).toString()" > file.dat
然后修改我们的index.js
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
到终端。
setInterval(()=>process.stdout.write('s'),10).unref()
然后我们修改index.js
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。
执行下面的代码:
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中,从而为计时器队列的处理留出了空间。
本文分享自 JavaScript高级程序设计 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!