一.Node.js 締造的傳奇
<code>I have a job now, andthis
guyis
the reason why I have that now. His hobby projectis
what I usefor
living. Thanks. —— Shajan Jacob /<code>
2009 年 Ryan Dahl 在 JSConf EU 大會上推出了 Node.js,最初是希望能夠通過異步模型突破傳統 Web 服務器的高併發瓶頸,之後愈漸發展成熟,應用越來越廣,出現了繁榮的 Node.js 生態
藉助 Node.js 走出瀏覽器之後,JavaScript 語言也 一發不可收拾 :
<code>Any
application that can be written in JavaScript, will eventually be written in JavaScript. —— Jeff Atwood /<code>
(摘自 The Principle of Least Power )
早在 2017 年,NPM 就憑藉茫茫多的社區模塊成為了 世界上最大的 package registry ,目前模塊數量已經 超過 125 萬 ,並且仍在快速增長中(每天新增900多個)
甚至 Node.js 工程師已經成為了一種新興職業,那麼, 帶有傳奇色彩的 Node.js 本身是怎麼實現的呢?
二.Node.js 架構概覽
JS 代碼跑在 V8 引擎上,Node.js 內置的 fs 、 http 等核心模塊通過 C++ Bindings 調用 libuv、c-ares、llhttp 等 C/C++類庫,從而接入操作系統提供的平臺能力
其中, 最重要的部分是 V8 和 libuv
三.源碼依賴
V8
<code>V8is
Google’sopen
source high-performance JavaScript and WebAssembly engine, writtenin
C++. Itis
usedin
Chrome andin
Node.js, among others. /<code>
一個用 C++寫的 JavaScript 引擎,由 Google 維護,用於 Chrome 瀏覽器和 Node.js
libuv
<code>libuvis
cross-platform support library which was originally writtenfor
Node.js. It’s designed around theevent
-driven asynchronous I/O model. /<code>
為 Node.js 量身打造,用 C 寫的跨平臺異步 I/O 庫,提供了非阻塞的文件系統、DNS、網絡、子進程、管道、信號、輪詢和流式處理機制:
對於無法在操作系統層面異步去做的工作,通過線程池來完成,如文件 I/O、DNS 查詢等,具體原因見 Complexities in File I/O
P.S.線程池的容量可以配置,默認是 4 個線程,具體見 Thread pool work scheduling
此外, Node.js 中的事件循環、事件隊列也都是由 libuv 提供的 :
<code>Libuv provides the entireevent
loop functionality to NodeJS including theevent
queuing mechanism. /<code>
具體運作機制如下圖:
其它依賴庫
另外,還依賴一些 C/C++庫:
- llhttp :用 TypeScript 和 C 寫的輕量級 HTTP 解析庫,比之前的 http_parser 快 1.5 倍,不含任何系統調用和內存分配(也不緩存數據),因此每個請求的內存佔用極小
- c-ares :一個 C 庫,用來處理異步的 DNS 請求,對應 Node.js 中 dns 模塊提供的 resolve() 系列方法
- OpenSSL :一個通用的加密庫,多用於網絡傳輸中的 TLS 和 SSL 協議實現,對應 Node.js 中的 tls 、 crypto 模塊
- zlib :提供快速壓縮和解壓支持
P.S.關於 Node.js 源碼依賴的更多信息,見 Dependencies
四.核心模塊
像瀏覽器提供的 DOM/BOM API 一樣,Node.js 不僅提供了 JavaScript 運行時環境,還擴展出了一系列平臺 API,例如:
- 文件系統相關:對應 fs 模塊
- HTTP 通信:對應 http 模塊
- 操作系統相關:對應 os 模塊
- 多進程:對應 child_process 、 cluster 模塊
這些內置模塊稱為 核心模塊,為邁出瀏覽器世界的 JavaScript 長上了手腳
五.C++ Bindings
在核心模塊之下,有一層 C++ Bindings,將上層的 JavaScript 代碼與下層 C/C++類庫橋接起來
底層模塊為了更好的性能,採用 C/C++實現,而上層的 JavaScript 代碼無法直接與 C/C++通信,因而需要一個橋樑(即 Binding):
<code>Bindings,as
the name implies, are glue codes that “bind” one languagewith
another so that they can talkwith
each other. Inthis
case
(Node.js), bindings simply expose core Node.js internal libraries writtenin
C/C++ (c-ares, zlib, OpenSSL, llhttp, etc.) to JavaScript. /<code>
另一方面,通過 Bindings 也可以複用可靠的老牌開源類庫,而不必手搓所有底層模塊
以文件 I/O 為例,讀取當前 JS 文件內容並輸出到標準輸出:
<code>const
fs =require
('fs'
)const
path =require
('path'
)const
filePath = path.resolve(__filename);function
callback
(data
) {return
data.toString() }const
readFileAsync =(
filePath
) => {return
new
Promise
((
resolve, reject
) => { fs.readFile(filePath,(
err, data
) => {if
(err)return
reject(err)return
resolve(callback(data)) }) }) } (()
=> { readFileAsync(filePath) .then(console
.log) .catch(console
.error) })()/<code>
然而,其中用到的 fs.readFile 接口既不是 V8 提供的,也不是 JS 自帶的,而是由 Node.js 以 C++ Binding 的形式藉助 libuv 實現的:
<code>const
binding = internalBinding('fs'
);const
{ FSReqCallback, statValues } = binding;function
readFile
(path, options, callback
) { callback = maybeCallback(callback || options); options = getOptions(options, {flag
:'r'
});if
(!ReadFileContext) ReadFileContext =require
('internal/fs/read_file_context'
);const
context =new
ReadFileContext(callback, options.encoding); context.isUserFd = isFd(path);const
req =new
FSReqCallback(); req.context = context; req.oncomplete = readFileAfterOpen;if
(context.isUserFd) { process.nextTick(function
tick
() { req.oncomplete(null
, path); });return
; } path = getValidatedPath(path);const
flagsNumber = stringToFlags(options.flags); binding.open(pathModule.toNamespacedPath(path), flagsNumber,0o666
, req); }/<code>
最後的 binding.open 是一個 C++調用,用來打開文件描述符,三個參數分別是文件路徑, C++ fopen 的文件訪問模式串(如 r 、 w+ ),以及八進制格式的文件讀寫權限( 666 表示每個人都有讀寫權限),和接收返回數據的 req 回調
其中, internalBinding 是個 C++ binding loader, internalBinding('fs') 實際加載的 C++代碼位於 node/src/node_file.cc
至此,關鍵的部分差不多都清楚了,那麼,一段 Node.js 代碼究竟是怎樣運行的呢?
六.運行原理
首先,編寫的 JavaScript 代碼由 V8 引擎來運行,運行中註冊的事件監聽會被保留下來,在對應的事件發生時收到通知
網絡、文件 I/O 等事件產生時,已註冊的回調函數將排到事件隊列中,接著被事件循環取出放到調用棧上,回調函數執行完(調用棧清空)之後,事件循環再取一個放上去……
執行過程中遇到 I/O 操作就交給 libuv 線程池中的某個 woker 來處理,結束之後 libuv 產生一個事件放入事件隊列。事件循環處理到返回事件時,對應的回調函數才在主線程開始執行,主線程在此期間繼續其它工作,而不阻塞等待
Node.js 就像一家咖啡館,店裡只有一個跑堂的(主線程),一大堆顧客湧過來的時候,會排隊等候(進入事件隊列),到號的顧客訂單會被傳給經理(libuv),經理將訂單分配給咖啡師(worker 線程),咖啡師用不同的原料和工具(底層依賴的 C/C++模塊)來製作訂單要求的各種咖啡,一般會有 4 個咖啡師值班,高峰時候可能會增加一些。訂單傳給經理後,不等咖啡做出來,而是接著處理下一個訂單。一杯咖啡做完之後,放到出餐流水線(IO Events 隊列),送達前臺後,跑堂的喊名字,顧客過來取