Node.js之HelloWorld背后的大坑

入坑


先貼一段代碼,再熟悉不過,她默默的待在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.js Structure Overview
  • 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)為可以輕松越過的,誰知道剛起步就停了下來,真的沒有那么簡單,如下圖所示。

Node.js Startup

首先聲明這不是正規(guī)的時(shí)序圖,只是為了更好的理解代碼才畫成了時(shí)序圖的樣子,來描述整個(gè)代碼的調(diào)用過程。上圖描述了從command line到進(jìn)入example.js之前的程序調(diào)用過程,屬于整個(gè)HelloWorld程序的起步階段。要想了解進(jìn)入主程序之前Node.js都干了什么,細(xì)讀這部分代碼就可以了,尤其是node.cc、node.jsmodule.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.ccSetupProcessObject中有如下設(shè)置,將process.bindingBinding進(jìn)行綁定,Binding干了什么?
NODE_SET_METHOD(process, "binding", Binding);
  • node.ccBinding中有如下調(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_modulenm_context_register_funcnode::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);
  • Modulerequire有如下定義。
Module.prototype.require = function(path) {
  assert(path, 'missing path');
  assert(util.isString(path), 'path must be a string');
  return Module._load(path, this);
};
  • Modulewrap有如下定義。
Module.wrap = NativeModule.wrap;
  • node.jsNavtiveModule有如下定義。
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 Inheritance
  • 然后就是HTTP Server的工作流程。
HTTP Server Workflow

通過上圖可以看出,絕大部分邏輯都在net.js中,細(xì)讀這部分代碼可以更好的了解其工作原理。其中tcp_warp.cc中有很多關(guān)于Libuv的調(diào)用,暫時(shí)不在本文的討論范圍,有興趣的可以了解一下。

步入正軌三


最后一句了,挺??!

console.log('Server running at http://127.0.0.1:1337/');

consolerequire還不一樣,不是以參數(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.jsdebuglog方法。這些日志都比較長,最好輸出到文件中以便反復(fù)查看。

感想


別總以為什么都知道,其實(shí)可能連最基本的都不知道!
知道的越多,就越覺得無知!
唯有學(xué)習(xí)!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容