簡要
Node已經如今發展很快,已經相對穩定和成熟,在某些時候有必要知道其內部運行原理以及運行處理過程。
種一棵樹最好的時間是十年前 其次是現在。希望能堅持下去。
Nodejs當前最新版本 8.9.4
Node.js主要分爲四大部分,Node Standard Library,Node Bindings,V8,Libuv
大體流程是這樣的:
-
初始化 V8 、LibUV , OpenSSL
-
創建 Environment 環境
-
設置 Process 進程對象
-
執行 node.js 文件
解壓包後代碼結構如下:
├── AUTHORS
├── BSDmakefile # bsd平臺makefile文件
├── BUILDING.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── COLLABORATOR_GUIDE.md
├── CONTRIBUTING.md
├── CPP_STYLE_GUIDE.md
├── GOVERNANCE.md
├── LICENSE
├── Makefile # Linux平臺makefile文件
├── README.md
├── android-configure
├── benchmark
├── common.gypi
├── configure
├── deps # Node底層核心依賴; 最核心的兩塊V8 Engine和libuv事件驅動的異步I/O模型庫
├── doc
├── lib # Node後端核心庫
├── node.gyp # Node編譯任務配置文件
├── node.gypi
├── src # C++內建模塊
├── test # 測試代碼
├── tools # 編譯時用到的工具
└── vcbuild.bat # Windows跨平臺makefile文件
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
Hello World 底層運行過程
#app.js
const http = require('http');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World\n');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
一個簡單的Helloworld涉及到多個模塊:
- global
- module
- http
- event
- net
1.0 從main執行到js
入口 src/node_main.cc 106行 通過 src/node.cc 調用 node::Start(argc, argv);
node_main.cc
namespace node {
extern bool linux_at_secure;
} // namespace node
int main(int argc, char *argv[]) {
#if defined(__linux__)
char** envp = environ;
while (*envp++ != nullptr) {}
Elf_auxv_t* auxv = reinterpret_cast<Elf_auxv_t*>(envp);
for (; auxv->a_type != AT_NULL; auxv++) {
if (auxv->a_type == AT_SECURE) {
node::linux_at_secure = auxv->a_un.a_val;
break;
}
}
#endif
// Disable stdio buffering, it interacts poorly with printf()
// calls elsewhere in the program (e.g., any logging from V8.)
setvbuf(stdout, nullptr, _IONBF, 0);
setvbuf(stderr, nullptr, _IONBF, 0);
// main作爲入口調用node::Start
return node::Start(argc, argv);
}
#endif
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
1.1 node::Start 加載js
調用順序:
Start() -> LoadEnviroment() -> ExecuteString()
最終在LoadEnvrioment()
裏面加載node.js文件,調用ExecuteString()
解析執行node.js文件,返回值是一個f_value
並且在ExecuteString()
調用V8的 Script::Compile()
和 Script::Run()
兩個接口去解析執行js代碼。
node.cc
# Nodejs啓動入口,
inline int Start(Isolate* isolate, IsolateData* isolate_data,
int argc, const char* const* argv,
int exec_argc, const char* const* exec_argv) {
HandleScope handle_scope(isolate);
Local<Context> context = Context::New(isolate);
Context::Scope context_scope(context);
Environment env(isolate_data, context);
CHECK_EQ(0, uv_key_create(&thread_local_env));
uv_key_set(&thread_local_env, &env);
env.Start(argc, argv, exec_argc, exec_argv, v8_is_profiling);
const char* path = argc > 1 ? argv[1] : nullptr;
StartInspector(&env, path, debug_options);
if (debug_options.inspector_enabled() && !v8_platform.InspectorStarted(&env))
return 12; // Signal internal error.
env.set_abort_on_uncaught_exception(abort_on_uncaught_exception);
if (force_async_hooks_checks) {
env.async_hooks()->force_checks();
}
{
Environment::AsyncCallbackScope callback_scope(&env);
env.async_hooks()->push_async_ids(1, 0);
//加載nodejs文件後調用ExecuteString()
LoadEnvironment(&env);
env.async_hooks()->pop_async_id(1);
}
env.set_trace_sync_io(trace_sync_io);
//事件循環池
{
SealHandleScope seal(isolate);
bool more;
PERFORMANCE_MARK(&env, LOOP_START);
do {
uv_run(env.event_loop(), UV_RUN_DEFAULT);
v8_platform.DrainVMTasks();
more = uv_loop_alive(env.event_loop());
if (more)
continue;
EmitBeforeExit(&env);
// Emit `beforeExit` if the loop became alive either after emitting
// event, or after running some callbacks.
more = uv_loop_alive(env.event_loop());
} while (more == true);
PERFORMANCE_MARK(&env, LOOP_EXIT);
}
env.set_trace_sync_io(false);
const int exit_code = EmitExit(&env);
RunAtExit(&env);
uv_key_delete(&thread_local_env);
v8_platform.DrainVMTasks();
WaitForInspectorDisconnect(&env);
#if defined(LEAK_SANITIZER)
__lsan_do_leak_check();
#endif
return exit_code;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
核心運行流程
整體運行流程圖
-
核心數據結構
default_loop_struct
結構體爲struct uv_loop_s
當加載js文件時,如果代碼有io操作,調用lib模塊->底層C++模塊->LibUV(deps uv)->拿到系統返回的一個fd(文件描述符),和 js代碼傳進來的回調函數callback,封裝成一個io觀察者(一個uv__io_s類型的對象),保存到default_loop_struct
. -
進入事件池,
default_loop_struct
保存對應io觀察着,V8 Engine處理js代碼, main函數調用libuv進入uv_run()
, node進入事件循環 ,判斷是否有存活的觀察者- 如果也沒有io, Node進程退出
- 如果有io觀察者, 執行
uv_run()
進入epoll_wait()
線程掛起,io觀察者檢測是否有數據返回callback
, 沒有數據則會一直在epoll_wait()
等待執行server.listen(3000)
會掛起一直等待。
Module對象
根據CommonJS規範,每一個文件就是一個模塊,在每個模塊中,都會有一個module對象,這個對象就指向當前的模塊。
module對象具有以下屬性:
- id:當前模塊的bi
- exports:表示當前模塊暴露給外部的值
- parent: 是一個對象,表示調用當前模塊的模塊
- children:是一個對象,表示當前模塊調用的模塊
- filename:模塊的絕對路徑
- paths:從當前文件目錄開始查找
node_modules
目錄;然後依次進入父目錄,查找父目錄下的node_modules
目錄;依次迭代,直到根目錄下的node_modules
目錄 - loaded:一個布爾值,表示當前模塊是否已經被完全加載
示例:
module.exports = {
name: 'fzxa',
getAge: function(age){
console.log(age)
}
}
console.log(module)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
執行node module.js 返回如下
Module {
id: '.',
exports: { name: 'fzxa', getAge: [Function: getAge] },
parent: null,
filename: '/Users/fzxa/Documents/study/module.js',
loaded: false,
children: [],
paths:
[ '/Users/fzxa/Documents/study/node_modules',
'/Users/fzxa/Documents/node_modules',
'/Users/fzxa/node_modules',
'/Users/node_modules',
'/node_modules' ] }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
module對象具有一個exports屬性,該屬性就是用來對外暴露變量、方法或整個模塊的。當其他的文件require
進來該模塊的時候,實際上就是讀取了該模塊module對象的exports屬性
exports對象
exports
和module.exports
都是引用類型的變量,而且這兩個對象指向同一塊內存地址
exports = module.exports = {};
- 1
例子:
var module = {
exports: {}
}
var exports = module.exports
function change(exports) {
//爲形參添加屬性,是會同步到外部的module.exports對象的
exports.name = "fzxa"
//在這裏修改了exports的引用,並不會影響到module.exports
exports = {
age: 24
}
console.log(exports) //{ age: 24 }
}
change(exports)
console.log(module.exports) //{exports: {name: "fzxa"}}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
直接給exports賦值,會改變當前模塊內部的形參exports對象的引用,也就是說當前的exports已經跟外部的module.exports對象沒有任何關係了,所以這個改變是不會影響到module.exports的
module.exports
就是爲了解決上述exports
直接賦值,會導致拋出不成功的問題而產生的。有了它,我們就可以這樣來拋出一個模塊了.
require方法
Node中引入模塊的機制步驟
- 路徑分析
- 文件定位
- 編譯執行
Node對引入過的模塊也會進行緩存。不同的地方是,node緩存的是編譯執行之後的對象而不是靜態文件
Module._load的源碼:
Module._load = function(request, parent, isMain) {
// 計算絕對路徑
var filename = Module._resolveFilename(request, parent);
// 第一步:如果有緩存,取出緩存
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
// 第二步:是否爲內置模塊
if (NativeModule.exists(filename)) {
return NativeModule.require(filename);
}
// 第三步:生成模塊實例,存入緩存
var module = new Module(filename, parent);
Module._cache[filename] = module;
// 第四步:加載模塊
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}
// 第五步:輸出模塊的exports屬性
return module.exports;
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
在Module._load
方法的內部調用了Module._findPath
這個方法,這個方法是用來返回模塊的絕對路徑的,源碼如下:
Module._findPath = function(request, paths) {
// 列出所有可能的後綴名:.js,.json, .node
var exts = Object.keys(Module._extensions);
// 如果是絕對路徑,就不再搜索
if (request.charAt(0) === '/') {
paths = [''];
}
// 是否有後綴的目錄斜槓
var trailingSlash = (request.slice(-1) === '/');
// 第一步:如果當前路徑已在緩存中,就直接返回緩存
var cacheKey = JSON.stringify({request: request, paths: paths});
if (Module._pathCache[cacheKey]) {
return Module._pathCache[cacheKey];
}
// 第二步:依次遍歷所有路徑
for (var i = 0, PL = paths.length; i < PL; i++) {
var basePath = path.resolve(paths[i], request);
var filename;
if (!trailingSlash) {
// 第三步:是否存在該模塊文件
filename = tryFile(basePath);
if (!filename && !trailingSlash) {
// 第四步:該模塊文件加上後綴名,是否存在
filename = tryExtensions(basePath, exts);
}
}
// 第五步:目錄中是否存在 package.json
if (!filename) {
filename = tryPackage(basePath, exts);
}
if (!filename) {
// 第六步:是否存在目錄名 + index + 後綴名
filename = tryExtensions(path.resolve(basePath, 'index'), exts);
}
// 第七步:將找到的文件路徑存入返回緩存,然後返回
if (filename) {
Module._pathCache[cacheKey] = filename;
return filename;
}
}
// 第八步:沒有找到文件,返回false
return false;
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
當我們第一次引入一個模塊的時候,require的緩存機制會將我們引入的模塊加入到內存中,以提升二次加載的性能。但是,如果我們修改了被引入模塊的代碼之後,當再次引入該模塊的時候,就會發現那並不是我們最新的代碼,這是一個麻煩的事情。如何解決呢
require有如下方法:
require(): 加載外部模塊
require.resolve():將模塊名解析到一個絕對路徑
require.main:指向主模塊
require.cache:指向所有緩存的模塊
require.extensions:根據文件的後綴名,調用不同的執行函數
//刪除指定模塊的緩存
delete require.cache[require.resolve('/*被緩存的模塊名稱*/')]
// 刪除所有模塊的緩存
Object.keys(require.cache).forEach(function(key) {
delete require.cache[key];
})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
HTTP_Server
首先需要創建一個 http.Server 類的實例,然後監聽它的 request 事件
requestListener 回調函數作爲觀察者,監聽了 request 事件, 默認超時時間爲2分
lib/_http_server.js
function Server(requestListener) {
if (!(this instanceof Server)) return new Server(requestListener);
net.Server.call(this, { allowHalfOpen: true });
if (requestListener) {
this.on('request', requestListener);
}
// Similar option to this. Too lazy to write my own docs.
// http://www.squid-cache.org/Doc/config/half_closed_clients/
// http://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F
this.httpAllowHalfOpen = false;
this.on('connection', connectionListener);
this.timeout = 2 * 60 * 1000;
this.keepAliveTimeout = 5000;
this._pendingResponseData = 0;
this.maxHeadersCount = null;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
觀察者 connectionListener
處理 connection
事件。
這時,則需要一個 HTTP parser 來解析通過 TCP 傳輸過來的數據:
lib/_http_server.js
function connectionListener(socket) {
debug('SERVER new http connection');
httpSocketSetup(socket);
// Ensure that the server property of the socket is correctly set.
// See https://github.com/nodejs/node/issues/13435
if (socket.server === null)
socket.server = this;
// If the user has added a listener to the server,
// request, or response, then it's their responsibility.
// otherwise, destroy on timeout by default
if (this.timeout)
socket.setTimeout(this.timeout);
socket.on('timeout', socketOnTimeout);
var parser = parsers.alloc();
parser.reinitialize(HTTPParser.REQUEST);
parser.socket = socket;
socket.parser = parser;
parser.incoming = null;
// Propagate headers limit from server instance to parser
if (typeof this.maxHeadersCount === 'number') {
parser.maxHeaderPairs = this.maxHeadersCount << 1;
} else {
// Set default value because parser may be reused from FreeList
parser.maxHeaderPairs = 2000;
}
var state = {
onData: null,
onEnd: null,
onClose: null,
onDrain: null,
outgoing: [],
incoming: [],
// `outgoingData` is an approximate amount of bytes queued through all
// inactive responses. If more data than the high watermark is queued - we
// need to pause TCP socket/HTTP parser, and wait until the data will be
// sent to the client.
outgoingData: 0,
keepAliveTimeoutSet: false
};
state.onData = socketOnData.bind(undefined, this, socket, parser, state);
state.onEnd = socketOnEnd.bind(undefined, this, socket, parser, state);
state.onClose = socketOnClose.bind(undefined, socket, state);
state.onDrain = socketOnDrain.bind(undefined, socket, state);
socket.on('data', state.onData);
socket.on('error', socketOnError);
socket.on('end', state.onEnd);
socket.on('close', state.onClose);
socket.on('drain', state.onDrain);
parser.onIncoming = parserOnIncoming.bind(undefined, this, socket, state);
// We are consuming socket, so it won't get any actual data
socket.on('resume', onSocketResume);
socket.on('pause', onSocketPause);
// Override on to unconsume on `data`, `readable` listeners
socket.on = socketOnWrap;
// We only consume the socket if it has never been consumed before.
var external = socket._handle._externalStream;
if (!socket._handle._consumed && external) {
parser._consumed = true;
socket._handle._consumed = true;
parser.consume(external);
}
parser[kOnExecute] =
onParserExecute.bind(undefined, this, socket, parser, state);
socket._paused = false;
}