前言
在我接觸Nodejs的時候,聽的最多的關鍵字就是:事件驅動、非阻塞I/O、高效、輕量,是單線程且支持高並發的腳本語言。可為什么單線程的nodejs可以支持高並發呢?很多人都不明白其原理,自己也在很長一段時間內被這些概念搞的是雲里霧里。下面我們就來一步一步揭開其神秘的面紗。並且,通過底層C/C++源碼的學習,來剖析Nodejs實現高並發的之一------事件循環的實現。
從Node.js進入我們的視野時,我們所知道的它就由這些關鍵字組成 事件驅動、非阻塞I/O、高效、輕量,它在官網中也是這么描述自己的。
於是在我們剛接觸Nodejs時,會有所疑問:
- 為什么在瀏覽器中運行的Javascript 能與操作系統進行如此底層的交互?
- nodejs 真的是單線程嗎?
- 如果是單線程,他是如何處理高並發請求的?
- nodejs 事件驅動是如何實現的?
架構一覽
-
Node.js 標准庫,這部分是由 Javascript 編寫的,即我們使用過程中直接能調用的 API。在源碼中的 lib 目錄下可以看到。
-
Node bindings,這一層是 Javascript 與底層 C/C++ 能夠溝通的關鍵,前者通過 bindings 調用后者,相互交換數據。實現在 node.cc
這一層是支撐 Node.js 運行的關鍵,由 C/C++ 實現。 -
Google 推出的 Javascript VM,也是 Node.js 為什么使用的是 Javascript 的關鍵,它為Javascript 提供了在非瀏覽器端運行的環境,它的高效是 Node.js 之所以高效的原因之一。
-
Libuv:它為 Node.js 提供了跨平台,線程池,事件池,異步 I/O 等能力,是 Node.js 如此強大的關鍵。
-
C-ares:提供了異步處理 DNS 相關的能力。
-
http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、數據壓縮等其他的能力。
與操作系統交互
舉個簡單的例子,我們想要打開一個文件,並進行一些操作,可以寫下面這樣一段代碼:
var fs = require('fs');
fs.open('./test.txt', "w", function(err, fd) {
//..do something
});
這段代碼的調用過程大致可描述為:lib/fs.js → src/node_file.cc → uv_fs
lib/fs.js
async function open(path, flags, mode) {
mode = modeNum(mode, 0o666);
path = getPathFromURL(path);
validatePath(path);
validateUint32(mode, 'mode');
return new FileHandle(
await binding.openFileHandle(pathModule.toNamespacedPath(path),
stringToFlags(flags),
mode, kUsePromises));
}
src/node_file.cc
static void Open(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
const int argc = args.Length();
if (req_wrap_async != nullptr) { // open(path, flags, mode, req)
AsyncCall(env, req_wrap_async, args, "open", UTF8, AfterInteger,
uv_fs_open, *path, flags, mode);
} else { // open(path, flags, mode, undefined, ctx)
CHECK_EQ(argc, 5);
FSReqWrapSync req_wrap_sync;
FS_SYNC_TRACE_BEGIN(open);
int result = SyncCall(env, args[4], &req_wrap_sync, "open",
uv_fs_open, *path, flags, mode);
FS_SYNC_TRACE_END(open);
args.GetReturnValue().Set(result);
}
}
uv_fs
/* Open the destination file. */
dstfd = uv_fs_open(NULL,
&fs_req,
req->new_path,
dst_flags,
statsbuf.st_mode,
NULL);
uv_fs_req_cleanup(&fs_req);
具體來說,當我們調用 fs.open 時,Node.js 通過 process.binding 調用 C/C++ 層面的 Open 函數,然后通過它調用 Libuv 中的具體方法 uv_fs_open,最后執行的結果通過回調的方式傳回,完成流程。我們在 Javascript 中調用的方法,最終都會通過 process.binding 傳遞到 C/C++ 層面,最終由他們來執行真正的操作。Node.js 即這樣與操作系統進行互動。
為什么一個單線程的效率可以這么高
同時處理數萬級的並發而不會造成阻塞呢?就是我們下面所說的--------事件驅動。
-
每個Node.js進程只有一個主線程在執行程序代碼,形成一個執行棧(execution context stack)。
-
主線程之外,還維護了一個"事件隊列"(Event queue)。當用戶的網絡請求或者其它的異步操作到來時,node都會把它放到Event Queue之中,此時並不會立即執行它,代碼也不會被阻塞,繼續往下走,直到主線程代碼執行完畢。
-
主線程代碼執行完畢完成后,然后通過Event Loop,也就是事件循環機制,開始到Event Queue的開頭取出第一個事件,從線程池中分配一個線程去執行這個事件,接下來繼續取出第二個事件,再從線程池中分配一個線程去執行,然后第三個,第四個。主線程不斷的檢查事件隊列中是否有未執行的事件,直到事件隊列中所有事件都執行完了,此后每當有新的事件加入到事件隊列中,都會通知主線程按順序取出交EventLoop處理。當有事件執行完畢后,會通知主線程,主線程執行回調,線程歸還給線程池。
-
主線程不斷重復上面的第三步。
我們所看到的node.js單線程只是一個js主線程,本質上的異步操作還是由線程池完成的,node將所有的阻塞操作都交給了內部的線程池去實現,本身只負責不斷的往返調度,並沒有進行真正的I/O操作,從而實現異步非阻塞I/O,這便是node單線程和事件驅動的精髓之處了