【譯】正確地關閉 Node.js app

本文是在阮一峯老師的科技愛好者週刊:第 96 期中介紹的文章。平時都是 Ctrl + C 簡單粗暴來解決,所以來看一下正確關閉 Node.js 程序的姿式。javascript

英文水平有限,若有錯誤之處還請指正。html


正文:正確地關閉 Node.js app

原文地址: Shutdown correctly Node.js apps前端

正確地關閉應用,並防止其處理新的請求是很是重要的。我將以一個服務器應用爲例子展開。java

const http = require('http');
const server = http.createServer(function (req, res) {
  setTimeout(function () {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World\n');
  }, 4000);
}).listen(9090, function (err) {
  console.log('listening http://localhost:9090/');
  console.log('pid is ' + process.pid);
});
複製代碼

正如咱們所見,服務器沒有正確地被關閉,而且請求也沒有獲得正確的處理。那麼爲了處理這個問題,首先咱們須要明白 Node.js 是如何關閉進程的。node

當關閉進程時,進程會收到一個信號。這些信號有許多種類,在這裏咱們主要關心下面三種。linux

  • SIGINT: 來自鍵盤 Ctrl + C 的組合(中斷進程)
  • SIGQUIT: 來自鍵盤 Ctrl + \(關閉進程)這個操做同時也會生成一個 core 文件
  • SIGTERM: 經過系統操做退出(例如使用 kill 命令)

當進程收到這些信號時,Node.js 會觸發事件。所以咱們就能夠對這些事件編寫對應的方法。在下面這個例子中咱們將關閉服務器,它會處理完那些被掛起的請求而且不會再接受新的請求。git

// ...
function handleExit(signal) {
  console.log(`Received ${signal}. Close my server properly.`)
  server.close(function () {
    process.exit(0);
  });
}
process.on('SIGINT', handleExit);
process.on('SIGQUIT', handleExit);
process.on('SIGTERM', handleExit);
複製代碼

如今咱們能夠很好的處理請求並正確地關閉服務器。你能夠在 Nairi Harutyunyan文章 中找到更多信息。文章對於如何正確地關閉帶有數據庫的服務器作了詳細解釋。github

如今還有名爲 Death 的 npm 包能夠處理這些邏輯。web

const ON_DEATH = require('death')
ON_DEATH(function(signal, err) {
  // clean up code here
})
複製代碼

有時這樣還不夠

最近遇到了這樣的場景,個人服務器會接受一個請求來關閉服務器。接下來我會舉一些例子。我爲服務器訂閱了一個 webhook。而這個 webhook 所接受的訂閱有限。那麼當我不想超出訂閱數量時,那我就須要正確地關閉服務器並取消訂閱。整個步驟以下:數據庫

  1. 向 webhook 發送取消訂閱的請求
  2. webhook 則會向服務器發送確認收到取消訂閱的請求
  3. 服務器通過校驗,而後取消訂閱

當進程的事件循環爲空時,程序就會自動關閉。正如上面的 一、2 步所示,若是事件循環爲空,則進程將會終止,咱們也將沒法成功取消訂閱。

下面將是新的服務器代碼:

const server = http.createServer(function (req, res) {
  const params = qs.decode(req.url.split("?")[1]);
  if (params.mode) {
    res.writeHead(200);
    res.write(params.challenge);
    res.end();
  } else {
    let body = "";
    req.on("data", chunk => {
      body += chunk;
    });
    req.on("end", () => {
      console.log('event', JSON.parse(body))
      res.writeHead(200);
      res.end();
    });
  }
}).listen(9090, function (err) {
  console.log('listening http://localhost:9090/');
  console.log('pid is ' + process.pid);
  fetch('http://localhost:3000/webhook?mode=subscribe&callback=http://localhost:9090')
});
複製代碼

這段代碼裏有兩個變化。當咱們在啓動服務器時,會向 webhook 服務器發送一個訂閱請求。

// ...
fetch('http://localhost:3000/webhook?mode=subscribe&callback=http://localhost:9090')
// ...
複製代碼

同時咱們還在服務器處理請求的方法中添加一個 challenge 的 token,來確認代表 response 來自於 webhook。

// ...
if (params.mode) {
  res.writeHead(200);
  res.write(params.challenge);
  res.end();
} else {
 // ...
}
// ...
複製代碼

讓咱們再來修改一下 handleExit 方法,讓它向 webhook 發送一個請求,告訴它取消訂閱。

function handleExit(signal) {
  console.log(`Received ${signal}. Close my server properly.`)
  fetch('http://localhost:3000/webhook?mode=unsubscribe&callback=http://localhost:9090')
}
複製代碼

咱們須要添加代碼讓服務器在 webhook 取消訂閱時可以關閉。

// ...
if (params.mode) {
  res.writeHead(200);
  res.write(params.challenge);
  res.end();
  if (params.mode === 'unsubscribe') {
    server.close(function () {
      process.exit(0);
    });
  }
} else {
  // ...
}
// ...
複製代碼

當 webhook 確認取消訂閱時,咱們將關閉服務器,這樣一來它就不會再處理新的請求並正常地退出。而這也是 Twitch API 中 webhook 的 subscription/unsubscription 的工做原理。

讓咱們看一下實際關閉服務器時的狀況。我添加了一些 log 讓其更容易理解。

如圖所示,服務器並不能正常關閉。服務器的進程在收到 webhook 返回的請求以前就關閉了,所以 webhook 仍然會往服務器發送請求。

爲了解決這個問題,咱們須要防止 Node.js 進程退出。咱們可使用 process.stdin.resume 方法。這個方法會使進程暫停並覆蓋默認方法,好比 Ctrl + C。

注:我本身跑了一下代碼,即便不加 process.stdin.resume 也能夠正常取消訂閱,但若是不添加 handleExit,就會出現沒法取消訂閱的狀況。

const http = require('http');
process.stdin.resume();
// ...
複製代碼

那麼如今再來看一下?

如今的服務器會在處理完請求後退出。

我建立了一個 Git 的 repo 上面有文中所提到的代碼。

但願能有所幫助。


歡迎關注個人公衆號:此方的手帳。一個與你分享生活、共同進步的公衆號。除了前端還有高達系列的分享哦~

相關文章
相關標籤/搜索