Discuz! Board

标题: 入门文章学习 [打印本页]

作者: zangcf    时间: 2016-7-28 18:21
标题: 入门文章学习
本帖最后由 zangcf 于 2016-7-28 18:24 编辑

http://cnodejs.org/topic/5594ada26ba28efa30a604e2
Node.js挖掘之一:从两个角度,一个小例子浅析Node.js架构
==========================================
最近周播剧《盗墓笔记》挺火。而我最近在干两件事,一是追《盗墓笔记》,二是研究Node.js。Node.js研究了也有一段时间了,把我的研究结果发到博客上来,写成"Node.js挖掘"系列,虽然挖掘node没有盗墓那么惊心动魄,但收获确是一样的丰厚。
从学习方法来看,无论我们学习一个新平台,或者接触一个新的产品,一开始不需要着急看细节,而是要从架构,从全局的角度来俯视这个新事物,如果一开始就陷入细节中,就会让我们迷失方向。这就好像我们去一个陌生的城市,了解它的第一件事是先看地图全貌,然后再慢慢找街道,这样我们才不至于迷路。当然细节非常重要,但细节的掌握是建立在对全局的把握之上,否则还是井底之蛙。
本文从两个角度查看Node.js的架构。一个是从模块直接的依赖关系角度,另一个是以一个建立一个TCP服务为例,从函数调用流程角度。Node.js挖掘系列后面的几篇会详细地介绍各个方面。

[attach]227[/attach]图1:Node.js模块依赖关系







作者: zangcf    时间: 2016-7-28 18:24
1.1        Host environment

如果我们自己去下载Node.js的源代码,并且编译的话,会发现node.js会最终生成一个可执行文件node-v0.12.4/out/Release/node。
既然是可执行文件,其运行时必然依赖执行环境。图1最下方标示的是Host environment,也即宿主环境, node 运行需要OS提供各种服务,如文件访问,socket编程,OpenSSL,多进程,多线程,锁,异步I/O库等等。
作者: zangcf    时间: 2016-7-28 18:28
1.2        Node.js

图1中间的部分展示了Node.js的主要组成部分:V8 engine, libuv, builtin modules, natives modules以及其它辅助服务代码。
Node.js以源代码形式将V8 engine包含在自己的代码树里面。V8 engine在Node.js里面起到两个重要的作用:

作为虚拟机,执行JS代码。包括我们自己写的JS代码、第三方JS代码以及natives module
提供C++函数接口,为Node.js提供包括V8初始化、创建context, scope、为builtin module模拟生成JS prototype-based class、各种模版等服务 对于第2点,比如在 src\Tcp_wrap.cc中的TCPWrap::Initialize()函数内部,通过函数模版创建类似JS prototype-base class、类属性以及prototype method。
Node.js里面有一个大神,libuv,它是基于事件驱动的异步I/O模型库。在第2节可以看到,我们的JS代码发起的异步请求,最终由libuv完成,而我们所设置的回调函数则由libuv最初触发。也就是说,异步请求由我们的JS代码发起,由libuv完成,而回调函数的执行则由libuv最初触发。

Node.js提供了一些辅助函数,如String_byte.cc里面的base64编解码函数,它们存在的意义是为builtin modules提供服务。

V8 engine提供的函数接口,libuv提供的异步I/O模型库以及Node.js提供的其他辅助函数一起为builtin modules提供了生存的土壤。Builtin modules是由C++代码写成的各类模块,涵盖了crypto,zlib, file stream在内的各种基础功能。

Node.js还包含了Natives modules。它是由JS写成的,供我们的应用程序调用的库。同时,这些模块又依赖builtin module来获得相应的支持服务。例如我们在建立一个http server时所用到的http_server.js,其背后的支撑者是tcp_wrap.cc和libuv。

让我们再俯瞰一下node.js部分。如果把node.js看出一个黑盒子的话,其暴露给我们开发者的接口则是Native modules。
当我们的代码发起异步请求时, 请求自上而下,穿越Natvie module,通过builtin module将请求传递给V8, libuv和node.js辅助服务。
而当请求结束,则从下回溯而上,最终调用我们的请求回调函数。

需要强调的是,这里面的自上而下和自下而上的两个过程中间会相隔很远的时间。而这个时间差会被用来执行更多的其它代码。
作者: zangcf    时间: 2016-7-28 18:29
1.3        我们的JS code

我们的JS code,依赖第三方模块以及Native modules完成自身业务需求。
假如我们的服务程序入口是 fooserv.js,那么通过 node fooserv.js启动我们的服务时,node会先做一些包括V8初始化,libuv启动在内的准备工作,然后交由V8 engine来执行Native module以及我们的JS代码。
这一部分的详细过程可以参考“Node.js挖掘之五:浅析模块加载流程,我们的应用如何启动以及如何加载依赖模块”。

作者: zangcf    时间: 2016-7-28 18:29
本帖最后由 zangcf 于 2016-7-28 18:33 编辑

2.        从函数调用流程看Node.js架构
让我们以建立一个http server时,重要的一步server.listen()为例,从函数调用流程来看看Node.js架构。图中从左到右依次为V8 engine, Node.js wrapper以及 libuv。这3者组成了强大灵活的Node.js。图2展示的是从JS代码呼叫server.listen()函数开始,开启的一段漫长旅程。旅程分为正、反两个方向。

[attach]228[/attach]

图2:Node.js函数调用流程示例

V8 执行 JaveScript代码 server.listen(config.crypto.service.port, function () {}) 时会通过一些基础服务调到 TCPWrap:isten()。TCPWrap是Node.js的内建模块。代码可以参考 src/Tcp_wrap.cc。TCPWrap:isten()是Node.js提供的wrapper函数,其通过调用libuv的API uv_listen()  的方式,由libuv来完成异步调用。图中步骤1,2,3,4,5标明了调用和返回路径。这几步会很快结束。留下callback TCPWrap::OnConnection()等着所需要的数据准备好后被调用。
libuv在得到所需要的请求后,会调用callback TCPWrap::OnConnection(),在该函数最后通过 tcp_wrap->MakeCallback(env->onconnection_string(), ARRAY_SIZE(argv), argv) 调用V8 engine中的JavaScript callback
Node.js内建模块http其实是建立在模块net之上的。如果看net.js代码会发现,其通过  new TCP()  返回的类对象完成后续的TCP connect, bind, open等socket动作。
可以看到Node.js做的工作像是一座桥。左手V8,右手libuv,将2者有机连接在一起。例如HandleWrap::HandleWrap()中记录了V8 instance中的JavaScript对象以及TCPWrap对象。这样在TCPWrap::OnConnection()中可以拿到这两个对象,执行后续的callback调用。

作者: zangcf    时间: 2016-7-28 18:46
Node.js挖掘之二:以http server为例浅析Node.js从前端到libuv调用过程
Node.js的由V8 engine,内建的模块以及libuv组成。结合Node.js源代码以及图1,可以看出几点
图2是我之前发表的一篇文章( 一个小例子浅析Node.JS架构)中所画的图。该图从V8 engine角度出发,描述了建立HTTP server时,从JS到C++的完整穿越过程。图2的过程既包括了V8所执行的JS和它所处的context,也包括了异步调用后端的参与者(Node.js TCPWrap以及libuv)。

对于图2,需要强调的是,图中的1,2,3,4,5这5个步骤是一个同步调用的过程。该过程始于server.listen(),终于第5步,也就是server.listen()的下一行JS代码。这5步执行完后,server.listen()所发起的请求其实并没有被处理完。V8就急忙动身执行后面的JS代码了。当server.listen()发起的请求最终处理完后,从libuv发起了一个逆向的callback调用过程。该逆向过程始于libuv的event loop,终于V8 engine调用server.listen()所设置的callback函数。而在这段时间内,V8已经不知道执行完多少JS代码,完成了多少其它任务了,这就是异步调用的魅力所在,这也是Node.js高并发的原因。
需要澄清的是,高并发不等于高实时性。以网站响应为例,高并发意味着可以同时接受很高数量的页面请求,但从接受每个请求到把最终数据返回给请求者的时间确是响应速度的问题。这就好像我们很多人同时去饭馆吃饭,如果饭馆接待员反应灵敏,调度得当,她会让我们每个人都能得到一个座位,并且开始点菜,但这不表示我们能在很快时间内吃上可口的菜。
本文将图2所示的过程进一步细化,从V8执行JS代码创建一个HTTP server开始分析,帮助需要的人从更细更完整的角度看完整个过程。
[attach]230[/attach]
图2:从V8到libuv异步调用过程
用Node.js建立一个HTTP server可谓方便至极也简单至极。寥寥十几行代码就完成了一个server的雏形。然而代码看似简单,背后必然发生了很多故事。暴露给应用开发者的接口越少,说明平台或者框架代替开发者做了越多的事情。
[attach]231[/attach]
图3:创建http server的JS code
套用著名技术作家侯捷的一句话“只会用一样东西,不明白他的道理,实在不高明”。作为一个探索欲望挺强的人,我按捺不住好奇心深入框架研究了一番,努力做一个高明的人。
我的研究从代码http.createServer()开始。图4展示了从V8开始执行图3的JS代码到TCPWrap的序列图。
[attach]232[/attach]
序列图从上至下分为2部分。第一部分创建了一个Server()的实例。这部分还是集中在JS部分。其中http.js, _http_server.js以及net.js是Node.js JS库中的一部分。这部分比较重要的细节是 _http_server.js中的Server()的prototype部分从net.js Server()集成了若干API(),这部分API会为之后提供服务。
第二部分开始从调用this.listen()开始。可以看到,第二部分直接调用net.js提供的API。让我们把注意力直接放到序列图的net.js 最后一步。
createTCP()通过process.binding(‘tcp_wrap’).TCP进入到Node.js C++部分。也就是图2所示的TCPWrap部分。new TCPWrap()是最先被调用的代码。从JS 到 C++如何穿越的问题,请参考V8 engine相关文档。
我们再来看看图4 net.js的倒数第二步 handle.open(fd)。这部分代码最终调用到C++部分的TCPWrap::Open()。







作者: zangcf    时间: 2016-7-28 19:03
Node.js挖掘之三:一张图介绍Node.js的各路英雄
[attach]233[/attach]
图1:Node.js overview
本文所做的研究基于Node.js v0.12.4 Linux版本。
熟悉Node.js的同学都知道Node.js由v8 engine,内建的模块以及libuv组成。这是一个宽泛的介绍。本文通过一张图来详细了解以下Node.js运行中涉及到的各路英雄。
对于Node.js而言,可以将v8 engine和libuv看成是一个库。虽然事实上,在编译Node时,二者是以源代码方式直接编译进可执行文件node中去的。v8 engine和libuv源代码放在node-vXXX/deps目录下。本文先依次介绍v8 engien和libuv,最后结合图1讲述Node.js如何将这二者优雅地结合在一起的。

v8 engine
V8 engine的详细说明可以参考V8的在线文档(https://developers.google.com/v8/intro)。
        #include "include/v8.h"        #include "include/libplatform/libplatform.h"        using namespace v8;        int main(int argc, char* argv[]) {        // Initialize V8.        V8::InitializeICU();        Platform* platform = platform::CreateDefaultPlatform();        V8::InitializePlatform(platform);        V8::Initialize();        // Create a new Isolate and make it the current one.        Isolate* isolate = Isolate::New();        {                Isolate::Scope isolate_scope(isolate);                // Create a stack-allocated handle scope.                HandleScope handle_scope(isolate);                // Create a new context.                Local<Context> context = Context::New(isolate);                  // Enter the context for compiling and running the hello world script.                Context::Scope context_scope(context);                  // Create a string containing the JavaScript source code.                Local<String> source = String::NewFromUtf8(isolate, "'Hello' + ', World!'");                  // Compile the source code.                Local<Script> script = Script::Compile(source);                  // Run the script to get the result.                Local<Value> result = script->Run();                  // Convert the result to an UTF8 string and print it.                String::Utf8Value utf8(result);                printf("%s\n", *utf8);        }        // Dispose the isolate and tear down V8.        isolate->Dispose();        V8::Dispose();        V8::ShutdownPlatform();        delete platform;        return 0;}
上面是一个典型的v8使用示例。总体而言,v8里面包含几个重要的概念:isolate, context, handles。

[attach]234[/attach]


图2:v8 engine context


[attach]235[/attach]
图3: v8 engine handle stack


libuv
libuv是一个多平台的,主要用于处理事件驱动的异步I/O模型库。可以参考http://docs.libuv.org/en/v1.x/ 获取更详细的信息。
[attach]236[/attach]
图4: libuv模块分布图
取决于所运行的操作系统平台,libuv通过epoll, IOCP等方式来实现异步network I/O。而对于File I/O, DNS的操作则依赖于Thread Pool来实现。如图4所示。http://nikhilm.github.io/uvbook/ 是一个非常好的libuv入门以及API参考文档。libuv的核心部分是I/O (or event) loop。如图5所示,该loop负责调用callback function、运行idle function、执行事件polling轮询等操作。
[attach]237[/attach]
图5:libuv I/O(or event) loop
Node.js
介绍完v8 engine和libuv,让我们把目光再次转移到图1上来。除了牵手v8 engine和libuv外,Node.js本身也做了不少工作。提供了包括JS模块,C++模块(包括各种wrap)在内的核心模块供开发者使用。
如果按照Node代码的命名方法,这些JS模块叫“native module”,而C++模块叫“builtin module”。Node.js的代码包括libuv基本上是以C++写成的,将这些用C++写成的模块叫builtin也是合情合理。而native则喻为与Node之生俱来,也与第三方开发的以示区别。
内建的C++模块位于Node源代码的src目录,而JS模块则位于lib目录。通常C++模块是作为内部模块导出给lib/*.js模块使用,如C++所写的tcp_wrap模块是导出给net.js模块使用,而C++所写的crypto则会提供给crypto.js使用。
图6是一个关于native module和builtin module如何编译以及加载到内存的过程。对于Node自身提供的模块,其实无论是JS模块还是C++模块,最终都在编译生成可执行文件node时,嵌入到了ELF文件里面。而对这两者的提取方式却不一样。对于JS模块,使用process.binding(“natives”),而对于C++模块则直接用get_builtin_module()得到。
[attach]238[/attach]
图6:native module和buildin module编译和加载过程
当我们执行 node test.js 执行我们所写的JS代码时,node_main.cc首先被调用。这一步主要的工作是初始化v8 engine和libuv执行环境。uv_run(env->event_loop(), UV_RUN_ONCE) 用于启动libuv event loop。而我们的JS代码 test.js则会作为输入传递给CreateEnvironment(),最终交由v8 engine执行。
查看Node.cc代码,可以看到若干与v8 engine交互的工作,包括context, handle等的创建、通过v8的函数模版创建类的constructor以及类的prototype object等操作。其中通过函数模版创建类的constructor以及类的prototype object等操作非常有意思。
在JavaScript中,类是基于prototype继承的方式来实现的。一个JS类包含一个constructor和prototype object以及包括其它如__proto__在内的property。通过v8提供的C++ API来完成这些操作其实是生成与JS的类概念相同的代码。我后面会写一篇文章介绍“callback from libuv to JS”,里面会详细介绍对于一个具体的callback function如何通过这种方式来实现从C++世界到JavaScript世界的穿越。






作者: zangcf    时间: 2016-7-28 19:11
本帖最后由 zangcf 于 2016-7-28 19:22 编辑

Node.js挖掘之四:从libuv到JS,一个TCP连接事件引发的一系列callback的逆向调用过程
本文所做的研究基于Node.js v0.12.4 Linux版本。
说段题外话。算上大学跟老师一起做的项目,接触软件应该也有11年了。这期间接触过各类主流和非主流编程语言,自然深深体会到C的精悍、C++ STL的强大、Java的友好、bash的方便快捷、JavaScript的古灵精怪以及万变归宗的汇编和机器码。嗯,还有matlab…… 曾经也在网上因为编程语言的选择跟人争吵,然而人到中年,对编程语言和软件工程的理解确是上了一层楼。当年的真吵其实源自自己理解的肤浅和不自信,人总是会因为底气不足而与人真吵。
编程语言仅是实现软件工程的手段,根本没有优劣之分,只有适不适合。一个软件的成功远不是选对一个实现语言就能决定了的,即使选对了适合的语言对于软件的成功最多也是充分条件,而不是必要条件。一个软件或者一个软件方面的创新能不能快速普及仅仅取决于软件本身的扩散速度、范围和模式。也就是说扩散的意义要远大于创新本身,更远大于支撑这个创新的语言。
很多人莫名崇拜C,C++,鄙视JS。一句”toy language”尽显不屑,非要把JS囚禁在网页前端编程的牢笼里不可,直到Node的出现,JS才被无罪释放出来。其实为什么一定要斗得你死我活而不能彼此和谐共存呢?你看风清扬作为华山气宗一等一高手,一手独孤九剑不也舞得名扬武林么?
喜欢Node是因为它能把JS,C++这两种侧重点完全不同的语言优雅地结合在一起,并且结合操作系统的基础功能充分释放各自的最大优势。如果说JS以前是一门用于实现小功能的语言话,遇上了Node.js后, JS就像小排量发动机装上了涡轮增压器,瞬间变成小怪兽了。还有什么比给别人一个机会去”发现自己的潜能,做最好的自己”更好呢?
言归正传。这是Node.js挖掘的第4篇文章。前三篇,分别描述了Node.js架构、组成Node.js的几个重要英雄好汉以及以建立一个http server为例,讲述从我们自己的JavaScript代码,到Node内建的JS和C++模块,再到最后的libuv的长途奔袭过程。图1中的步骤1,2,3,4,5展示了这样长途奔袭的路线。该路线开始于我们JS代码 server.listen(),终止于libuv库的uv_listen()。如果把libuv看做是与Node.js独立的项目的话,这个奔袭相当于跨越了3个国境。
[attach]239[/attach]
图1:建立http server涉及的操作
如果说图1中的5个步骤是一个正向调用过程的话,那么当客户端向此http server发起一个TCP连接时,server端发生的一系列callback调用则是一个逆向调用过程。该逆向过程开始于libuv,终止于我们的JS代码里面设置的JS callback 函数。本文主要讲述这个逆向过程中发生的事情以及为实现这个逆向调用所做的准备工作。
让我们先回顾一下正向调用过程中发生的事情,特别是Node.js提供的C++模块 tcp_wrap所做的事情。这样我们能更好地理解逆向过程。图2所示是创建http server的正向调用过程。该过程大部分的时间花在net.js上,直到最下面红色方框内所标识的关键一步调用 createTCP()。
        // constructor for lazy loading        function createTCP() {          var TCP = process.binding('tcp_wrap').TCP;          return new TCP();        }代码很简单。绑定模块tcp_wrap,并调用TCP constructor。然而这真的简单吗? 表面平静的水面,下面很可能暗潮汹涌。让我们来看看水面下面发生了什么事情。
图2:create http server正向调用序列图
1.1        tcp_wrap模块的准备工作
我们来详细看下TCPWrap::Initialize()所做的工作。为了节省版面,我把代码做了排版,删除了一些与本文主题无关的代码。有强迫症的人可以自己去看源代码。
        void TCPWrap::Initialize(Handle<Object> target, Handle<Value> unused, Handle<Context> context) {          Environment* env = Environment::GetCurrent(context);                  Local<FunctionTemplate> t = FunctionTemplate::New(env->isolate(), New);          t->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "TCP"));          t->InstanceTemplate()->SetInternalFieldCount(1);          // Init properties          t->InstanceTemplate()->Set(String::NewFromUtf8(env->isolate(), "reading"),Boolean::New(env->isolate(), false));          t->InstanceTemplate()->Set(String::NewFromUtf8(env->isolate(), "owner"),Null(env->isolate()));          t->InstanceTemplate()->Set(String::NewFromUtf8(env->isolate(), "onread"),Null(env->isolate()));          t->InstanceTemplate()->Set(String::NewFromUtf8(env->isolate(),"onconnection"),Null(env->isolate()));                  NODE_SET_PROTOTYPE_METHOD(t, "open", Open);          NODE_SET_PROTOTYPE_METHOD(t, "bind", Bind);          NODE_SET_PROTOTYPE_METHOD(t, "listen", Listen);          NODE_SET_PROTOTYPE_METHOD(t, "getsockname", GetSockName);                  target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "TCP"), t->GetFunction());          }                NODE_MODULE_CONTEXT_AWARE_BUILTIN(tcp_wrap, node::TCPWrap::Initialize)
我想说,这真是一段非常优雅的代码。这个函数其实利用v8 engine的function template(函数模版)来模拟创建了一个JS类。类的constructor是 TCP(),类的prototype则由一系列NODE_SET_PROTOTYPE_METHOD()的调用设置,而类的属性设置则由t->InstanceTemplate()->Set()完成。上面这段代码有几个重要地方值得关注:
在net.js的函数createTCP()里调用process.binding(‘tcp_wrap’)时,其实会调用到图3所示的代码截图。在红色高亮方框里,可以看到参数target实际上是被设成了export对象。换句话说,我们的tcp_wrap模块的真正导出函数为TCP,而TCP被设置成了函数模版t的实例化函数。
[attach]240[/attach]
图3:Binding代码截图


当然这段代码不可能真的生成JS代码,而是在编译时直接变成了机器码。为了理解方便,下面显示的是模拟生成的JS代码。此处请关注一个重要的部分TCP.onconnection = null。后面会再次回过来分析这句话。
        function TCP()        {         /*code*/        }        TCP.prototype.listen = function(){/*code*/};        TCP.prototype.bind = function(){/*code*/};        TCP.prototype.getsockname = function(){/*code*/};        TCP.onconnection = null;        TCP.reading = false;
当在net.js的函数createTCP()里调用net TCP()时,返回了上面函数模版t生的实例函数(或者上面模拟生成的JS代码)的对象。这是非常重要的一步。因为这步返回的对象会 _listen2函数中被使用。其实无论是JS语言,还是C++语言,最终都会编译成机器码,供CPU执行。用殊途同归来形容这两个不同的旅程再合适不过。
1.2        Server.prototype._listen2所做的准备工作
结合图2以及1.1节所做的分析,可以看到在函数_listen2中调用createServerHandle()返回了一个重要的对像_handle,如图4所示。
[attach]241[/attach]
图4:net.js _listen2截图
图4中除了返回_handle这样一个重要的对象外,还有另外一个非常重要的操作:self._handle.onconnection = onconnection;聪明的你一定想到了在1.1节我提到函数模版t的类属性onconnection被设置成了null。而这个地方将其设置成了真正有效的函数地址。
1.3        AsyncWrap所做的准备工作
[attach]242[/attach]
图5:AsynWrap和Env代码截图
你一定会好奇我为什么要截一段代码放这里,却什么都不写。其实不是不写,而是未到写的时候,这段截图会在第2节中用到。
行文至此,让我们把上面3个准备工作做一个总结。
[attach]243[/attach]
图6:准备工作总结
终于到我们的正题了。没有办法,准备工作做得太多了。我们的图2其实仅仅画到了Open()操作。其实如果你仔细看图4中的绿色高亮框里面的代码,会发现在正向调用过程中,还包括了listen()的调用,这个调用最终在TCPWrap:isten()中实现。
需要强调的是,在正向调用过程中,从createServer()开始,到listen()结束,我们的目的还都是在创建一个基于TCP的http server。如果你熟悉socket编程,一定会知道,到此为止,创建的工作已经完成,剩下要做的是等客户端的连接过来。本文所提到的所有的callback调用都是基于一个触发事件: 客户端TCP连接。
而这所有的callback执行的目的则是对应用程序开发者构造出一个类型为Socket的对象,并且基于此对象完成面向此连接的数据流读操作。这个数据流读操作由2.3节的callback connectionListener完成。
当数据流读操作完成,connectionListener()会创建出一个req对象,而你一定会知道这个req是什么。图7描述了这样的过程。
[attach]244[/attach]
图7:正向和逆向全局图
2.1        第一个callback调用
TCPWrap:isten()通过libuv提供的函数uv_listen()实现了异步listen调用,并且指定了调用返回callback函数TCPWrap::OnConnection()。结合图1所示的流程可以看奥OnConnection()由libuv的event loop调用。让我们看看这个callback调用发生了什么事情。
让我们跳过所有与主题无关的代码,直接看最后一行:tcp_wrap->MakeCallback(env->onconnection_string(), ARRAY_SIZE(argv), argv);
看到MakeCallback你一定会想到1.3节的截图。对,图5里面有MakeCallback。但是env->onconnection_string()又是什么呢?
在Node.js Env.h和Env-inl.h中搜索PER_ISOLATE_STRING_PROPERTIES(v)会出现若干引用以及宏定义。如果将其展开,就得到图5中所示的代码。所以env->onconnection_string()会返回symbole “onconnection”。而在图5的MakeCall中,通过object()->Get()来获取与symbol对应的函数。如果对C++熟悉的话,你一定会想起来这个地方的object是谁。那么这个地方与symbol对于的函数是谁呢?答案是:我们在图4中设置的JS callback function _handle.onconnection = onconnection
2.2        第二个callback调用
后面的事情大家都猜到了。net.js中的onconnection()会被调用。我们来看下这个callback。图8是代码截图。

[attach]245[/attach]
图8: net.js onconnection callback
很显然,这里做了2个重要的事情,一个是创建了Socket对象,另外一个是发出了一个connection事件。那么这个connection事件是发给谁的呢?让我们再回到图2。开发者调用createServer()时,其实是在执行 new Server()。而在类Server中对于connection时间,有一个侦听者,那就是connectionListener(),也就是下面要说的第3个callback。
2.3 第三个callback调用
我觉得后面的事情,可以不要再赘述了。












作者: zangcf    时间: 2016-7-28 19:41
Node.js挖掘之五:浅析模块加载流程以及我们的应用如何启动
本文所做的研究基于Node.js v0.12.4 Linux版本。 Node.js挖掘进行到第5篇。
我们的Node应用不可避免,直接或间接地引用第三方模版。这些模块或是Node自带的,或是发布在npm上的。本文主要探讨两个问题:
应用加载和启动的过程涉及到模块的加载,所以放在第二个来探讨。
1. 模块加载详细过程

严格来讲,Node世界里面分以下几种模块:
1.1 builtin module和native module生成过程

[attach]246[/attach]
图1:builtin module和native module生成过程
native JS module的生成过程相对复杂一点,把node的源代码下载下来,自己编译后,会在 out/Release/obj/gen目录下生成一个文件node_natives.h。如图2所示。

[attach]247[/attach]
图2:node源码编译后的输出截图
该文件由js2c.py生成。 js2c.py会将node源代码中的lib目录下所有js文件以及src目录下的node.js文件中每一个字符转换成对应的ASCII码,并存放在相应的数组里面。如图3的截图所示(数组内容被截掉一部分)。
[attach]248[/attach]
图3: node_natives.h截图
图3左边的数组node_native,对应的文件为src/node.js。数组里面数字分别为node.js中每个字符的ASCII码。图3右边为node_natives.h其余代码,在1.2节会用到。
builtin C++ module生成过程较为简单。每个builtin C++模块的入口,都会通过宏NODE_MODULE_CONTEXT_AWARE_BUILTIN扩展为一个函数。例如对于tcp_wrap模块而言,会被扩展为函数static void _register_tcp_wrap (void) attribute((constructor))。熟悉GCC的同学会知道通过attribute((constructor))修饰的函数会在node的main()函数之前被执行,也就是说,我们的builtin C++模块会被main()函数之前被加载进modlist_builtin链表。modlist_builtin是一个struct node_module类型的指针,以它为头,get_builtin_module()会遍历查找我们需要的模块。
对于node自身提供的模块,其实无论是native JS模块还是builtin C++模块,最终都在编译生成可执行文件时,嵌入到了ELF格式的二进制文件node里面,输入命令”readelf -s node|grep node_native”可以看到详细的信息。而对这两者的提取方式却不一样。对于JS模块,使用process.binding(“natives”),而对于C++模块则直接用get_builtin_module()得到,这部分会在1.2节讲述。

1.2 详解Binding()
在node.cc里面提供了一个函数Binding()。当我们的应用或者node内建的模块调用require()来引用另一个模块时,背后的支撑者即是这里提到的Binding()函数。后面会讲述这个函数如何支撑require()的。这里先主要剖析这个函数。
[attach]249[/attach]
图4:Binding()函数代码截图
图4是函数代码截图。可以看到函数主要为三类模块服务: builtin, constants以及native。对这三类模块,由exports带回的值所代表的意义是不一样的。
builtin优先级最高。对于任何一个需要绑定的模块,都会优先到builtin模块列表modlist_builtin中去查找。查找过程非常简单,直接遍历这个列表,找到模块名字相同的那个模块即可。找到这个模块后,模块的注册函数会先被执行,且将一个重要的数据exports返回。对于builtin module而言,exports object包含了builtin C++模块暴露出来的接口名以及对于的代码。例如对模块tcp_wrap而言,exports包含的内容可以用如下格式表示: {“TCP”: “/function code of TCPWrap entrance/”, “TCPConnectWrap”: “/function code of TCPConnectWrap entrance/”}。
constants模块优先级次之。node中的常量定义通过constants导出。导出的exports格式如下: {“SIGHUP”:1, “SIGKILL”:9, “SSL_OP_ALL”: 0x80000BFFL}
对于native module而言,图3中除了数组node_native之外,所有的其它模块都会导出到exports。格式如下: {“_debugger”: _debugger_native , “module”: module_native ,“config”: config_native } 其中,_debugger_native,module_native等为数组名,或者说就是内存地址。
对比上面三类模块导出的exports结构会发现对于每个属性,它们的值代表着完全不同的意义。对于builtin 模块而言,exports的TCP属性值代表着函数代码入口,对于constants模块,SIGHUP的属性值则代表一个数字,而对于native模块,_debugger的属性值则代表内存地址(准确说应该是 .rodata段地址)。
1.3 process
如果我们去查看Node在线的API文档,会发现有一个process类,而且该类提供了若干方法。另外查看src\node.js源代码,会发现调用了大量的process.binding()。那么这个process到底是什么呢,源代码在哪里?又是如何提供给我们使用的?
在node.cc Start()函数内部,会调用另外一个函数CreateEnvironment()。在这个函数内部,利用V8的function template机制,构建了一个process类以及其对应的方法和属性。读者可以参考笔者的“Node.js挖掘之四:从libuv到JS,一个TCP连接事件引发的一系列callback的逆向调用过程”的1.1节了解如果使用function template去构建一个prototype based JS类。
通过函数CreateEnvironment()以及它内部调用的SetupProcessObject()函数:
构建好的process类会产生一个对象,名为process_object,并且将其作为V8的Persistent handle通过env->set_process_object()函数保存。读者可以参照“Node.js挖掘之四:从libuv到JS,一个TCP连接事件引发的一系列callback的逆向调用过程”的1.3节详细了解这一步是如何实现的。
因为process_object为Persistent handle,使得该handle不会因为handle scope问题而被销毁掉。
env指向数据结构Environment。Environment像是一个中介,它作用之一便是把代表JS运行环境的V8 context和异步核心libuv的event loop撮合在一起。V8和libuv都是重量级人物,而Environment的存在使得node得以左手翻云,右手覆雨。


2. 应用启动详细过程
[attach]250[/attach]

图5:应用启动流程


上一节描述了模块的加载和引用(绑定)过程。这个过程与本节要讲述的应用启动过程关系密切。当然,模块的引用不只是为应用启动服务的,在任何时候,任何JS代码中,我们都可以引用其它模块。图5是一个流程图,它描绘了从node将我们的应用test.js作为参数启动开始,到我们的test.js最终被执行的过程。从途中可以看到,一共有如下几个主要参与者:
这个过程可以用一句话概括:跋涉千山万水。
2.1 跋涉第一步
跋涉的第一步是准备process 这样一个V8 Persistent handle。这步在1.3节已经做了详细说明。
跋涉的第二步由src\node.js开始。可以说这才是真正的启动脚本,脚步入口在函数startup() src\node.js在编译的时候,编译进数组node_native,而触发这一步的地方在node.cc LoadEnvironment()。如果我们仔细看src\node.js代码,会发现它是一个匿名函数,而且参数为process。那么这个参数在哪里设置的呢?仔细研究图6所示的LoadEnvironment()代码截图,就会得到答案。
[attach]251[/attach]
图6:LoadEnvironment关键代码截图
2.3 跋涉第三步
第三步源于src\node.js需要引用native模块module.js。引用事件由NativeModule.require(‘module’)触发。而这步又由4个步骤组成。可以参考图5。这里有两个重要的地方需要重点关注,我们都知道在node里面,每个JS模块都会被加上如图7所示的wrap。那么这一个动作是在什么时候做的呢?答案就在图5里面。
[attach]252[/attach]
图7:JS模块wrap代码截图
第二个需要关注的是,我们都知道node为每个JS模块添加的wrap都包含参数exports,require,module等,JS模块内部的代码也可以自由访问这些参数。那么这些参数到底是什么?答案也在图5里面。
2.4 跋涉第四步
第四步跋涉则完全由module.js完成。module.js负责完成我们应用程序的加载、wrap、编译以及最终的调用。




作者: zangcf    时间: 2016-7-28 19:47
第六章 暂时不学习,网址为如下:

http://cnodejs.org/topic/55e3aff095d4c65e3d88a704




欢迎光临 Discuz! Board (http://47.89.242.157:9000/bbs/discuz/) Powered by Discuz! X3.2