资讯专栏INFORMATION COLUMN

干货 | 走进Node.js之启动过程剖析

luck / 2719人阅读

摘要:具体调用链路如图函数主要是解析启动参数,并过滤选项传给引擎。查阅文档之后发现,通过指定参数可以设置线程池大小。原来的字节码编译优化还有都是通过多线程完成又继续深入调查,发现环境变量会影响的线程池大小。执行过程如下调用执行。

作者:正龙 (沪江Web前端开发工程师)
本文原创,转载请注明作者及出处。

随着Node.js的普及,越来越多的开发者使用Node.js来搭建环境,也有很多公司开始把Web站点迁移到Node.js服务器。Node.js的优势显而易见,本文不再赘述,那么它是如何做到的呢?内部的逻辑又是什么?带着这些问题,笔者开始了研究Node.js的漫漫长征路。今天,笔者将跟大家探讨一下Node.js的启动原理。

Node.js内部主要依赖Google的V8引擎和libuv实现。V8,想必大家会比较熟悉,它首创把JavaScript直接翻译成汇编代码的方式执行,让很多不可能变成了可能,例如Node.js。libuv,是一个跨平台的异步IO库,它所说的IO除了包含本地文件操作,还包含TCP、UDP等网络套接字操作,范围甚至可以扩展到所有流操作(Stream)。所以,我们可以把Node.js理解为添加了网络功能的V8。

为了描述方便,下面提到的环境是基于Windows 7专业版。用MAC的伙伴们也不用慌,内容实质仍然适用,可能具体名词有些区别。另外,伙伴们可以下载一份Node.js的源代码(点此下载),本文用的是6.10.0 LTS。

我们打开Node.js的二进制发布包,里面内容很简单:node.exe、npm和node.h头文件。node.h头文件只有开发Node.js插件时才会用到。当我们启动node.exe时,它到底做了哪些事情?

首先,它是一个EXE可执行文件,那肯定会有一个main函数。Node.js的main函数定义在node_main.cc中,它主要是初始化V8 Platform和v8引擎;然后会启动一个Node.js实例。具体调用链路如图:

Init函数主要是解析Node.js启动参数,并过滤V8选项传给JavaScript引擎。

Node.js的main函数原来这么短,那它应该很快运行完并返回。实际上,命令行窗口会一直等待着,并没有马上退出,这又是怎么回事呢?答案就在StartInstance里。首先它会创建V8执行沙盒,生成并初始化Node.js运行环境对象,然后启动Node.js的循环等待。具体如图:

也就是说Node.js的主线程主要消费来自UV默认事件循环(uv_default_loop)和V8的MainThreadQueue和MainThreadDelayedQueue的任务。uv_run是一个阻塞调用。如果队列中有任务,则执行并返回true,如果没有的话,会阻塞住当前线程;如果返回false,则整个Node.js进程会释放资源并退出。注意参数UV_RUN_ONCE,意思是从队列中只取一个任务执行,不管队列中当前是否有多个任务。

到这儿,大概可以理解到Node.js的“单线程”是怎么回事。那运行的Node.js进程确实只开启了一个线程吗?我们打开任务管理器看看:

实际上,Node.js进程当前有7个线程。查阅文档之后发现,Node.js通过指定参数--v8-pool-size可以设置V8线程池大小。原来V8的字节码编译、优化还有GC都是通过多线程完成;又继续深入调查,发现环境变量UV_THREADPOOL_SIZE会影响libuv的线程池大小。

Node.js目前为止做的事情可以归纳为,初始化V8和libuv。接下来,我们看看Node.js自身运行环境是怎样构建起来的。Node.js自身的运行环境由Environment类表示,我们需要把process对象构建起来。process对象在JavaScript应用代码中是可以访问到,它的文档可以狠戳这儿。注意,process现在还没有赋值给Global对象。CreateEnvironment执行流程如图:

调用setAutorunMicrotask禁止V8自己消费队列中的任务。SetupProcessObject主要设置process的属性,例如比较重要的binding,还有其它提供给开发者的字段,比如cpuUsage、hrtime、uptime等。binding用于获取C/C++构建的模块,Node.js中的net库就是通过这种方式最终调用到libuv。

binding就是做模块查找,其执行过程如下:

从Args中获取到模块名称。

从Binding Cache中看是否能找到模块,如果有直接返回模块的exports。

3往Module Load List中追加一条模块记录,名称为"binding " + 模块名。

调用get_builtin_module,参数是模块名,get_builtin_module会从modlist_builtin列表中查找内置模块,所有内置模块和第三方扩展都记录在modlist_builtin列表中。C/C++模块通过NODE_MODULE_CONTEXT_AWARE_BUILTIN注册,第三方扩展模块通过NODE_MODULE注册。最终都会调用node_module_register。node_module结构体包含注册函数、模块名称、文件名称等信息。

如果查找到,则返回对应模块的exports。

如果模块名是constants,则调用DefineContstants。

如果模块名是natives,则调用DefineJavaScript,会返回所有内置模块,它们一般由Javascript实现。这些模块在/lib目录下,会通过js2c.py转成c代码,js2c.py会生成一个临时文件node_natives.h,里面包含了NODE_NATIVES_MAP的定义。

否则,抛出错误:没有指定名称的模块。

环境对象准备好之后,就开始真正加载Node.js自身提供的JavaScript类库代码。LoadEnvironment执行过程如下:

调用ExecuteString执行bootstrap_node.js。bootstrap_node.js文件里定义了一个函数它会往Global对象上添加属性,通过internal/module加载Node.js自身提供的JavaScript类库。

执行上一步返回的函数,并传入env->process_object()对象。

到这儿,我们可以总结2个问题:

Node.js里面自己提供的JavaScript库是怎么实现的?

通过C/C++代码封装成Node.js内置模块,然后再通过process.binding暴露给JavaScript。

JavaScript库文件是怎么打包在node.exe中?

Node.js内置的JavaScript文件,通过js2c.py编译生成临时文件node_natives.h。

原理思路基本搞明白之后,下面我们来做个小实例:如何把C++对象暴露给JavaScript。
程序主要是C++和JavaScript的交互,通过Node.js插件的方式运行。所以大家需要先了解下如何编译Node.js插件,官方文档猛戳这儿。

首先定义要导出的C++类,构造器可以传入一个数值;调用成员方法PlusOne,数值自增1并返回当前值。

namespace demo {
    class MyObject : public node::ObjectWrap {
    public:
        static void Init(v8::Local exports);
        static void NewInstance(const v8::FunctionCallbackInfo& args);
        inline double value() const { return _value; }

    private:
        explicit MyObject(double value = 0);
        ~MyObject();

        static void New(const v8::FunctionCallbackInfo& args);
        static void PlusOne(const v8::FunctionCallbackInfo& args);
        static v8::Persistent constructor;
        double _value;
    };
}

实现文件

    void MyObject::NewInstance(const FunctionCallbackInfo& args) {
        Isolate* isolate = args.GetIsolate();

        const unsigned argc = 1;
        Local argv[argc] = { args[0] };
        Local cons = Local::New(isolate, constructor);
        Local context = isolate->GetCurrentContext();
        Local instance = cons->NewInstance(context, argc, argv).ToLocalChecked();

        args.GetReturnValue().Set(instance);
    }


    void MyObject::Init(Local exports) {
        Isolate* isolate = exports->GetIsolate();

        Local tpl = FunctionTemplate::New(isolate, New);
        tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject"));
        tpl->InstanceTemplate()->SetInternalFieldCount(1);

        NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);

        constructor.Reset(isolate, tpl->GetFunction());
        exports->Set(String::NewFromUtf8(isolate, "MyObject"), tpl->GetFunction());
    }

    void MyObject::New(const FunctionCallbackInfo& args) {
        double value = args[0]->IsUndefined() ? 0 : args[0]->NumberValue();
        MyObject* obj = new MyObject(value);
        obj->Wrap(args.This());
        args.GetReturnValue().Set(args.This());
    }

    void MyObject::PlusOne(const FunctionCallbackInfo& args) {
        Isolate* isolate = args.GetIsolate();
        MyObject* obj = ObjectWrap::Unwrap(args.Holder());
        obj->_value += 1;

        args.GetReturnValue().Set(Number::New(isolate, obj->_value));
    }

    NODE_MODULE(addon, MyObject::Init)

修改binding.gyp文件

{
  "targets": [
    {
      "target_name": "addon",
      "sources": [ "myobject.cc" ]
    }
  ]
}

通过node-gyp build编译成功之后会在build/Release/目录下生成文件addon.node。这样我们就可以在JavaScript中使用MyObject了:

const addon = require("./addon");

let obj = new addon.MyObject();
console.log(obj.plusOne());
console.log(obj.plusOne());
console.log(obj.plusOne());

let obj1 = new addon.MyObject(10);
console.log(obj1.plusOne());

执行结果如下:

虽然Node.js的启动过程很简洁,但还是有一些问题可以继续深挖。比如,一个网络请求在Node.js中到底是怎么被处理的呢?希望本文可以抛砖引玉,在入门阶段给大家一点帮助。

iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。

>> 沪江Web前端上海团队招聘【Web前端架构师】,有意者简历至:zhouyao@hujiang.com <<

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/88435.html

相关文章

  • 干货剖析 | 走进Node.js启动过程

    摘要:具体调用链路如图函数主要是解析启动参数,并过滤选项传给引擎。查阅文档之后发现,通过指定参数可以设置线程池大小。原来的字节码编译优化还有都是通过多线程完成又继续深入调查,发现环境变量会影响的线程池大小。执行过程如下调用执行。 作者:正龙 (沪江Web前端开发工程师)本文原创,转载请注明作者及出处。 随着Node.js的普及,越来越多的开发者使用Node.js来搭建环境,也有很多公司开始把...

    Simon 评论0 收藏0
  • 系列3|走进Node.js多进程模型

    摘要:例如,在方法中,如果需要主从进程之间建立管道,则通过环境变量来告知从进程应该绑定的相关的文件描述符,这个特殊的环境变量后面会被再次涉及到。 文:正龙(沪江网校Web前端工程师)本文原创,转载请注明作者及出处 之前的文章走进Node.js之HTTP实现分析中,大家已经了解 Node.js 是如何处理 HTTP 请求的,在整个处理过程,它仅仅用到单进程模型。那么如何让 Web 应用扩展到...

    snowell 评论0 收藏0
  • 走进Node.js HTTP实现分析

    摘要:事实上,协议确实是基于协议实现的。的可选参数用于监听事件另外,它也监听事件,只不过回调函数是自己实现的。并且会把本次连接的套接字文件描述符封装成对象,作为事件的参数。过载保护理论上,允许的同时连接数只与进程可以打开的文件描述符上限有关。 作者:正龙(沪江Web前端开发工程师)本文为原创文章,转载请注明作者及出处 上文走进Node.js启动过程中我们算是成功入门了。既然Node.js的强...

    April 评论0 收藏0
  • Node.js 进程平滑离场剖析

    摘要:在谈如何做到进程平滑离场前,我们需要一种机制,这种机制能让我们主动通知进程何时离场,这就涉及到进程间通信的知识了,我们先简单了解下。进程间通信对或类系统而言,进程间通信的方式有很多种信号是其中的一种。 本文由云+社区发表作者:草小灰 使用 Node.js 搭建 HTTP Server 已是司空见惯的事。在生产环境中,Node 进程平滑重启直接关系到服务的可靠性,它的重要性不容我们忽视...

    xbynet 评论0 收藏0
  • 《阿里云前端技术周刊》第二期

    摘要:作者也树校对染陌素材也树英布阿里云前端技术周刊由阿里云智能商业中台体验技术团队整理编写。如何在工作中快速成长致工程师的个简单技巧工程师成长干货,全文提纲如下,图片来自阿里技术公众号关于我们我们是阿里云智能中台体验技术团队。 作者:@也树 校对:@染陌 素材:@也树、@英布 《阿里云前端技术周刊》由阿里云智能商业中台体验技术团队整理编写。 知乎:阿里云中台前端/全栈团队专栏 Github...

    kyanag 评论0 收藏0

发表评论

0条评论

luck

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<