Node.js之HelloWorld背后的大坑

入坑


先贴一段代码,再熟悉不过,她默默的待在Node.js官方首页上已经不知多长时间,迎接着初入Node.js世界的程序员们,所有人都认识她,但并非所有人都了解她,甚至很多人都没有想过要去了解她。

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/

我也是很多认识但不了解她的人们的其中之一,为什么我会想要去了解她呢,其实事情是这样的。之前用PM2设置集群的时候很容易,一条命令就搞定了,当时感觉很神奇,于是便看了下官网上关于集群的文档,但是Sample代码实在非常怪异,完全不是正常的思路,便越发想了解一下Node.js的集群机制到底是如何实现的,看源码吧,遂入坑,卒!先看cluster.js,然后看child_process.js,最后看net.js,为什么最后是net.js,因为实在看不下去了,各种异步,东一榔头西一棒子,完全理不清头绪。总结一下失败原因,首先并不了解Node.js代码结构、实现方式和内部机制,不适应异步逻辑,一直以顺序的思路去看代码,导致很多地方看不明白。其次对JavaScript其实并不熟悉,尤其这种大型项目,一些高级特性的使用,所谓的对象和继承等等,相比自己之前写的那些业务代码,完全不属于一个次元。然后就是轻敌,单枪匹马杀入乱军之中,没有赵子龙那样的本事,不卒才怪。于是痛定思痛,还是从头开始学吧!

体系架构


Node.js主要分为四大部分,Node Standard Library,Node Bindings,V8,Libuv,架构图如下。


Node.js Structure Overview
  • Node Standard Library 是我们每天都在用的标准库,如require('http'),官方的API文档说的就是他。
  • Node Bindings 是沟通上下层的桥梁,封装V8和Libuv的细节,向上层提供基础功能。
  • V8 是Google开发的JavaScript引擎,提供JavaScript运行环境,可以说没有他就没有Node.js。
  • Libuv 是专门为Node.js开发的一个封装库,提供跨平台的异步I/O能力。

代码结构


以下是代码的简易结构,已经囊括了Node.js的四大部分,对于入门来说已经足够了,并且本文分析的绝大部分代码都在lib和src下面。另外,本文是基于v0.12.7版本进行的代码分析,网上也有一些老版本的分析,好像完全说的不是一回事,由于一时没注意带来了很多困扰,特此说明。

node   
├─deps
│   ├─uv
│   └─v8
├─lib (Node Standard Library)
└─src (Node Bindings)

特别声明


后文只着重描述了看代码的思路,并没有进行过多的说明,一是表达能力有限,二是感觉任何的说明都显得苍白无力,所以光看文章不看代码是不行的!

下面以Linus Torvalds的一句名言来开启Node.js的源码之旅。

Talk is cheap, show me the code.

Let's go!

起步停车


本来我刚开始分析的是第二句代码http.createServer(...).listen(...);,因为这句最长,一看就是重点嘛,但是分析完之后才发现,在这之前Node.js还做了好多好多事情,这才只是冰山一角,还是需要从真正的起跑线开始。

% node example.js

这句话有啥可分析的,一开始确实是这样想的,本来认为可以轻松越过的,谁知道刚起步就停了下来,真的没有那么简单,如下图所示。

Node.js Startup

首先声明这不是正规的时序图,只是为了更好的理解代码才画成了时序图的样子,来描述整个代码的调用过程。上图描述了从command line到进入example.js之前的程序调用过程,属于整个HelloWorld程序的起步阶段。要想了解进入主程序之前Node.js都干了什么,细读这部分代码就可以了,尤其是node.ccnode.jsmodule.js。其中node_Contextify.cc中有很多关于V8的调用,暂时不在本文的讨论范围,有兴趣的可以了解一下。

Node Bindings


其实上一节还留有一个疑点,上图右边第三个Script是怎么来的,是如何与C++代码联系上的?接着看代码!

  • vm.js中有如下调用,process.binding干了什么?
var binding = process.binding('contextify');
var Script = binding.ContextifyScript;
  • node.ccSetupProcessObject中有如下设置,将process.bindingBinding进行绑定,Binding干了什么?
NODE_SET_METHOD(process, "binding", Binding);
  • node.ccBinding中有如下调用,对模块进行注册,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中有如下宏调用,终于看清楚了!结合前面几点,实际上就是把node_modulenm_context_register_funcnode::InitContextify进行了绑定。
NODE_MODULE_CONTEXT_AWARE_BUILTIN(contextify, node::InitContextify);

兜了这么大一个圈子,省略去中间步骤,代码对应如下,Node.js就是如此完成了Node Bindings。

process.binding('contextify'); 
↓↓↓
NODE_MODULE_CONTEXT_AWARE_BUILTIN(contextify, node::InitContextify);

步入正轨一


说了这么多终于到第一句代码了,再不到就要放弃了,赶快来看看吧。

var http = require('http');

require是怎么来的,为什么平白无故就能用呢,实际上都干了些什么?

  • 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});'
];

不用多解释了,代码已经说明了一切。

步入正轨二


正餐开始,不过感觉前面的开胃菜似乎有点多……

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的继承关系,有利于更好的理解代码。
HTTP Server Inheritance
  • 然后就是HTTP Server的工作流程。
HTTP Server Workflow

通过上图可以看出,绝大部分逻辑都在net.js中,细读这部分代码可以更好的了解其工作原理。其中tcp_warp.cc中有很多关于Libuv的调用,暂时不在本文的讨论范围,有兴趣的可以了解一下。

步入正轨三


最后一句了,挺住!

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

consolerequire还不一样,不是以参数的形式传进来的,这就要说到global对象了,Node.js的顶层对象。官方文档已经有了相关的说明,在这就不多做解释,重点看看他是怎么来的。

  • node.js中有如下定义,这个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是一个意思,也就是说f中的this指向的就是global
Local<Object> global = env->context()->Global();
Local<Value> arg = env->process_object();
f->Call(global, 1, &arg);

这样console作为全局变量的身份也就真相大白了。

大功告成?


所有代码都分析完了,“Hello World”这两个字竟然还没有出现!?
这是段服务端程序,没有请求,哪来的应答!?
哎,你要是长这样该多好……

console.log('Hello World');

即使没完,也准备告一段落了。给有缘人一个探索的空间?No!No!No!累了,需要恢复元气!
如果确实非常想知道后事如何,那便在此留下一些线索,以供参考。

其实在看代码的过程中,Server响应请求的过程更加令人匪夷所思,卡了好久,还好找到了比较好的办法才算弄清楚,那就是看!日!志!其实看日志根本不算什么办法,地球人都知道,但是怎么让日志打出来,还真费了半天功夫,反正百度上是没找到。

  • V8日志
% node --trace example.js
  • 源码Debug日志
% NODE_DEBUG=HTTP,STREAM,MODULE,NET node example.js

关于V8的一些参数可以通过node --v8-options查看,--trace的作用是输出方法调用过程。源码Debug日志的原理可以查看util.jsdebuglog方法。这些日志都比较长,最好输出到文件中以便反复查看。

感想


别总以为什么都知道,其实可能连最基本的都不知道!
知道的越多,就越觉得无知!
唯有学习!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,012评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,628评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,653评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,485评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,574评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,590评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,596评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,340评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,794评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,102评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,276评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,940评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,583评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,201评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,441评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,173评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,136评论 2 352

推荐阅读更多精彩内容