如何在 Node.js 中启动子进程

作者选择了 COVID-19 救援基金作为 Write for Donations计划的一部分接受捐款。

介绍

当用户运行一个单一的 Node.js程序时,它作为一个单一的操作系统(OS) process 运行,代表该程序运行的实例。在该过程中,Node.js 运行在单个线程上。正如本系列中早些时候所提到的 How To Write Asynchronous Code in Node.js教程中所述,因为只有一个线程可以在一个过程上运行,在 JavaScript中执行需要很长时间的操作可以阻止 Node.js 线程并延迟其他代码的执行。围绕这个问题工作的关键策略是启动一个 _child 过程,或者由另一个过程创建的过程,当面对长时间任务。当一个新的过程启动时

Node.js 包括 child_process 模块,该模块具有创建新进程的功能。除了处理漫长的任务外,该模块还可以与操作系统交互并运行 shell命令。系统管理员可以使用 Node.js 运行 shell 命令来构建和维护其操作作为 Node.js 模块而不是 shell 脚本

在本教程中,您将在执行一系列样本 Node.js 应用程序时创建儿童流程. 您将使用child_process模块创建流程,通过使用缓冲器(https://andsky.com/tech/tutorials/using-buffers-in-node-js)或使用exec()函数(https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback)的字符串获取儿童流程的结果,然后从具有spawn()函数(https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options)的数据流程中创建流程。

前提条件

要在 macOS 或 Ubuntu 18.04 上安装此功能,请遵循 如何在 macOS 上安装 Node.js 并创建本地开发环境如何在 Ubuntu 18.04 上安装 Node.js中的步骤。

步骤 1 – 使用exec()创建一个孩子过程

开发人员通常会创建小程序来在操作系统上执行命令,当他们需要使用壳来操纵 Node.js 程序的输出时,例如使用壳管道或重定向. Node.js 中的 exec() 函数创建了一个新的壳过程并在该壳中执行一个命令。

让我们开始在 Node.js 中创建我们的第一个小程序,首先,我们需要设置我们的编码环境来存储我们将在本教程中创建的脚本,在终端中创建一个名为小程序的文件夹:

1mkdir child-processes

使用cd命令在终端中输入该文件夹:

1cd child-processes

创建一个名为listFiles.js的新文件,并在文本编辑器中打开该文件. 在本教程中,我们将使用 nano,一个终端文本编辑器:

1nano listFiles.js

我们将编写一个 Node.js 模块,该模块使用exec()函数来运行ls命令。ls命令列出一个目录中的文件和文件夹。

在文本编辑器中,添加以下代码:

 1[label ~/child-processes/listFiles.js]
 2const { exec } = require('child_process');
 3
 4exec('ls -lh', (error, stdout, stderr) => {
 5  if (error) {
 6    console.error(`error: ${error.message}`);
 7    return;
 8  }
 9
10  if (stderr) {
11    console.error(`stderr: ${stderr}`);
12    return;
13  }
14
15  console.log(`stdout:\n${stdout}`);
16});

我们首先从child_process模块中导入exec()命令,使用JavaScript destructuring(https://andsky.com/tech/tutorials/understanding-destructuring-rest-parameters-and-spread-syntax-in-javascript#destructuring)。一旦导入,我们使用了exec()函数。第一个参数是我们想要执行的命令。

第二个参数是带有三个参数的回调函数: error, stdoutstderr. 如果命令失败了, error 会捕捉它失败的原因。如果壳无法找到你试图执行的命令,则会发生这种情况。如果命令成功执行,它写给 标准输出流的任何数据都会被捕捉到 stdout,并且它写给 标准错误流的任何数据都会被捕捉到 `stderr。

<$>[注] **注:**重要的是要记住错误stderr之间的差异.如果命令本身失败,则错误会捕捉错误。

在我们的回复函数中,我们首先检查是否收到错误。如果我们这样做,我们将错误的消息(错误对象的属性)显示为console.error(),然后将函数结束为return

让我们运行这个文件,以便在行动中看到它。首先,通过按CTRL+X来保存和退出nano

回到您的终端,使用node命令运行您的应用程序:

1node listFiles.js

您的终端将显示以下输出:

1[secondary_label Output]
2stdout:
3total 4.0K
4-rw-rw-r-- 1 sammy sammy 280 Jul 27 16:35 listFiles.js

这将列出长格式的child-processes目录的内容,以及顶部的内容的大小. 您的结果将有自己的用户和组,而不是sammy

现在,让我们看看执行并发过程的另一种方式。Node.js的child_process模块也可以运行具有execFile()函数的可执行文件。execFile()exec()函数之间的关键区别在于,execFile()的第一个参数现在是可执行文件的路径,而不是命令。可执行文件的输出存储在像exec()这样的缓冲器中,我们通过使用error,stdoutstderr参数的回调函数访问。

<$>[注] 注: Windows 中的脚本(如 .bat.cmd 文件)不能用 execFile() 运行,因为该函数在运行文件时不会创建壳。在 Unix、Linux 和 macOS 上,可执行的脚本并不总是需要壳才能运行。

但是,请注意,您可以在 Windows 中使用execFile()成功执行.exe 文件. 此限制仅适用于需要壳执行的脚本文件。

我们会写一个 bash脚本,它从 Node.js 网站下载了 Node.js 标志并编码了 Base64以将其数据转换为 ASCII字符串。

创建一个名为 `processNodejsImage.sh’的新壳脚本文件:

1nano processNodejsImage.sh

现在写一个脚本来下载图像,并将其转换为 base64:

1[label ~/child-processes/processNodejsImage.sh]
2#!/bin/bash
3curl -s https://nodejs.org/static/images/logos/nodejs-new-pantone-black.svg > nodejs-logo.svg
4base64 nodejs-logo.svg

第一个陈述是 shebang statement. 它在 Unix、Linux 和 macOS 中使用,当我们想指定一个壳来执行我们的脚本时. 第二个陈述是curl命令. 该 cURL 实用程序,其命令是curl,是一个可以将数据传输到服务器和从服务器的命令行工具。 我们使用 cURL 来从网站下载 Node.js 标志,然后我们使用 redirection将下载的数据保存到一个新的文件nodejs-logo.svg。 最后一个陈述使用base64实用程序来编码用 cURL下载的nodejs-logo.svg`文件。 该脚本然后将编码的字符串

保存和退出,然后继续。

为了使我们的 Node 程序运行 bash 脚本,我们必须使其可执行。

1chmod u+x processNodejsImage.sh

这将给您的当前用户执行该文件的权限。

有了我们的脚本,我们可以编写一个新的Node.js模块来执行它. 这个脚本将使用execFile()在一个小程序中运行脚本,捕捉任何错误并显示所有输出到控制台。

在您的终端中,创建一个名为getNodejsImage.js的新 JavaScript 文件:

1nano getNodejsImage.js

在文本编辑器中输入以下代码:

 1[label ~/child-processes/getNodejsImage.js]
 2const { execFile } = require('child_process');
 3
 4execFile(__dirname + '/processNodejsImage.sh', (error, stdout, stderr) => {
 5  if (error) {
 6    console.error(`error: ${error.message}`);
 7    return;
 8  }
 9
10  if (stderr) {
11    console.error(`stderr: ${stderr}`);
12    return;
13  }
14
15  console.log(`stdout:\n${stdout}`);
16});

我们使用JavaScript破结构来从child_process模块中导入execFile()函数,然后使用该函数,将文件路径作为第一名。__dirname包含该模块的目录路径。Node.js在模块运行时向模块提供__dirname变量。使用__dirname,我们的脚本将始终在不同的操作系统中找到processNodejsImage.sh文件,无论我们在哪里运行getNodejsImage.js

第二个参数是带有错误stdoutstderr参数的回调,与我们之前使用的例子一样,我们检查了脚本文件的每个可能的输出,并将其登录到控制台。

在文本编辑器中,保存此文件并离开编辑器。

在您的终端中,使用节点来执行模块:

1node getNodejsImage.js

运行此脚本将产生这样的输出:

1[secondary_label Output]
2stdout:
3PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNDQyLjQgMjcwLjkiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0iYiIgeDE9IjE4MC43IiB5MT0iODAuNyIge
4...

请注意,由于其大小,我们在本文中削减了输出。

在 base64 编码图像之前, `processNodejsImage.sh 首先将其下载,您也可以通过检查当前目录来验证您已下载图像。

运行listFiles.js以在我们的目录中找到最新的文件列表:

1node listFiles.js

脚本将在终端上显示类似于以下内容:

1[secondary_label Output]
2stdout:
3total 20K
4-rw-rw-r-- 1 sammy sammy 316 Jul 27 17:56 getNodejsImage.js
5-rw-rw-r-- 1 sammy sammy 280 Jul 27 16:35 listFiles.js
6-rw-rw-r-- 1 sammy sammy 5.4K Jul 27 18:01 nodejs-logo.svg
7-rwxrw-r-- 1 sammy sammy 129 Jul 27 17:56 processNodejsImage.sh

现在,我们已经成功地在 Node.js 中使用execFile()函数执行了processNodejsImage.sh作为一个小程序。

exec()execFile()函数可以在 Node.js 子进程中运行操作系统壳上的命令. Node.js 还提供了另一种具有类似功能的方法, spawn()

步骤 2 — 使用spawn()创建一个孩子的过程

该函数通过 stream API返回数据,因此,要获取孩子进程的输出,我们需要听到流 events

Node.js 中的流程是事件发射器的实例. 如果您想了解更多关于收听事件和与流程互动的基本知识,请参阅我们的指南 使用 Node.js 中的事件发射器

选择spawn()而不是exec()execFile()通常是一个好主意,当你想要执行的命令可以输出大量数据时,使用缓冲器,如exec()execFile()所使用,所有处理的数据都存储在计算机的内存中。对于大量数据,这可能会降低系统性能。 通过流量,数据被处理和转移成小块。

我们会写一个新的Node.js模块,创建一个孩子进程来运行find命令,我们会使用find命令列出当前目录中的所有文件。

创建一个名为findFiles.js的新文件:

1nano findFiles.js

在文本编辑器中,请通过调用spawn()命令开始:

1[label ~/child-processes/findFiles.js]
2const { spawn } = require('child_process');
3
4const child = spawn('find', ['.']);

我们首先从child_process模块中导入了spawn()函数,然后调用了spawn()函数,以创建一个执行find命令的子过程。

「spawn()」中的第一个参数是要运行的命令,在这种情况下是「find」。第二个参数是包含执行命令的参数的 array

使用exec()execFile()函数,我们在一个字符串中编写了参数和命令,但是,在spawn()中,所有参数都必须输入数组,因为spawn(),与exec()execFile()不同,在运行一个过程之前不会创建一个新的壳。

让我们通过为命令的输出添加听者来继续我们的模块。

 1[label ~/child-processes/findFiles.js]
 2const { spawn } = require('child_process');
 3
 4const child = spawn('find', ['.']);
 5
 6child.stdout.on('data', data => {
 7  console.log(`stdout:\n${data}`);
 8});
 9
10child.stderr.on('data', data => {
11  console.error(`stderr: ${data}`);
12});

命令可以返回stdout流或stderr流中的数据,因此您可以为两者添加听众。您可以通过呼叫每个流对象的on()方法来添加听众。

然后我们听到另外两个事件:命令执行失败或被中断的错误事件,以及命令完成执行时的关闭事件,从而关闭流。

在文本编辑器中,通过写下以下突出的行来完成 Node.js 模块:

 1[label ~/child-processes/findFiles.js]
 2const { spawn } = require('child_process');
 3
 4const child = spawn('find', ['.']);
 5
 6child.stdout.on('data', (data) => {
 7  console.log(`stdout:\n${data}`);
 8});
 9
10child.stderr.on('data', (data) => {
11  console.error(`stderr: ${data}`);
12});
13
14child.on('error', (error) => {
15  console.error(`error: ${error.message}`);
16});
17
18child.on('close', (code) => {
19  console.log(`child process exited with code ${code}`);
20});

对于错误关闭事件,您将直接在孩子变量上设置一个倾听器。当听到错误事件时,如果出现一个,Node.js 会提供一个错误对象。

当听到关闭事件时,Node.js 会提供命令的 _exit 代码。 输出代码表示命令是否成功运行。 当命令运行时没有错误,它会返回输出代码的最低可能值: `0。

保存和退出nanoCTRL+X

现在,用node命令运行代码:

1node findFiles.js

一旦完成,你会发现以下输出:

 1[secondary_label Output]
 2stdout:
 3.
 4./findFiles.js
 5./listFiles.js
 6./nodejs-logo.svg
 7./processNodejsImage.sh
 8./getNodejsImage.js
 9
10child process exited with code 0

虽然我们的当前目录有少量的文件,如果我们在我们的主目录中运行这个代码,我们的程序会列出每个文件在每个可访问的文件夹为我们的用户。

到目前为止,我们已经使用函数来创建子程式来在我们的操作系统中执行外部命令. 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程序运行在一个单一的过程上。因此,CPU密集的任务,如重复大循环或解析大型JSON文件(https://andsky.com/tech/tutorials/how-to-work-with-json-in-javascript)可以阻止其他JavaScript代码运行。

让我们通过创建一个有两个端点的 Web 服务器来实践一下这一点,一个端点会做一个缓慢的计算,这会阻止 Node.js 过程,另一个端点会返回一个 JSON 对象,说你好

首先,创建一个名为httpServer.js的新文件,其中将包含我们的 HTTP 服务器的代码:

1nano httpServer.js

我们将从设置 HTTP 服务器开始,这包括导入http模块,创建请求倾听函数,创建服务器对象,并听取服务器对象上的请求。

在文本编辑器中输入以下代码来设置 HTTP 服务器:

 1[label ~/child-processes/httpServer.js]
 2const http = require('http');
 3
 4const host = 'localhost';
 5const port = 8000;
 6
 7const requestListener = function (req, res) {};
 8
 9const server = http.createServer(requestListener);
10server.listen(port, host, () => {
11  console.log(`Server is running on http://${host}:${port}`);
12});

此代码设置了一个 HTTP 服务器,该服务器将运行在 http://localhost:8000. 它使用 template literals来动态生成该 URL。

接下来,我们会写一个故意缓慢的函数,该函数在循环中计数50亿次。

 1[label ~/child-processes/httpServer.js]
 2...
 3const port = 8000;
 4
 5const slowFunction = () => {
 6  let counter = 0;
 7  while (counter < 5000000000) {
 8    counter++;
 9  }
10
11  return counter;
12}
13
14const requestListener = function (req, res) {};
15...

这使用箭头函数语法(https://andsky.com/tech/tutorials/how-to-define-functions-in-javascript#arrow-functions)创建一个计算为5000000000‘而’循环

要完成此模块,我们需要将代码添加到requestListener()函数中,我们的函数将调用slowFunction()在子路径上,然后返回另一个小JSON消息。

 1[label ~/child-processes/httpServer.js]
 2...
 3const requestListener = function (req, res) {
 4  if (req.url === '/total') {
 5    let slowResult = slowFunction();
 6    let message = `{"totalCount":${slowResult}}`;
 7
 8    console.log('Returning /total results');
 9    res.setHeader('Content-Type', 'application/json');
10    res.writeHead(200);
11    res.end(message);
12  } else if (req.url === '/hello') {
13    console.log('Returning /hello results');
14    res.setHeader('Content-Type', 'application/json');
15    res.writeHead(200);
16    res.end(`{"message":"hello"}`);
17  }
18};
19...

如果用户在/total子路径上到达服务器,那么我们会运行slowFunction()

保存和退出文件,按CTRL+X

要测试,请使用node运行此服务器模块:

1node httpServer.js

当我们的服务器启动时,控制台将显示以下内容:

1[secondary_label Output]
2Server is running on http://localhost:8000

现在,要测试我们的模块的性能,打开两个额外的终端. 在第一个终端中,使用‘curl’命令向‘/total’终端点提出请求,我们预计会缓慢:

1curl http://localhost:8000/total

在另一个终端中,使用‘curl’来向‘/hello’终端发出这样的请求:

1curl http://localhost:8000/hello

第一个请求将返回以下JSON:

1[secondary_label Output]
2{"totalCount":5000000000}

虽然第二个请求会返回这个 JSON:

1[secondary_label Output]
2{"message":"hello"}

请求到 /hello 仅在请求到 /total 后完成. slowFunction() 阻止了其他代码在其循环中执行,您可以通过查看原始终端登录的 Node.js 服务器输出来验证此情况:

1[secondary_label Output]
2Returning /total results
3Returning /hello results

为了处理封锁代码,同时仍然接受接入请求,我们可以将封锁代码移动到一个fork()的子进程中,我们将将封锁代码移动到自己的模块中。

重构服务器,首先创建一个名为getCount.js的新模块,该模块将包含slowFunction():

1nano getCount.js

现在再输入slowFunction()的代码:

1[label ~/child-processes/getCount.js]
2const slowFunction = () => {
3  let counter = 0;
4  while (counter < 5000000000) {
5    counter++;
6  }
7
8  return counter;
9}

由于这个模块将是用fork()创建的子过程,我们还可以添加代码以便在 slowFunction()完成处理时与母进程进行通信。

 1[label ~/child-processes/getCount.js]
 2const slowFunction = () => {
 3  let counter = 0;
 4  while (counter < 5000000000) {
 5    counter++;
 6  }
 7
 8  return counter;
 9}
10
11process.on('message', (message) => {
12  if (message == 'START') {
13    console.log('Child process received START message');
14    let slowResult = slowFunction();
15    let message = `{"totalCount":${slowResult}}`;
16    process.send(message);
17  }
18});

让我们把这个代码区块分开来。由fork()创建的父母和子女的过程之间的信息可以通过Node.js全球 ‘process’ 对象访问。我们将一个听者添加到‘process’变量中,以寻找‘message’事件。一旦我们收到一个‘message’事件,我们会检查它是否是‘START’事件。当有人访问‘/total’终端点时,我们的服务器代码会发送‘START’事件。

保存和退出getCount.js通过在 nano 中输入CTRL+X

现在,让我们修改httpServer.js文件,这样而不是调用slowFunction(),它会创建一个孩子过程,执行getCount.js

重新打开httpServer.jsnano:

1nano httpServer.js

首先,从child_process模块中导入fork()函数:

1[label ~/child-processes/httpServer.js]
2const http = require('http');
3const { fork } = require('child_process');
4...

接下来,我们将从这个模块中删除slowFunction(),并修改requestListener()函数以创建一个小程序。

 1[label ~/child-processes/httpServer.js]
 2...
 3const port = 8000;
 4
 5const requestListener = function (req, res) {
 6  if (req.url === '/total') {
 7    const child = fork(__dirname + '/getCount');
 8
 9    child.on('message', (message) => {
10      console.log('Returning /total results');
11      res.setHeader('Content-Type', 'application/json');
12      res.writeHead(200);
13      res.end(message);
14    });
15
16    child.send('START');
17  } else if (req.url === '/hello') {
18    console.log('Returning /hello results');
19    res.setHeader('Content-Type', 'application/json');
20    res.writeHead(200);
21    res.end(`{"message":"hello"}`);
22  }
23};
24...

当某人进入/total终端点时,我们现在创建一个新的子进程,使用fork()fork()的参数是 Node.js 模块的路径,在这种情况下,它是我们当前目录中的getCount.js文件,我们从__dirname中获取。

然后我们将一个倾听器添加到孩子对象中,这个倾听器捕捉了孩子进程给我们的任何消息,在这种情况下,getCount.js将返回一个 JSON 字符串,其总数由循环计算。

我们使用send()函数的child变量给它一个消息. 这个程序发送的消息START,这开始执行的slowFunction()在孩子的过程。

保存和退出nano通过键入CTRL+X

要測試使用 HTTP 伺服器所做的「fork()」的改進,請先執行「httpServer.js」檔案與「node」:

1node httpServer.js

与之前一样,它在启动时会发出以下消息:

1[secondary_label Output]
2Server is running on http://localhost:8000

为了测试服务器,我们需要另外两个终端,就像我们第一次一样,如果它们仍然开放的话,您可以重新使用它们。

在第一个终端中,使用弯曲命令向/total终端点提出请求,计算需要一段时间:

1curl http://localhost:8000/total

在另一个终端中,使用‘curl’向‘/hello’终端发出请求,该终端在短时间内响应:

1curl http://localhost:8000/hello

第一个请求将返回以下JSON:

1[secondary_label Output]
2{"totalCount":5000000000}

虽然第二个请求会返回这个 JSON:

1[secondary_label Output]
2{"message":"hello"}

与我们第一次尝试这种情况不同,第二个请求即时运行,您可以通过查看日志来确认,这些日志将看起来像这样:

1[secondary_label Output]
2Child process received START message
3Returning /hello results
4Returning /total results

这些日志显示,对/hello终端点的请求在创建儿童进程后运行,但在儿童进程完成任务之前运行。

由于我们使用fork()移动了儿童进程中的封锁代码,服务器仍然能够响应其他请求并执行其他JavaScript代码.由于fork()函数的消息传递能力,我们可以控制儿童进程开始活动时,我们可以从儿童进程返回数据到家长进程。

结论

在本文中,您使用了各种函数在 Node.js 中创建一个子程式。您首先创建了子程式使用exec()来从 Node.js 代码中运行壳命令。您然后运行了一个可执行的文件使用execFile()函数。您查看了spawn()函数,该函数也可以运行命令,但通过流返回数据,而不会启动像exec()execFile()这样的壳。

要了解更多关于child_process的模块,您可以阅读 Node.js 文档. 如果您想继续学习 Node.js,您可以返回 如何在 Node.js 系列中编码,或浏览我们的 Node 主题页面的编程项目和设置。

Published At
Categories with 技术
comments powered by Disqus