入坑
先貼一段代碼,再熟悉不過,她默默的待在Node.js官方首頁上已經(jīng)不知多長時(shí)間,迎接著初入Node.js世界的程序員們,所有人都認(rèn)識她,但并非所有人都了解她,甚至很多人都沒有想過要去了解她。
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
}).listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');
% node example.js
Server running at http://127.0.0.1:1337/
我也是很多認(rèn)識但不了解她的人們的其中之一,為什么我會(huì)想要去了解她呢,其實(shí)事情是這樣的。之前用PM2設(shè)置集群的時(shí)候很容易,一條命令就搞定了,當(dāng)時(shí)感覺很神奇,于是便看了下官網(wǎng)上關(guān)于集群的文檔,但是Sample代碼實(shí)在非常怪異,完全不是正常的思路,便越發(fā)想了解一下Node.js的集群機(jī)制到底是如何實(shí)現(xiàn)的,看源碼吧,遂入坑,卒!先看cluster.js,然后看child_process.js,最后看net.js,為什么最后是net.js,因?yàn)閷?shí)在看不下去了,各種異步,東一榔頭西一棒子,完全理不清頭緒??偨Y(jié)一下失敗原因,首先并不了解Node.js代碼結(jié)構(gòu)、實(shí)現(xiàn)方式和內(nèi)部機(jī)制,不適應(yīng)異步邏輯,一直以順序的思路去看代碼,導(dǎo)致很多地方看不明白。其次對JavaScript其實(shí)并不熟悉,尤其這種大型項(xiàng)目,一些高級特性的使用,所謂的對象和繼承等等,相比自己之前寫的那些業(yè)務(wù)代碼,完全不屬于一個(gè)次元。然后就是輕敵,單槍匹馬殺入亂軍之中,沒有趙子龍那樣的本事,不卒才怪。于是痛定思痛,還是從頭開始學(xué)吧!
體系架構(gòu)
Node.js主要分為四大部分,Node Standard Library,Node Bindings,V8,Libuv,架構(gòu)圖如下。

- Node Standard Library 是我們每天都在用的標(biāo)準(zhǔn)庫,如
require('http'),官方的API文檔說的就是他。 - Node Bindings 是溝通上下層的橋梁,封裝V8和Libuv的細(xì)節(jié),向上層提供基礎(chǔ)功能。
- V8 是Google開發(fā)的JavaScript引擎,提供JavaScript運(yùn)行環(huán)境,可以說沒有他就沒有Node.js。
- Libuv 是專門為Node.js開發(fā)的一個(gè)封裝庫,提供跨平臺的異步I/O能力。
代碼結(jié)構(gòu)
以下是代碼的簡易結(jié)構(gòu),已經(jīng)囊括了Node.js的四大部分,對于入門來說已經(jīng)足夠了,并且本文分析的絕大部分代碼都在lib和src下面。另外,本文是基于v0.12.7版本進(jìn)行的代碼分析,網(wǎng)上也有一些老版本的分析,好像完全說的不是一回事,由于一時(shí)沒注意帶來了很多困擾,特此說明。
node
├─deps
│ ├─uv
│ └─v8
├─lib (Node Standard Library)
└─src (Node Bindings)
特別聲明
后文只著重描述了看代碼的思路,并沒有進(jìn)行過多的說明,一是表達(dá)能力有限,二是感覺任何的說明都顯得蒼白無力,所以光看文章不看代碼是不行的!
下面以Linus Torvalds的一句名言來開啟Node.js的源碼之旅。
Talk is cheap, show me the code.
Let's go!
起步停車
本來我剛開始分析的是第二句代碼http.createServer(...).listen(...);,因?yàn)檫@句最長,一看就是重點(diǎn)嘛,但是分析完之后才發(fā)現(xiàn),在這之前Node.js還做了好多好多事情,這才只是冰山一角,還是需要從真正的起跑線開始。
% node example.js
這句話有啥可分析的,一開始確實(shí)是這樣想的,本來認(rèn)為可以輕松越過的,誰知道剛起步就停了下來,真的沒有那么簡單,如下圖所示。

首先聲明這不是正規(guī)的時(shí)序圖,只是為了更好的理解代碼才畫成了時(shí)序圖的樣子,來描述整個(gè)代碼的調(diào)用過程。上圖描述了從
command line到進(jìn)入example.js之前的程序調(diào)用過程,屬于整個(gè)HelloWorld程序的起步階段。要想了解進(jìn)入主程序之前Node.js都干了什么,細(xì)讀這部分代碼就可以了,尤其是node.cc、node.js和module.js。其中node_Contextify.cc中有很多關(guān)于V8的調(diào)用,暫時(shí)不在本文的討論范圍,有興趣的可以了解一下。
Node Bindings
其實(shí)上一節(jié)還留有一個(gè)疑點(diǎn),上圖右邊第三個(gè)Script是怎么來的,是如何與C++代碼聯(lián)系上的?接著看代碼!
-
vm.js中有如下調(diào)用,process.binding干了什么?
var binding = process.binding('contextify');
var Script = binding.ContextifyScript;
-
node.cc的SetupProcessObject中有如下設(shè)置,將process.binding與Binding進(jìn)行綁定,Binding干了什么?
NODE_SET_METHOD(process, "binding", Binding);
-
node.cc的Binding中有如下調(diào)用,對模塊進(jìn)行注冊,nm_context_register_func干了什么?
mod->nm_context_register_func(exports, unused, env->context(), mod->nm_priv);
-
node.h中對mod的類型node_module有如下定義,往下看!
struct node_module {
int nm_version;
unsigned int nm_flags;
void* nm_dso_handle;
const char* nm_filename;
node::addon_register_func nm_register_func;
node::addon_context_register_func nm_context_register_func;
const char* nm_modname;
void* nm_priv;
struct node_module* nm_link;
};
-
node.h中還有如下宏定義,接著往下看!
#define NODE_MODULE_CONTEXT_AWARE_X(modname, regfunc, priv, flags) \
extern "C" { \
static node::node_module _module = \
{ \
NODE_MODULE_VERSION, \
flags, \
NULL, \
__FILE__, \
NULL, \
(node::addon_context_register_func) (regfunc), \
NODE_STRINGIFY(modname), \
priv, \
NULL \
}; \
NODE_C_CTOR(_register_ ## modname) { \
node_module_register(&_module); \
} \
}
#define NODE_MODULE_CONTEXT_AWARE_BUILTIN(modname, regfunc) \
NODE_MODULE_CONTEXT_AWARE_X(modname, regfunc, NULL, NM_F_BUILTIN) \
-
node_contextify.cc中有如下宏調(diào)用,終于看清楚了!結(jié)合前面幾點(diǎn),實(shí)際上就是把node_module的nm_context_register_func與node::InitContextify進(jìn)行了綁定。
NODE_MODULE_CONTEXT_AWARE_BUILTIN(contextify, node::InitContextify);
兜了這么大一個(gè)圈子,省略去中間步驟,代碼對應(yīng)如下,Node.js就是如此完成了Node Bindings。
process.binding('contextify');
↓↓↓
NODE_MODULE_CONTEXT_AWARE_BUILTIN(contextify, node::InitContextify);
步入正軌一
說了這么多終于到第一句代碼了,再不到就要放棄了,趕快來看看吧。
var http = require('http');
require是怎么來的,為什么平白無故就能用呢,實(shí)際上都干了些什么?
-
module.js的_compile中有如下代碼。
var self = this;
...
function require(path) {
return self.require(path);
}
...
var wrapper = Module.wrap(content);
...
var compiledWrapper = runInThisContext(wrapper, { filename: filename });
...
var args = [self.exports, require, self, filename, dirname];
return compiledWrapper.apply(self.exports, args);
-
Module的require有如下定義。
Module.prototype.require = function(path) {
assert(path, 'missing path');
assert(util.isString(path), 'path must be a string');
return Module._load(path, this);
};
-
Module的wrap有如下定義。
Module.wrap = NativeModule.wrap;
-
node.js中NavtiveModule有如下定義。
NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
不用多解釋了,代碼已經(jīng)說明了一切。
步入正軌二
正餐開始,不過感覺前面的開胃菜似乎有點(diǎn)多……
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
}).listen(1337, '127.0.0.1');
一如既往,看圖說話。
- 首先了解一下HTTP Server的繼承關(guān)系,有利于更好的理解代碼。

- 然后就是HTTP Server的工作流程。

通過上圖可以看出,絕大部分邏輯都在net.js中,細(xì)讀這部分代碼可以更好的了解其工作原理。其中tcp_warp.cc中有很多關(guān)于Libuv的調(diào)用,暫時(shí)不在本文的討論范圍,有興趣的可以了解一下。
步入正軌三
最后一句了,挺??!
console.log('Server running at http://127.0.0.1:1337/');
console和require還不一樣,不是以參數(shù)的形式傳進(jìn)來的,這就要說到global對象了,Node.js的頂層對象。官方文檔已經(jīng)有了相關(guān)的說明,在這就不多做解釋,重點(diǎn)看看他是怎么來的。
-
node.js中有如下定義,這個(gè)this到底是誰?
this.global = this;
...
startup.globalVariables = function() {
global.process = process;
global.global = global;
global.GLOBAL = global;
global.root = global;
global.Buffer = NativeModule.require('buffer').Buffer;
process.domain = null;
process._exiting = false;
};
...
startup.globalConsole = function() {
global.__defineGetter__('console', function() {
return NativeModule.require('console');
});
};
-
node.cc中的LoadEnvironment有如下定義,f代表node.js所形成的方法,Call跟JavaScript中的Function.prototype.call是一個(gè)意思,也就是說f中的this指向的就是global。
Local<Object> global = env->context()->Global();
Local<Value> arg = env->process_object();
f->Call(global, 1, &arg);
這樣console作為全局變量的身份也就真相大白了。
大功告成?
所有代碼都分析完了,“Hello World”這兩個(gè)字竟然還沒有出現(xiàn)???
這是段服務(wù)端程序,沒有請求,哪來的應(yīng)答?。?br>
哎,你要是長這樣該多好……
console.log('Hello World');
即使沒完,也準(zhǔn)備告一段落了。給有緣人一個(gè)探索的空間?No!No!No!累了,需要恢復(fù)元?dú)猓?br> 如果確實(shí)非常想知道后事如何,那便在此留下一些線索,以供參考。
其實(shí)在看代碼的過程中,Server響應(yīng)請求的過程更加令人匪夷所思,卡了好久,還好找到了比較好的辦法才算弄清楚,那就是看!日!志!其實(shí)看日志根本不算什么辦法,地球人都知道,但是怎么讓日志打出來,還真費(fèi)了半天功夫,反正百度上是沒找到。
- V8日志
% node --trace example.js
- 源碼Debug日志
% NODE_DEBUG=HTTP,STREAM,MODULE,NET node example.js
關(guān)于V8的一些參數(shù)可以通過node --v8-options查看,--trace的作用是輸出方法調(diào)用過程。源碼Debug日志的原理可以查看util.js的debuglog方法。這些日志都比較長,最好輸出到文件中以便反復(fù)查看。
感想
別總以為什么都知道,其實(shí)可能連最基本的都不知道!
知道的越多,就越覺得無知!
唯有學(xué)習(xí)!