资讯专栏INFORMATION COLUMN

Node.js child_process模块解读

baiy / 861人阅读

摘要:而且方式创建的子进程与父进程之间建立了通信管道,因此子进程和父进程之间可以通过的方式发送消息。与事件的回调函数有两个参数和,代码子进程最终的退出码,如果子进程是由于接收到信号终止的话,会记录子进程接受的值。

在介绍child_process模块之前,先来看一个下面的代码。

const http = require("http");
const longComputation = () => {
  let sum = 0;
  for (let i = 0; i < 1e10; i++) {
    sum += i;
  };
  return sum;
};
const server = http.createServer();
server.on("request", (req, res) => {
  if (req.url === "/compute") {
    const sum = longComputation();
    return res.end(`Sum is ${sum}`);
  } else {
    res.end("Ok")
  }
});

server.listen(3000);

可以试一下使用上面的代码启动Node.js服务,然后打开两个浏览器选项卡分别访问/compute和/,可以发现node服务接收到/compute请求时会进行大量的数值计算,导致无法响应其他的请求(/)。

在Java语言中可以通过多线程的方式来解决上述的问题,但是Node.js在代码执行的时候是单线程的,那么Node.js应该如何解决上面的问题呢?其实Node.js可以创建一个子进程执行密集的cpu计算任务(例如上面例子中的longComputation)来解决问题,而child_process模块正是用来创建子进程的。

创建子进程的方式

child_process提供了几种创建子进程的方式

异步方式:spawn、exec、execFile、fork

同步方式:spawnSync、execSync、execFileSync

首先介绍一下spawn方法

child_process.spawn(command[, args][, options])

command: 要执行的指令
args:    传递参数
options: 配置项
const { spawn } = require("child_process");
const child = spawn("pwd");

pwd是shell的命令,用于获取当前的目录,上面的代码执行完控制台并没有任何的信息输出,这是为什么呢?

控制台之所以不能看到输出信息的原因是由于子进程有自己的stdio流(stdin、stdout、stderr),控制台的输出是与当前进程的stdio绑定的,因此如果希望看到输出信息,可以通过在子进程的stdout 与当前进程的stdout之间建立管道实现

child.stdout.pipe(process.stdout);

也可以监听事件的方式(子进程的stdio流都是实现了EventEmitter API的,所以可以添加事件监听)

child.stdout.on("data", function(data) {
  process.stdout.write(data);
});

在Node.js代码里使用的console.log其实底层依赖的就是process.stdout

除了建立管道之外,还可以通过子进程和当前进程共用stdio的方式来实现

const { spawn } = require("child_process");
const child = spawn("pwd", {
  stdio: "inherit"
});

stdio选项用于配置父进程和子进程之间建立的管道,由于stdio管道有三个(stdin, stdout, stderr)因此stdio的三个可能的值其实是数组的一种简写

pipe 相当于["pipe", "pipe", "pipe"](默认值)

ignore 相当于["ignore", "ignore", "ignore"]

inherit 相当于[process.stdin, process.stdout, process.stderr]

由于inherit方式使得子进程直接使用父进程的stdio,因此可以看到输出

ignore用于忽略子进程的输出(将/dev/null指定为子进程的文件描述符了),因此当ignore时child.stdout是null。

spawn默认情况下并不会创建子shell来执行命令,因此下面的代码会报错

const { spawn } = require("child_process");
const child = spawn("ls -l");
child.stdout.pipe(process.stdout);

// 报错
events.js:167
      throw er; // Unhandled "error" event
      ^

Error: spawn ls -l ENOENT
    at Process.ChildProcess._handle.onexit (internal/child_process.js:229:19)
    at onErrorNT (internal/child_process.js:406:16)
    at process._tickCallback (internal/process/next_tick.js:63:19)
    at Function.Module.runMain (internal/modules/cjs/loader.js:746:11)
    at startup (internal/bootstrap/node.js:238:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:572:3)
Emitted "error" event at:
    at Process.ChildProcess._handle.onexit (internal/child_process.js:235:12)
    at onErrorNT (internal/child_process.js:406:16)
    [... lines matching original stack trace ...]
    at bootstrapNodeJSCore (internal/bootstrap/node.js:572:3)

如果需要传递参数的话,应该采用数组的方式传入

const { spawn } = require("child_process");
const child = spawn("ls", ["-l"]);
child.stdout.pipe(process.stdout);

如果要执行ls -l | wc -l命令的话可以采用创建两个spawn命令的方式

const { spawn } = require("child_process");
const child = spawn("ls", ["-l"]);
const child2 = spawn("wc", ["-l"]);
child.stdout.pipe(child2.stdin);
child2.stdout.pipe(process.stdout);

也可以使用exec

const { exec } = require("child_process");
exec("ls -l | wc -l", function(err, stdout, stderr) {
  console.log(stdout);
});

由于exec会创建子shell,所以可以直接执行shell管道命令。spawn采用流的方式来输出命令的执行结果,而exec也是将命令的执行结果缓存起来统一放在回调函数的参数里面,因此exec只适用于命令执行结果数据小的情况。

其实spawn也可以通过配置shell option的方式来创建子shell进而支持管道命令,如下所示

const { spawn, execFile } = require("child_process");
const child = spawn("ls -l | wc -l", {
  shell: true
});
child.stdout.pipe(process.stdout);

配置项除了stdio、shell之外还有cwd、env、detached等常用的选项

cwd用于修改命令的执行目录

const { spawn, execFile, fork } = require("child_process");
const child = spawn("ls -l | wc -l", {
  shell: true,
  cwd: "/usr"
});
child.stdout.pipe(process.stdout);

env用于指定子进程的环境变量(如果不指定的话,默认获取当前进程的环境变量)

const { spawn, execFile, fork } = require("child_process");
const child = spawn("echo $NODE_ENV", {
  shell: true,
  cwd: "/usr"
});
child.stdout.pipe(process.stdout);
NODE_ENV=randal node b.js

// 输出结果
randal

如果指定env的话就会覆盖掉默认的环境变量,如下

const { spawn, execFile, fork } = require("child_process");
spawn("echo $NODE_TEST $NODE_ENV", {
  shell: true,
  stdio: "inherit",
  cwd: "/usr",
  env: {
    NODE_TEST: "randal-env"
  }
});

NODE_ENV=randal node b.js

// 输出结果
randal

detached用于将子进程与父进程断开连接

例如假设存在一个长时间运行的子进程

// timer.js
while(true) {

}

但是主进程并不需要长时间运行的话就可以用detached来断开二者之间的连接

const { spawn, execFile, fork } = require("child_process");
const child = spawn("node", ["timer.js"], {
  detached: true,
  stdio: "ignore"
});
child.unref();

当调用子进程的unref方法时,同时配置子进程的stdio为ignore时,父进程就可以独立退出了

execFile与exec不同,execFile通常用于执行文件,而且并不会创建子shell环境

fork方法是spawn方法的一个特例,fork用于执行js文件创建Node.js子进程。而且fork方式创建的子进程与父进程之间建立了IPC通信管道,因此子进程和父进程之间可以通过send的方式发送消息。

注意:fork方式创建的子进程与父进程是完全独立的,它拥有多带带的内存,多带带的V8实例,因此并不推荐创建很多的Node.js子进程

fork方式的父子进程之间的通信参照下面的例子

parent.js

const { fork } = require("child_process");

const forked = fork("child.js");

forked.on("message", (msg) => {
  console.log("Message from child", msg);
});

forked.send({ hello: "world" });

child.js

process.on("message", (msg) => {
  console.log("Message from parent:", msg);
});

let counter = 0;

setInterval(() => {
  process.send({ counter: counter++ });
}, 1000);
node parent.js

// 输出结果
Message from parent: { hello: "world" }
Message from child { counter: 0 }
Message from child { counter: 1 }
Message from child { counter: 2 }
Message from child { counter: 3 }
Message from child { counter: 4 }
Message from child { counter: 5 }
Message from child { counter: 6 }

回到本文初的那个问题,我们就可以将密集计算的逻辑放到多带带的js文件中,然后再通过fork的方式来计算,等计算完成时再通知主进程计算结果,这样避免主进程繁忙的情况了。

compute.js

const longComputation = () => {
  let sum = 0;
  for (let i = 0; i < 1e10; i++) {
    sum += i;
  };
  return sum;
};

process.on("message", (msg) => {
  const sum = longComputation();
  process.send(sum);
});

index.js

const http = require("http");
const { fork } = require("child_process");

const server = http.createServer();

server.on("request", (req, res) => {
  if (req.url === "/compute") {
    const compute = fork("compute.js");
    compute.send("start");
    compute.on("message", sum => {
      res.end(`Sum is ${sum}`);
    });
  } else {
    res.end("Ok")
  }
});

server.listen(3000);
监听进程事件

通过前述几种方式创建的子进程都实现了EventEmitter,因此可以针对进程进行事件监听

常用的事件包括几种:close、exit、error、message

close事件当子进程的stdio流关闭的时候才会触发,并不是子进程exit的时候close事件就一定会触发,因为多个子进程可以共用相同的stdio。

close与exit事件的回调函数有两个参数code和signal,code代码子进程最终的退出码,如果子进程是由于接收到signal信号终止的话,signal会记录子进程接受的signal值。

先看一个正常退出的例子

const { spawn, exec, execFile, fork } = require("child_process");
const child = exec("ls -l", {
  timeout: 300
});
child.on("exit", function(code, signal) {
  console.log(code);
  console.log(signal);
});

// 输出结果
0
null

再看一个因为接收到signal而终止的例子,应用之前的timer文件,使用exec执行的时候并指定timeout

const { spawn, exec, execFile, fork } = require("child_process");
const child = exec("node timer.js", {
  timeout: 300
});
child.on("exit", function(code, signal) {
  console.log(code);
  console.log(signal);
});
// 输出结果
null
SIGTERM

注意:由于timeout超时的时候error事件并不会触发,并且当error事件触发时exit事件并不一定会被触发

error事件的触发条件有以下几种:

无法创建进程

无法结束进程

给进程发送消息失败

注意当代码执行出错的时候,error事件并不会触发,exit事件会触发,code为非0的异常退出码

const { spawn, exec, execFile, fork } = require("child_process");
const child = exec("ls -l /usrs");
child.on("error", function(code, signal) {
  console.log(code);
  console.log(signal);
});
child.on("exit", function(code, signal) {
  console.log("exit");
  console.log(code);
  console.log(signal);
});

// 输出结果
exit
1
null

message事件适用于父子进程之间建立IPC通信管道的时候的信息传递,传递的过程中会经历序列化与反序列化的步骤,因此最终接收到的并不一定与发送的数据相一致。

sub.js

process.send({ foo: "bar", baz: NaN });
const cp = require("child_process");
const n = cp.fork(`${__dirname}/sub.js`);

n.on("message", (m) => {
  console.log("got message:", m);   // got message: { foo: "bar", baz: null }
});

关于message有一种特殊情况要注意,下面的message并不会被子进程接收到

const { fork } = require("child_process");

const forked = fork("child.js");

forked.send({
  cmd: "NODE_foo",
  hello: "world"
});

当发送的消息里面包含cmd属性,并且属性的值是以NODE_开头的话,这样的消息是提供给Node.js本身保留使用的,因此并不会发出message事件,而是会发出internalMessage事件,开发者应该避免这种类型的消息,并且应当避免监听internalMessage事件。

message除了发送字符串、object之外还支持发送server对象和socket对象,正因为支持socket对象才可以做到多个Node.js进程监听相同的端口号。

未完待续......

参考资料

https://medium.freecodecamp.o...

https://nodejs.org/dist/lates...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/107877.html

相关文章

  • Node.js process模块解读

    摘要:默认情况下,会打印堆栈信息到然后退出进程。适用于父子进程之间发送消息,关于如何创建父子进程会放在模块中进行。信号虽然也是用于请求终止进程,但是它与有所不同,进程可以选择响应还是忽略此信号。 process存在于全局对象上,不需要使用require()加载即可使用,process模块主要做两方面的事情 读:获取进程信息(资源使用、运行环境、运行状态) 写:执行进程操作(监听事件、调度任...

    Riddler 评论0 收藏0
  • 通过源码解析 Node.js 中进程间通信中的 socket 句柄传递

    摘要:子进程使用反序列化消息字符串为消息对象。在调用这类方法时,遍历列表中的实例发送内部消息,子进程列表中的对应项收到内部消息并处理返回,父进程中再结合返回结果和对应着这个类实例维护的信息,保证功能的正确性。 在 Node.js 中,当我们使用 child_process 模块创建子进程后,会返回一个 ChildProcess 类的实例,通过调用 ChildProcess#send(mess...

    HackerShell 评论0 收藏0
  • 深入理解Node.js 进程与线程(8000长文彻底搞懂)

    摘要:在单核系统之上我们采用单进程单线程的模式来开发。由进程来管理所有的子进程,主进程不负责具体的任务处理,主要工作是负责调度和管理。模块与模块总结无论是模块还是模块,为了解决实例单线程运行,无法利用多核的问题而出现的。 前言 进程与线程是一个程序员的必知概念,面试经常被问及,但是一些文章内容只是讲讲理论知识,可能一些小伙伴并没有真的理解,在实际开发中应用也比较少。本篇文章除了介绍概念,通过...

    Harpsichord1207 评论0 收藏0
  • Node模块--child_process

    摘要:说明模块是的原始模块主要作用执行命令行命令该模块的功能主要由函数提供区分和执行命令第一个参数是将要执行的命令,命令之间的参数使用空格分开第二个参数是回调函数,有三个参数回调中的第一个参数命令执行错误会有值,否则为回调中的第二个参数子进程 1.说明 child_process 模块是 Node.js 的原始模块: 主要作用:执行命令行命令 该模块的功能主要由 child_process...

    ormsf 评论0 收藏0
  • 初识Node.js

    摘要:一旦替换已经完成,该模块将被完全弃用。用作错误处理事件文件,由在标准功能上的简单包装器提供所有模块都提供这些对象。 Node.js简介 Node 定义 Node.js是一个建立在Chrome v8 引擎上的javascript运行时环境 Node 特点 异步事件驱动 showImg(https://segmentfault.com/img/bVMLD1?w=600&h=237); no...

    lk20150415 评论0 收藏0

发表评论

0条评论

baiy

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<