你須要瞭解的有關 Node.js 的全部信息

Node.js 是當前用來構建可擴展的、高效的 REST API's 的最流行的技術之一。它還能夠用來構建混合移動應用、桌面應用甚至用於物聯網領域。html

我真的很喜歡它,我已經使用 Node.js 工做了 6 年。這篇文章試圖成爲了解 Node.js 工做原理的終極指南。node

Node.js 以前的世界

多線程服務器

Web 應用程序是用一個 client/server(客戶端/服務器)模式所編寫的,其中 client 將向 server 請求資源而且 server 將會根據這個資源以響應。server 僅在 client 請求時作出響應,並在每次響應後關閉鏈接。git

這種模式是有效的,由於對服務器的每個請求都須要時間和資源(內存、CPU 等)。服務器必須完成上一個請求,才能接受下一個請求。github

因此,服務器在必定的時間內只處理一個請求?這不徹底是,當服務器收到一個新請求時,這個請求將會被一個線程處理。數據庫

簡而言之,線程是 CPU 爲執行一小段指令所花費的時間和資源。 話雖如此,服務器一次要處理多個請求,每一個線程一個(也能夠稱爲 thread-per-request 模式)。編程

注:thread-per-request 意爲每個請求一個線程json

要同時處理 N 個請求,服務器就須要 N 個線程。若是如今有 N+1 個請求,它就必須等待,直到 N 個線程中的任何一個可用。api

在多線程服務器示例中,服務器同時最多容許 4 個請求(線程)當接下來收到 3 個請求時,這些請求必須等待直到這 4 個線程中的任何一個可用。數組

解決此限制的一種方法是向服務器添加更多資源(內存,CPU內核等),但這可能根本不是一個好主意...緩存

固然,會有技術限制。

阻塞 I/O

服務器中的線程數不只僅是這裏惟一的問題。也許你想知道爲何一個線程不能同時處理 2 個或更多的請求?這是由於阻塞了 Input/Output 操做。

假設你正在開發一個在線商店應用,而且它須要一個頁面,用戶能夠在其中查看您的全部產品。

用戶訪問 yourstore.com/products 服務器將從數據庫中獲取你的所有產品來呈現一個 HTML 文件,這很簡單吧?

可是,後面會發生什麼?...

  • 1. 當用戶訪問 /products 時,須要執行特定的方法或函數來知足請求,所以會有一小段代碼來解析這個請求的 url 並定位到正確的方法或函數。線程正在工做。✔️

  • 2. 該方法或函數以及第一行將被執行。線程正在工做。✔️

  • 3. 由於你是一名優秀的開發者,你會保存全部的系統日誌在一個文件中,要確保路由執行了正確的方法/函數,你的日誌要增長一個字符串 「Method X executing!!」(某某方法正在執行),這是一個阻塞的 I/O 操做。線程正在等待。❌

  • 4. 日誌已被保存而且下一行將被執行。線程正在工做。✔️

  • 5. 如今是時候去數據庫並獲取全部產品了,一個簡單的查詢,例如 SELECT * FROM products 操做,可是您猜怎麼着?這是一個阻塞的 I/O 操做。線程正在等待。❌

  • 6. 你會獲得一個全部的產品列表,但要確保將它們記錄下來。線程正在等待。❌

  • 7. 使用這些產品,是時候渲染模版了,可是在渲染它以前,你應該先讀取它。線程正在等待。❌

  • 8. 模版引擎完成它的工做,並將響應發送到客戶端。線程再次開始工做。✔️

  • 9. 線程是自由的(空閒的),像鳥兒同樣。🕊️

I/O 操做有多慢?這得須要看狀況。

讓咱們檢查如下表格:

操做 CPU 時鐘週期數
CPU 寄存器 3 ticks
L1 Cache(一級緩存) 8 ticks
L2 Cache(二級緩存) 12 ticks
RAM(隨機存取存儲器) 150 ticks
Disk(磁盤) 30,000,000 ticks
Network(網絡) 250,000,000 ticks

譯者備註:時鐘週期也稱(tick、clock cycle、clock period 等),指一個硬件在被使用過程當中,被劃分爲多個時間週期,當咱們須要比較不一樣硬件的性能時,就在不一樣硬件之上測試同一個軟件,觀察它們的時鐘週期時間和週期數,若是時鐘週期時間越長、週期數越多,就意味着這個硬件須要的性能較低。

磁盤和網絡操做太慢了。您的系統進行了多少次查詢或外部 API 調用?

在恢復過程當中,I/O 操做使得線程等待且浪費資源。

C10K 問題

早在 2000 年代初期,服務器和客戶端機器運行緩慢。這個問題是在一臺服務器機器上同時運行 10,000 個客戶端連接。

爲何咱們傳統的 「thread-per-request」 模式不可以解決這個問題?如今讓咱們作一些數學運算。

本地線程實現爲每一個線程分配大約 1 MB 的內存,因此 10K 線程就須要 10GB 的 RAM,請記住這僅僅是在 2000 年代初期!!

現在,服務器和客戶端的計算能力比這更好,幾乎任何編程語言和框架都解決了這個問題。實際,該問題已更新爲在一臺服務器上處理 10 million(1000 萬) 個客戶端連接(也稱 C10M 問題)。

JavaScript 進行救援?

劇透提醒 🚨🚨🚨!!

Node.js 解決了這個 C10K 問題... 可是爲何?

JavaScript 服務端早在 2000 年代並非什麼新鮮事,它基於 「thread-per-request」 模式在 Java 虛擬機之上有一些實現,例如,RingoJS、AppEngineJS。

可是,若是那不能解決 C10K 問題,爲何 Node.js 能夠?好吧,由於它是單線程的。

Node.js 和 Event Loop

Node.js

Node.js 是一個構建在 Google Chrome's JavaScript 引擎(V8 引擎)之上的服務端平臺,可將 JavaScript 代碼編譯爲機器代碼。

Node.js 基於事件驅動、非阻塞 I/O 模型,從而使其輕巧和高效。它不是一個框架,也不是一個庫,它是一個運行時。

一個簡單的例子:

// Importing native http module
const http = require('http');

// Creating a server instance where every call
// the message 'Hello World' is responded to the client
const server = http.createServer(function(request, response) {
  response.write('Hello World');
  response.end();
});

// Listening port 8080
server.listen(8080);
複製代碼

非阻塞 I/O

Node.js 是非阻塞 I/O,這意味着:

  • 主線程不會在 I/O 操做中阻塞。
  • 服務器將會繼續參加請求。
  • 咱們將使用異步代碼。

讓咱們寫一個例子,在每一次 /home 請求時,服務器將響應一個 HTML 頁面,不然服務器響應一個 'Hello World' 文本。要響應 HTML 頁面,首先要讀取這個文件。

home.html

<html>
  <body>
    <h1>This is home page</h1>
  </body>
</html>
複製代碼

index.js

const http = require('http');
const fs = require('fs');

const server = http.createServer(function(request, response) {
  if (request.url === '/home') {
    fs.readFile(`${ __dirname }/home.html`, function (err, content) {
      if (!err) {
        response.setHeader('Content-Type', 'text/html');
        response.write(content);
      } else {
        response.statusCode = 500;
        response.write('An error has ocurred');
      }

      response.end();
    });
  } else {
    response.write('Hello World');
    response.end();
  }
});

server.listen(8080);  
複製代碼

若是這個請求的 url 是 /home,咱們使用 fs 本地模塊讀取這個 home.html 文件。

傳遞給 http.createServer 和 fs.readFile 的函數稱爲回調。這些功能將在未來的某個時間執行(第一個功能將在收到一個請求時執行,第二個功能將在文件讀取而且緩衝以後執行)。

在讀取文件時,Node.js 仍然能夠處理請求,甚至再次讀取文件,all at once in a single thread... but how?!

The Event Loop(事件循環)

事件循環是 Node.js 背後的魔力,簡而言之,事件循環其實是一個無限循環,而且是線程裏惟一可用的。

Libuv 是一個實現此模式的 C 語言庫,是 Node.js 核心模塊的一部分。閱讀關於 Libuv 的更多內容 here

事件循環須要經歷 6 個階段,全部階段的執行被稱爲 tick。

  • timers:這個階段執行定時器 setTimeout() 和 setInterval() 的回調函數。
  • pending callbacks:幾乎全部的回調在這裏執行,除了 close 回調、定時器 timers 階段的回調和 setImmediate()。
  • idle, prepare: 僅在內部應用。
  • poll:檢索新的 I/O 事件;適當時 Node 將在此處阻塞。
  • check:setImmediate() 回調函數將在這裏執行。
  • close callbacks: 一些準備關閉的回調函數,如:socket.on('close', ...)。

好的,因此只有一個線程而且該線程是一個 EventLoop,可是 I/O 操做由誰來執行呢?

注意 📢📢📢!!!

當 Event Loop 須要執行 I/O 操做時,它將從一個池(經過 Libuv 庫)中使用系統線程,當這個做業完成時,回調將排隊等待在 「pending callbacks」 階段被執行。

那不是很完美嗎?

CPU 密集型任務問題

Node.js 彷佛很完美,你能夠用它來構建任何你想要的東西。

讓咱們構建一個 API 來計算質數。

質數又稱素數。一個大於 1 的天然數,除了 1 和它自身外,不能被其餘天然數整除的數叫作質數;

給一個數 N,這個 API 必須計算並在一個數組中返回 N 個天然數。

primes.js

function isPrime(n) {
  for(let i = 2, s = Math.sqrt(n); i <= s; i++)
    if(n % i === 0) return false;
  return n > 1;
}

function nthPrime(n) {
  let counter = n;
  let iterator = 2;
  let result = [];

  while(counter > 0) {
    isPrime(iterator) && result.push(iterator) && counter--;
    iterator++;
  }

  return result;
}

module.exports = { isPrime, nthPrime };
複製代碼

index.js

const http = require('http');
const url = require('url');
const primes = require('./primes');

const server = http.createServer(function (request, response) {
  const { pathname, query } = url.parse(request.url, true);

  if (pathname === '/primes') {
    const result = primes.nthPrime(query.n || 0);
    response.setHeader('Content-Type', 'application/json');
    response.write(JSON.stringify(result));
    response.end();
  } else {
    response.statusCode = 404;
    response.write('Not Found');
    response.end();
  }
});

server.listen(8080);
複製代碼

primes.js 是質數功能實現,isPrime 檢查給予的參數 N 是否爲質數,若是是一個質數 nthPrime 將返回 n 個質數

index.js 建立一個服務並在每次請求 /primes 時使用這個庫。經過 query 傳遞參數。

獲取 20 前的質數,咱們發起一個請求 http://localhost:8080/primes?n=2

假設有 3 個客戶端訪問這個驚人的非阻塞 API:

  • 第一個每秒請求前 5 個質數。
  • 第二個每秒請求前 1,000 個質數
  • 第三個請求一次性輸入前 10,000,000,000 個質數,可是...

當咱們的第三個客戶端發送請求時,客戶端將會被阻塞,由於質數庫會佔用大量的 CPU。主線程忙於執行密集型的代碼將沒法作其它任何事情。

可是 Libuv 呢?若是你記得這個庫使用系統線程幫助 Node.js 作一些 I/O 操做以免主線程阻塞,那你是對的,這個能夠幫助咱們解決這個問題,可是使用 Libuv 庫咱們必需要使用 C++ 語言編寫。

值得慶祝的是 Node.js v10.5 引入了工做線程。

工做線程

文檔所述

工做線程對於執行 CPU 密集型的 JavaScript 操做很是有用。 它們在 I/O 密集型的工做中用途不大。 Node.js 的內置的異步 I/O 操做比工做線程效率更高。

修改代碼

如今修復咱們的初始化代碼:

primes-workerthreads.js

const { workerData, parentPort } = require('worker_threads');

function isPrime(n) {
  for(let i = 2, s = Math.sqrt(n); i <= s; i++)
    if(n % i === 0) return false;
  return n > 1;
}

function nthPrime(n) {
  let counter = n;
  let iterator = 2;
  let result = [];

  while(counter > 0) {
    isPrime(iterator) && result.push(iterator) && counter--;
    iterator++;
  }

  return result;
}

parentPort.postMessage(nthPrime(workerData.n));
複製代碼

index-workerthreads.js

const http = require('http');
const url = require('url');
const { Worker } = require('worker_threads');

const server = http.createServer(function (request, response) {                                                                                              
  const { pathname, query } = url.parse(request.url, true);

  if (pathname === '/primes') {                                                                                                                                    
    const worker = new Worker('./primes-workerthreads.js', { workerData: { n: query.n || 0 } });

    worker.on('error', function () {
      response.statusCode = 500;
      response.write('Oops there was an error...');
      response.end();
    });

    let result;
    worker.on('message', function (message) {
      result = message;
    });

    worker.on('exit', function () {
      response.setHeader('Content-Type', 'application/json');
      response.write(JSON.stringify(result));
      response.end();
    });
  } else {
    response.statusCode = 404;
    response.write('Not Found');
    response.end();
  }
});

server.listen(8080);
複製代碼

index-workerthreads.js 在每一個請求中將建立一個 Worker 實例,在一個工做線程中加載並執行 primes-workerthreads.js 文件。當這個質數列表計算完成,這個 message 消息將會被觸發,接收信息並賦值給 result。因爲這個 job 已完成,將會再次觸發 exit 事件,容許主線程發送數據給到客戶端。

primes-workerthreads.js 變化小一點。它導入 workerData(從主線程傳遞參數),parentPort 這是咱們向主線程發送消息的方式。

如今讓咱們再次作 3 個客戶端例子,看看會發生什麼:

主線程再也不阻塞 🎉🎉🎉🎉🎉!!!!!

它的工做方式與預期的同樣,可是生成工做線程並非最佳實踐,建立新線程並不便宜。必定先建立一個線程池。

結論

Node.js 是一項功能強大的技術,值得學習。

個人建議老是很好奇,若是您知道事情的進展,您將作出更好的決定。

夥計們,到此爲止。但願您對 Node.js 有所瞭解。

感謝您的閱讀,下一篇文章中相見。❤️

相關文章
相關標籤/搜索