作者选择了COVID-19 救济基金来接受捐赠,作为Write for DOnations计划的一部分。
介绍
当用户执行单个Node.js程序时,它作为单个操作系统 (OS)进程运行,该进程代表正在运行的程序实例。在该过程中,Node.js 在单个线程上执行程序。正如本系列前面的How To Write Asynchronous Code in Node.js教程中提到的,因为只有一个线程可以运行在一个进程上,所以在JavaScript 中执行需要很长时间的操作会阻塞 Node.js 线程并延迟执行的其他代码。解决此问题的关键策略是启动子进程或由另一个进程创建的进程,当面临长时间运行的任务时。当启动一个新进程时,操作系统可以采用多处理技术来确保主 Node.js 进程和附加子进程并发或同时运行。
Node.js 包含child_process
模块,该模块具有创建新进程的功能。除了处理长时间运行的任务外,该模块还可以与操作系统交互并运行shell命令。系统管理员可以使用 Node.js 运行 shell 命令来构建和维护作为Node.js 模块而不是shell 脚本的操作。
在本教程中,您将在执行一系列示例 Node.js 应用程序的同时创建子进程。您将创建与流程child_process
通过经检索子过程的结果模块缓冲区或字符串的exec()
函数,然后从与数据流spawn()
功能。最后,您将使用fork()
创建另一个 Node.js 程序的子进程,您可以在它运行时与之通信。为了说明这些概念,您将编写一个列出目录内容的程序、一个查找文件的程序以及一个具有多个端点的 Web 服务器。
先决条件
-
您必须安装 Node.js 才能运行这些示例。本教程使用版本 10.22.0。要在 macOS 或 Ubuntu 18.04 上安装它,请按照如何在 macOS 上安装 Node.js 和创建本地开发环境或如何在 Ubuntu 18.04 上安装 Node.js 的使用 PPA 安装部分中的步骤进行操作。
-
本文使用一个创建 Web 服务器的示例来解释该
fork()
功能的工作原理。要熟悉创建 Web 服务器,您可以阅读我们关于如何使用 HTTP 模块在 Node.js 中创建 Web 服务器的指南。
第 1 步 – 创建一个子进程 exec()
开发人员通常会创建子进程以在他们的操作系统上执行命令,当他们需要使用 shell 操作 Node.js 程序的输出时,例如使用 shell 管道或重定向。exec()
Node.js 中的函数创建一个新的 shell 进程并在该 shell 中执行命令。命令的输出保存在内存中的缓冲区中,您可以通过传入的回调函数接受该缓冲区exec()
。
让我们开始在 Node.js 中创建我们的第一个子进程。首先,我们需要设置我们的编码环境来存储我们将在本教程中创建的脚本。在终端中,创建一个名为 的文件夹child-processes
:
- mkdir child-processes
使用以下cd
命令在终端中输入该文件夹:
- cd child-processes
创建一个名为的新文件listFiles.js
并在文本编辑器中打开该文件。在本教程中,我们将使用nano,一个终端文本编辑器:
- nano listFiles.js
我们将编写一个 Node.js 模块,使用该exec()
函数来运行ls
命令。该ls
命令列出目录中的文件和文件夹。该程序获取ls
命令的输出并将其显示给用户。
在文本编辑器中,添加以下代码:
const { exec } = require('child_process');
exec('ls -lh', (error, stdout, stderr) => {
if (error) {
console.error(`error: ${error.message}`);
return;
}
if (stderr) {
console.error(`stderr: ${stderr}`);
return;
}
console.log(`stdout:\n${stdout}`);
});
我们首先使用JavaScript 解构exec()
从child_process
模块中导入命令。导入后,我们使用该函数。第一个参数是我们想要运行的命令。在这种情况下,它是,它以长格式列出当前目录中的所有文件和文件夹,在输出的顶部以人类可读的单位列出总文件大小。exec()
ls -lh
第二个参数是三个参数的回调函数:error
,stdout
,和stderr
。如果命令运行失败,error
将捕获失败的原因。如果 shell 找不到您要执行的命令,就会发生这种情况。如果命令成功执行,它写入标准输出流的stdout
任何数据都会在 中捕获,并且它写入标准错误流的任何数据都会在中捕获stderr
。
注意:记住error
和之间的区别很重要stderr
。如果命令本身无法运行,error
将捕获错误。如果命令运行但将输出返回到错误流,stderr
将捕获它。最具弹性的 Node.js 程序将处理子进程的所有可能输出。
在我们的回调函数中,我们首先检查是否收到错误。如果我们这样做了,我们用 显示错误message
(Error
对象的一个属性)console.error()
并用 结束函数return
。然后我们检查该命令是否打印了错误消息,return
如果是。如果命令执行成功,我们记录其输出到控制台用console.log()
。
让我们运行这个文件来查看它的运行情况。首先,nano
按保存并退出CTRL+X
。
回到您的终端,使用以下node
命令运行您的应用程序:
- node listFiles.js
您的终端将显示以下输出:
Outputstdout:
total 4.0K
-rw-rw-r-- 1 sammy sammy 280 Jul 27 16:35 listFiles.js
这child-processes
以长格式列出目录的内容,以及顶部内容的大小。您的结果将有您自己的用户和组来代替sammy
。这表明listFiles.js
程序成功运行了 shell 命令ls -lh
。
现在让我们看看另一种执行并发进程的方法。Node.js 的child_process
模块也可以通过该execFile()
函数运行可执行文件。execFile()
和exec()
函数之间的主要区别在于,的第一个参数execFile()
现在是可执行文件的路径,而不是命令。该可执行文件的输出被存储在象一个缓冲器exec()
,这是我们通过与一个回调函数访问error
,stdout
和stderr
参数。
注意: Windows 中的脚本(如.bat
和.cmd
文件)无法运行,execFile()
因为该函数在运行文件时不会创建外壳。在 Unix、Linux 和 macOS 上,可执行脚本并不总是需要 shell 才能运行。但是,Windows 机器需要一个 shell 来执行脚本。要在 Windows 上执行脚本文件,请使用exec()
,因为它会创建一个新的 shell。或者,您可以使用spawn()
,稍后将在此步骤中使用。
但是,请注意,您可以.exe
使用execFile()
. 此限制仅适用于需要 shell 执行的脚本文件。
让我们首先添加一个可执行脚本execFile()
来运行。我们将编写一个bash脚本,该脚本从 Node.js 网站下载Node.js 徽标,并对其进行Base64编码以将其数据转换为一串ASCII字符。
创建一个名为的新 shell 脚本文件processNodejsImage.sh
:
- nano processNodejsImage.sh
现在编写一个脚本来下载图像并进行 base64 转换:
#!/bin/bash
curl -s https://nodejs.org/static/images/logos/nodejs-new-pantone-black.svg > nodejs-logo.svg
base64 nodejs-logo.svg
第一个语句是shebang 语句。当我们想要指定一个 shell 来执行我们的脚本时,它在 Unix、Linux 和 macOS 中使用。第二个语句是curl
命令。的卷曲工具,其命令curl
,是一个命令行工具,可以将数据传输到和从一个服务器。我们使用cURL从网站下载Node.js logo,然后我们使用重定向将下载的数据保存到一个新文件中nodejs-logo.svg
。最后一条语句使用该base64
实用程序对nodejs-logo.svg
我们使用 cURL 下载的文件进行编码。然后脚本将编码后的字符串输出到控制台。
在继续之前保存并退出。
为了让我们的 Node 程序运行 bash 脚本,我们必须使其可执行。为此,请运行以下命令:
- chmod u+x processNodejsImage.sh
这将使您的当前用户有权执行该文件。
有了我们的脚本,我们可以编写一个新的 Node.js 模块来执行它。此脚本将用于execFile()
在子进程中运行脚本,捕获任何错误并将所有输出显示到控制台。
在您的终端中,创建一个名为 的新 JavaScript 文件getNodejsImage.js
:
- nano getNodejsImage.js
在文本编辑器中输入以下代码:
const { execFile } = require('child_process');
execFile(__dirname + '/processNodejsImage.sh', (error, stdout, stderr) => {
if (error) {
console.error(`error: ${error.message}`);
return;
}
if (stderr) {
console.error(`stderr: ${stderr}`);
return;
}
console.log(`stdout:\n${stdout}`);
});
我们使用 JavaScript 解构execFile()
从child_process
模块中导入函数。然后我们使用该函数,将文件路径作为名字传递。__dirname
包含写入它的模块的目录路径。Node.js__dirname
在模块运行时向模块提供变量。通过使用__dirname
,我们的脚本将始终processNodejsImage.sh
在不同的操作系统中找到文件,无论我们在哪里运行getNodejsImage.js
. 请注意,我们目前的项目设置,getNodejsImage.js
并且processNodejsImage.sh
必须是在同一文件夹。
第二个参数是与所述回调error
,stdout
和stderr
参数。与我们之前使用的示例一样exec()
,我们检查脚本文件的每个可能输出并将它们记录到控制台。
在您的文本编辑器中,保存此文件并退出编辑器。
在您的终端中,使用node
来执行模块:
- node getNodejsImage.js
运行此脚本将产生如下输出:
Outputstdout:
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNDQyLjQgMjcwLjkiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0iYiIgeDE9IjE4MC43IiB5MT0iODAuNyIge
...
请注意,我们截断了本文中的输出,因为它的尺寸很大。
在对图像进行 base64 编码之前,processNodejsImage.sh
首先要下载它。您还可以通过检查当前目录来验证您是否下载了图像。
执行listFiles.js
以在我们的目录中查找更新的文件列表:
- node listFiles.js
该脚本将在终端上显示类似于以下内容:
Outputstdout:
total 20K
-rw-rw-r-- 1 sammy sammy 316 Jul 27 17:56 getNodejsImage.js
-rw-rw-r-- 1 sammy sammy 280 Jul 27 16:35 listFiles.js
-rw-rw-r-- 1 sammy sammy 5.4K Jul 27 18:01 nodejs-logo.svg
-rwxrw-r-- 1 sammy sammy 129 Jul 27 17:56 processNodejsImage.sh
我们现在已经成功地processNodejsImage.sh
使用该execFile()
函数在 Node.js 中作为子进程执行了。
该exec()
和execFile()
功能上都可以运行操作系统的一个Node.js的子进程的shell命令。Node.js 还提供了另一种具有类似功能的方法,spawn()
. 不同之处在于,我们不是一次性获取所有 shell 命令的输出,而是通过流将它们分块获取。在下一节中,我们将使用该spawn()
命令创建子进程。
第 2 步 – 创建子进程 spawn()
该spawn()
函数在进程中运行命令。此函数通过流 API返回数据。因此,要获得子进程的输出,我们需要监听流事件。
Node.js 中的流是事件发射器的实例。如果您想了解更多关于监听事件以及与流交互的基础知识,您可以阅读我们关于在 Node.js 中使用事件发射器的指南。
这往往是一个好主意,选择spawn()
了exec()
或execFile()
当命令要运行可以输出大量的数据。使用exec()
和使用的缓冲区,execFile()
所有处理过的数据都存储在计算机的内存中。对于大量数据,这会降低系统性能。对于流,数据以小块的形式进行处理和传输。因此,您可以在任何时候处理大量数据而无需使用过多内存。
让我们看看如何使用它spawn()
来创建子进程。我们将编写一个新的 Node.js 模块来创建一个子进程来运行find
命令。我们将使用该find
命令列出当前目录中的所有文件。
创建一个名为 的新文件findFiles.js
:
- nano findFiles.js
在您的文本编辑器中,首先调用以下spawn()
命令:
const { spawn } = require('child_process');
const child = spawn('find', ['.']);
我们首先spawn()
从child_process
模块中导入函数。然后我们调用该spawn()
函数来创建一个执行find
命令的子进程。我们在child
变量中保存对流程的引用,我们将使用它来监听其流式事件。
in 的第一个参数spawn()
是要运行的命令,在本例中为find
。第二个参数是一个数组,其中包含已执行命令的参数。在这种情况下,我们告诉 Node.js 执行find
带有参数.
的命令,从而使命令查找当前目录中的所有文件。终端中的等效命令是find .
.
使用exec()
和execFile()
函数,我们将参数与命令一起写入一个字符串中。但是,使用spawn()
,命令的所有参数都必须输入到数组中。那是因为spawn()
与exec()
和不同execFile()
,它不会在运行进程之前创建新的 shell。要在一个字符串中包含带有参数的命令,您还需要 Node.js 来创建一个新的 shell。
让我们通过为命令的输出添加侦听器来继续我们的模块。添加以下突出显示的行:
const { spawn } = require('child_process');
const child = spawn('find', ['.']);
child.stdout.on('data', data => {
console.log(`stdout:\n${data}`);
});
child.stderr.on('data', data => {
console.error(`stderr: ${data}`);
});
命令可以在stdout
流或stderr
流中返回数据,因此您为两者都添加了侦听器。您可以通过调用on()
每个流的对象的方法来添加侦听器。data
来自流的事件为我们提供了该流的命令输出。每当我们在任一流上获取数据时,我们都会将其记录到控制台。
然后我们侦听另外两个事件:error
命令执行失败或被中断时的close
事件,以及命令完成执行时的事件,从而关闭流。
在文本编辑器中,通过编写以下突出显示的行来完成 Node.js 模块:
const { spawn } = require('child_process');
const child = spawn('find', ['.']);
child.stdout.on('data', (data) => {
console.log(`stdout:\n${data}`);
});
child.stderr.on('data', (data) => {
console.error(`stderr: ${data}`);
});
child.on('error', (error) => {
console.error(`error: ${error.message}`);
});
child.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});
对于error
和close
事件,您可以直接在child
变量上设置侦听器。侦听error
事件时,如果发生,Node.js 会提供一个Error
对象。在这种情况下,您记录错误的message
属性。
在监听close
事件时,Node.js 提供了命令的退出代码。退出代码表示命令是否成功运行。当命令运行没有错误时,它会返回退出代码的最低可能值:0
。当执行出错时,它返回一个非零代码。
该模块已完成。保存并退出nano
用CTRL+X
。
现在,使用以下node
命令运行代码:
- node findFiles.js
完成后,您将看到以下输出:
Outputstdout:
.
./findFiles.js
./listFiles.js
./nodejs-logo.svg
./processNodejsImage.sh
./getNodejsImage.js
child process exited with code 0
我们找到了当前目录中所有文件的列表以及命令的退出代码,这是0
它成功运行的结果。虽然我们当前目录有少量文件,但如果我们在主目录中运行此代码,我们的程序将为我们的用户列出每个可访问文件夹中的每个文件。因为它具有如此大的潜在输出,所以使用该spawn()
函数是最理想的,因为它的流不需要与大缓冲区一样多的内存。
到目前为止,我们已经使用函数来创建子进程以在我们的操作系统中执行外部命令。Node.js 还提供了一种创建执行其他 Node.js 程序的子进程的方法。让我们fork()
在下一节中使用该函数为 Node.js 模块创建子进程。
第 3 步 – 创建一个子进程 fork()
Node.js 提供了一个fork()
函数,它是 的变体spawn()
,用于创建一个也是 Node.js 进程的子进程。使用的主要好处fork()
创建一个Node.js的处理过spawn()
或者exec()
是,fork()
让家长和子进程之间的通信。
使用fork()
,除了从子进程检索数据之外,父进程还可以向正在运行的子进程发送消息。同样,子进程可以向父进程发送消息。
让我们看一个示例,其中使用fork()
创建新的 Node.js 子进程可以提高我们应用程序的性能。Node.js 程序在单个进程上运行。因此,诸如迭代大型循环或解析大型JSON 文件之类的 CPU 密集型任务会阻止其他 JavaScript 代码运行。对于某些应用程序,这不是一个可行的选择。如果 Web 服务器被阻塞,则在阻塞代码完成执行之前,它无法处理任何新的传入请求。
让我们通过创建具有两个端点的 Web 服务器在实践中看到这一点。一个端点会进行缓慢的计算,从而阻塞 Node.js 进程。另一个端点将返回一个 JSON 对象说hello
.
首先,创建一个名为 的新文件httpServer.js
,其中包含我们 HTTP 服务器的代码:
- nano httpServer.js
我们将从设置 HTTP 服务器开始。这包括导入http
模块、创建请求侦听器函数、创建服务器对象以及侦听服务器对象上的请求。如果您想更深入地了解在 Node.js 中创建 HTTP 服务器或想要复习,您可以阅读我们的指南,了解如何使用 HTTP 模块在 Node.js 中创建 Web 服务器。
在文本编辑器中输入以下代码以设置 HTTP 服务器:
const http = require('http');
const host = 'localhost';
const port = 8000;
const requestListener = function (req, res) {};
const server = http.createServer(requestListener);
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
});
此代码设置了一个 HTTP 服务器,该服务器将在http://localhost:8000
. 它使用模板文字来动态生成该 URL。
接下来,我们将故意编写一个缓慢的函数,该函数在循环中计数 50 亿次。在requestListener()
函数前添加如下代码:
...
const port = 8000;
const slowFunction = () => {
let counter = 0;
while (counter < 5000000000) {
counter++;
}
return counter;
}
const requestListener = function (req, res) {};
...
这使用箭头函数语法来创建一个计数为的while
循环5000000000
。
为了完成这个模块,我们需要在requestListener()
函数中添加代码。我们的函数将调用slowFunction()
on 子路径,并为另一个返回一个小的 JSON 消息。将以下代码添加到模块中:
...
const requestListener = function (req, res) {
if (req.url === '/total') {
let slowResult = slowFunction();
let message = `{"totalCount":${slowResult}}`;
console.log('Returning /total results');
res.setHeader('Content-Type', 'application/json');
res.writeHead(200);
res.end(message);
} else if (req.url === '/hello') {
console.log('Returning /hello results');
res.setHeader('Content-Type', 'application/json');
res.writeHead(200);
res.end(`{"message":"hello"}`);
}
};
...
如果用户在/total
子路径到达服务器,那么我们运行slowFunction()
. 如果我们在打/hello
子路径,我们回到这个JSON消息:{"message":"hello"}
。
按 保存并退出文件CTRL+X
。
要进行测试,请使用以下命令运行此服务器模块node
:
- node httpServer.js
当我们的服务器启动时,控制台将显示以下内容:
OutputServer is running on http://localhost:8000
现在,为了测试我们模块的性能,打开另外两个终端。在第一个终端中,使用curl
命令向/total
端点发出请求,我们预计它会很慢:
- curl http://localhost:8000/total
在另一个终端中,使用curl
向/hello
端点发出请求,如下所示:
- curl http://localhost:8000/hello
第一个请求将返回以下 JSON:
Output{"totalCount":5000000000}
而第二个请求将返回此 JSON:
Output{"message":"hello"}
/hello
只有在请求到 之后才能完成请求/total
。在slowFunction()
从执行虽然仍处于环封锁了所有其他代码。您可以通过查看原始终端中记录的 Node.js 服务器输出来验证这一点:
OutputReturning /total results
Returning /hello results
为了在仍然接受传入请求的同时处理阻塞代码,我们可以将阻塞代码移动到带有fork()
. 我们将把阻塞代码移到它自己的模块中。然后,当有人访问/total
端点并监听来自该子进程的结果时,Node.js 服务器将创建一个子进程。
通过首先创建一个名为的新模块来重构服务器,该模块getCount.js
将包含slowFunction()
:
- nano getCount.js
现在slowFunction()
再次输入代码:
const slowFunction = () => {
let counter = 0;
while (counter < 5000000000) {
counter++;
}
return counter;
}
由于这个模块将是一个用 创建的子进程fork()
,我们还可以添加代码在slowFunction()
完成处理后与父进程通信。添加以下代码块,使用 JSON 向父进程发送消息以返回给用户:
const slowFunction = () => {
let counter = 0;
while (counter < 5000000000) {
counter++;
}
return counter;
}
process.on('message', (message) => {
if (message == 'START') {
console.log('Child process received START message');
let slowResult = slowFunction();
let message = `{"totalCount":${slowResult}}`;
process.send(message);
}
});
让我们分解这个代码块。创建的父进程和子进程之间的消息fork()
可通过 Node.js 全局process
对象访问。我们向process
变量添加一个侦听器以查找message
事件。一旦我们收到一个message
事件,我们就会检查它是否是START
事件。START
当有人访问/total
端点时,我们的服务器代码将发送事件。收到该事件后,我们运行slowFunction()
并使用该函数的结果创建一个 JSON 字符串。我们process.send()
用来向父进程发送消息。
getCount.js
输入CTRL+X
nano保存并退出。
现在,让我们修改httpServer.js
文件,以便它不调用slowFunction()
,而是创建一个执行 的子进程getCount.js
。
重新打开httpServer.js
具有nano
:
- nano httpServer.js
首先,fork()
从child_process
模块中导入函数:
const http = require('http');
const { fork } = require('child_process');
...
接下来,我们slowFunction()
将从这个模块中删除并修改requestListener()
函数以创建一个子进程。更改文件中的代码,使其看起来像这样:
...
const port = 8000;
const requestListener = function (req, res) {
if (req.url === '/total') {
const child = fork(__dirname + '/getCount');
child.on('message', (message) => {
console.log('Returning /total results');
res.setHeader('Content-Type', 'application/json');
res.writeHead(200);
res.end(message);
});
child.send('START');
} else if (req.url === '/hello') {
console.log('Returning /hello results');
res.setHeader('Content-Type', 'application/json');
res.writeHead(200);
res.end(`{"message":"hello"}`);
}
};
...
当有人访问/total
端点时,我们现在创建一个新的子进程fork()
。的参数fork()
是 Node.js 模块的路径。在这种情况下,它是getCount.js
我们当前目录中的文件,我们从__dirname
. 对这个子进程的引用存储在一个变量中child
。
然后我们向child
对象添加一个侦听器。此侦听器捕获子进程提供给我们的任何消息。在这种情况下,getCount.js
将返回一个 JSON 字符串,其中包含while
循环计数的总数。当我们收到该消息时,我们将 JSON 发送给用户。
我们使用变量的send()
函数child
来给它一个消息。该程序发送消息START
,该消息开始slowFunction()
在子进程中执行。
nano
输入保存并退出CTRL+X
。
要fork()
在 HTTP 服务器上使用made测试改进,请先执行以下httpServer.js
文件node
:
- node httpServer.js
和以前一样,它在启动时会输出以下消息:
OutputServer is running on http://localhost:8000
为了测试服务器,我们将需要额外的两个终端,就像我们第一次做的那样。如果它们仍然打开,您可以重复使用它们。
在第一个终端中,使用curl
命令向/total
端点发出请求,这需要一段时间来计算:
- curl http://localhost:8000/total
在另一个终端中,用于curl
向/hello
端点发出请求,端点在短时间内响应:
- curl http://localhost:8000/hello
第一个请求将返回以下 JSON:
Output{"totalCount":5000000000}
而第二个请求将返回此 JSON:
Output{"message":"hello"}
与我们第一次尝试不同的是,第二个请求/hello
立即运行。您可以通过查看日志进行确认,日志如下所示:
OutputChild process received START message
Returning /hello results
Returning /total results
这些日志显示对/hello
端点的请求在子进程创建之后但在子进程完成其任务之前运行。
由于我们使用 移动了子进程中的阻塞代码fork()
,因此服务器仍然能够响应其他请求并执行其他 JavaScript 代码。由于fork()
函数的消息传递能力,我们可以控制子进程何时开始活动,并且可以将数据从子进程返回到父进程。
结论
在本文中,您使用了各种函数在 Node.js 中创建了一个子进程。您首先创建了子进程,exec()
用于从 Node.js 代码运行 shell 命令。然后,您使用该execFile()
函数运行了一个可执行文件。您查看了该spawn()
函数,它也可以运行命令,但通过流返回数据,并且不会启动像exec()
and这样的 shell execFile()
。最后,您使用该fork()
函数允许父进程和子进程之间进行双向通信。
要了解有关该child_process
模块的更多信息,您可以阅读Node.js 文档。如果您想继续学习 Node.js,可以返回到如何在 Node.js 中编码系列,或在我们的Node 主题页面浏览编程项目和设置。