首頁  >  文章  >  web前端  >  深入淺析Node.js中建立子進程的方法

深入淺析Node.js中建立子進程的方法

青灯夜游
青灯夜游轉載
2021-10-12 10:05:482980瀏覽

這篇文章帶大家了解一下Node.js中的子進程,介紹Node.js中創建子進程的四種方法,希望對大家有幫助!

深入淺析Node.js中建立子進程的方法

眾所周知,Node.js 是單執行緒、非同步非阻塞的程式語言,那該如何充分利用多核心 CPU 的優勢呢?這就需要用到child_process 模組來建立子進程了,在Node.js 中,有四種方法可以建立子進程:

  • ##exec

  • execFile

  • #spawn

  • ##fork

  • 【推薦學習:《
nodejs 教學

》】上面四個方法都會回傳

ChildProcess

實例(繼承自EventEmitter),此實例有三個標準的 stdio 串流:

    ##child.stdin
  • child.stdout
  • fchild.stderr
  • 子程序生命週期內可以註冊監聽的事件有:

exit

:子程序結束時觸發,參數為code 錯誤碼和signal 中斷訊號。

close

:子程序結束並且 stdio 流被關閉時觸發,參數同

exit 事件。 disconnect

:父程序呼叫

child.disconnect() 或子程序呼叫 process.disconnect() 時觸發。 error

:子進程無法建立、或無法被殺、或發送訊息給子進程失敗時觸發。

message

:子進程透過

process.send() 發送訊息時觸發。 spawn

:當子程序建立成功時觸發(Node.js v15.1版本才加入此事件)。

exec

execFile 方法也額外提供了一個回呼函數,會在子程序終止的時候觸發。接下來進行詳細分析:exec

exec 方法用於執行 bash 命令,它的參數是一個命令字串。例如統計目前目錄下的檔案數量,用 exec 函數的寫法為:

const { exec } = require("child_process")
exec("find . -type f | wc -l", (err, stdout, stderr) => {
  if (err) return console.error(`exec error: ${err}`)
  console.log(`Number of files ${stdout}`)
})

exec 會新建一個子進程,然後快取它的運作結果,執行結束後呼叫回呼函數。

可能你已經想到了,exec 指令是比較危險的,假如把使用者提供的字串當作exec 函數的參數,會面臨指令列注入的風險,例如:

find . -type f | wc -l; rm -rf /;

另外,由於exec 會在記憶體中快取全部的輸出結果,當資料比較大的時候,spawn 會是更好的選擇。

execFile

execFile 和exec 的差別在於它並不會建立shell,而是直接執行指令,所以會更有效率一點,例如:

const { execFile } = require("child_process")
const child = execFile("node", ["--version"], (error, stdout, stderr) => {
  if (error) throw error
  console.log(stdout)
})

由於沒有建立shell,程式的參數會作為陣列傳入,因此具有較高的安全性。

spawn

spawn 函數和 execFile 類似,預設不會開啟shell,但差異在於execFile 會快取命令列的輸出,然後把結果傳入回呼函數中,而spawn 則是以流的方式輸出,有了流,就能非常方便的對接輸入和輸出了,例如典型的

wc

命令:

const child = spawn("wc")
process.stdin.pipe(child.stdin)
child.stdout.on("data", data => {
  console.log(`child stdout:\n${data}`)
})
此時就會從命令列stdin 獲取輸入,當使用者觸發回車ctrl D

時就開始執行指令,並把結果從stdout 輸出。

wc 是Wo​​rd Count 的縮寫,用於統計單字數,語法為:

wc [OPTION]... [FILE]...
如果在終端機上輸入wc 指令並回車,這時候統計的是從鍵盤輸入終端機中的字符,再次按回車鍵,然後按

Ctrl D

會輸出統計的結果。

透過管道還可以組合複雜的命令,例如統計當前目錄下的檔案數量,在Linux 命令列中會這麼寫:

find . -type f | wc -l
在Node.js 中的寫法和命令列一模一樣:

const find = spawn("find", [".", "-type", "f"])
const wc = spawn("wc", ["-l"])
find.stdout.pipe(wc.stdin)
wc.stdout.on("data", (data) => {
  console.log(`Number of files ${data}`)
})

spawn 有豐富的自訂配置,例如:

const child = spawn("find . -type f | wc -l", {
  stdio: "inherit", // 继承父进程的输入输出流
  shell: true, // 开启命令行模式
  cwd: "/Users/keliq/code", // 指定执行目录
  env: { ANSWER: 42 }, // 指定环境变量(默认是 process.env)
  detached: true, // 作为独立进程存在
})

fork

fork 函數是spawn 函數的變體,使用fork 建立的子行程和父行程之間會自動建立一個通訊通道,子行程的全域物件process 上面會掛載send 方法。例如父進程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)

當呼叫

fork("child.js")

的時候,實際上就是用node 來執行該檔案中的程式碼,相當於

spawn('node', ['./child.js'])fork 的一個典型的應用場景如下:假如現在用 Node.js 建立一個 http 服務,當路由為 compute

的時候,執行一個耗時的運算。

const http = require("http")
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);
可以用下面的程式碼來模擬該耗時的運算:
const longComputation = () => {
  let sum = 0;
  for (let i = 0; i < 1e9; i++) {
    sum += i
  }
  return sum
}

那么在上线后,只要服务端收到了 compute 请求,由于 Node.js 是单线程的,耗时运算占用了 CPU,用户的其他请求都会阻塞在这里,表现出来的现象就是服务器无响应。

解决这个问题最简单的方法就是把耗时运算放到子进程中去处理,例如创建一个 compute.js 的文件,代码如下:

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

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

再把服务端的代码稍作改造:

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)

这样的话,主线程就不会阻塞,而是继续处理其他的请求,当耗时运算的结果返回后,再做出响应。其实更简单的处理方式是利用 cluster 模块,限于篇幅原因,后面再展开讲。

总结

掌握了上面四种创建子进程的方法之后,总结了以下三条规律:

  • 创建 node 子进程用 fork,因为自带通道方便通信。
  • 创建非 node 子进程用 execFile 或 spawn。如果输出内容较少用 execFile,会缓存结果并传给回调方便处理;如果输出内容多用 spawn,使用流的方式不会占用大量内存。
  • 执行复杂的、固定的终端命令用 exec,写起来更方便。但一定要记住 exec 会创建 shell,效率不如 execFile 和 spawn,且存在命令行注入的风险。

更多编程相关知识,请访问:编程视频!!

以上是深入淺析Node.js中建立子進程的方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除