zoukankan      html  css  js  c++  java
  • nodejs代码初探之nodejs启动

    nodejs启动

    入口在node_main.cc,解析参数后进入node.cc 中的node::Start()

    V8::Initialize() //初始化v8
    SetupProcessObject() //在v8中创建process对象
    Load() //bootstrap,加载执行node.js
    uv_run() //调用libuv,开始异步事件polling和处理
    模块装载

    为了和用户编写的模块做区别,我将nodejs自带的模块称为系统模块,其中C++模块在src目录中,node_开头的那些文件大部分是,javascript模块在lib目录中。模块装载是指将C++或者javascript模块加载到v8引擎中,生成对象系统,可供javascript代码调用。nodejs采用了延迟加载的策略,系统模块只有用到的时候才会加载,加载完后放到binding_cache中。

    上面提到,nodejs在启动的时候会创建process对象。process.binding()方法用来将系统模块加载到v8中去(参见node.cc中的Binding函数)。

    C++模块的装载比较直接,一个典型的C++模块具有如下形式,在register_func中将对象,函数等注册到v8中,这样javascript代码就能调用它们了。

    void register_func(Handle<Object> target) {
    // 模块注册函数
    }
    NODE_MODULE(node_test, register_func) // 加入C++模块列表
    对于上面的模块,调用process.binding(“test”)就可以装载。

    对于javascript模块,装载稍微麻烦点。首先,lib目录中的js文件,编译nodejs的时候会通过js2c.py将它们转换成C数组,放在中间文件node_natives.h中,这样子,这些js文件就已经成为代码的一部分,nodejs就不需要再读取这些系统js文件了,加快了装载速度。包括之前提到的用来引导系统的node.js也是通过这种方式处理的,你可以通过require(‘native_module’)来使用node.js。

    好了,现在我们知道了系统js文件已经作为C数组编译到代码中了,怎么将它们装入v8?分两步来进行,第一步,引导脚本node.js调用我们之前提到的process.binding(‘natives’),将它们作为数组装入v8(参见node.cc中的Binding函数和node_javascript.cc中的DefineJavaScript函数)。但是这时候装载还未完成,因为还没有执行这些js文件。第二步,调用node.js中的NativeModule.require()装载单个js模块,在执行之前,系统会给js模块加个wrapper,传入几个常用对象,这样模块中就能使用系统传入的这几个对象了。

    NativeModule.wrapper =
    ['(function (exports, require, module, __filename, __dirname) { ',
    ' });'
    ];
    上面介绍的模块都是静态的,其实nodejs也可以装载动态模块,unix下的共享库,Windows下的DLL。前面提到的process对象有一个dlopen方法,可以用来装载动态模块。对动态模块有一个要求,就是要导出一个init函数,系统会调用此函数来将对象装载到v8中。

    void init(Handle<Object> target) {
    // 模块注册函数
    }
    最后,还有一个问题,对于用户编写的模块,系统是如何加载的?nodejs通过模块module.js来管理和装载用户模块,装载使用Module.prototype.require(),包括node_modules的路径处理等等细节参见module.js文件。

    说完了模块加载,扩展nodejs是很容易的事情了。大部分情况我们是不需要扩展nodejs的。在一些特殊的场合下,可能要求对nodejs做扩展,比如有大量legacy的C/C++的代码想要集成到nodejs。

    异步调用的实现

    接下来说说nodejs中的异步调用是如何实现的。这块也是个人兴趣较大的地方,在之前的工作中也实现过异步的操作模式,无非是线程消息队列嘛,但当时我的实现不是通用的模块。

    前面提到函数node::Start()在装载完所有模块后,就开始执行消息队列的pooling和处理,调用uv_run(uv_default_loop())。uv_default_loop()函数会初始化uv,也会初始化并返回一个default loop。参见core.c,既然我们已经进入libuv的领地了,先简单介绍一下libuv。libuv显然是要抹平操作系统的差异,封装libev,libeio和Windows的io completion port,向用户提供一个跨平台的异步操作库。

    libuv 简介

    libuv 的API是C风格的,很容易读。你可能觉得uv.h中暴露了太多的数据结构了,不够简洁,我想是因为libuv涵盖的内容非常的广泛,从网络,pipe,文件,终端等等,包罗万象。而且uv.h也不是接口的全部,还有两个头文件,uv-unix.h和uv-win.h,里面定义了操作系统specific的数据结构。其实,libuv已经对接口的简化做了一些努力,比如说,通过uv_write一个函数,我们可以写TCP,Pipe和tty。

    因为libuv涵盖广泛,我们的目的只是为了了解它如何与nodejs,v8协调工作的,不会面面俱到。有几个重要的数据结构有必要先了解一下:

    #define UV_HANDLE_FIELDS
    uv_loop_t* loop;
    uv_handle_type type;
    uv_close_cb close_cb;
    void* data;
    UV_HANDLE_PRIVATE_FIELDS

    /* The abstract base class of all handles. */
    struct uv_handle_s {
    UV_HANDLE_FIELDS
    };
    uv_handle_s是其它handle的父类,比如说,uv_tcp_s就是它的子类,在那里你能找到socket。loop字段表明它属于哪个loop,handle里还有一些callback函数,异步调用通常会有两个参数,一个是handle,一个是callback函数,调用完成的时候,libuv会调用callback函数。data字段是留给使用者的,nodejs实现异步机制的时候会用到。

    #define UV_REQ_FIELDS
    uv_req_type type;
    void* data;
    UV_REQ_PRIVATE_FIELDS

    /* Abstract base class of all requests. */
    struct uv_req_s {
    UV_REQ_FIELDS
    };
    这是所有request的父类。loop维护一个request queue。data字段有时用来存放handle。
    再来看loop,这是libuv里面最关键的数据结构了,一般会指定一个线程负责一个loop的处理,nodejs只使用了一个loop,由主线程负责对它进行处理。

    struct uv_loop_s {
    UV_LOOP_PRIVATE_FIELDS
    uv_ares_task_t* uv_ares_handles_;
    uv_async_t uv_eio_want_poll_notifier;
    uv_async_t uv_eio_done_poll_notifier;
    uv_idle_t uv_eio_poller;
    uv_counters_t counters;
    uv_err_t last_err;
    void* data;
    };
    我们只看Windows平台(参看uv-win.h)

    #define UV_LOOP_PRIVATE_FIELDS
    HANDLE iocp;
    int refs;
    int64_t time;
    uv_req_t* pending_reqs_tail;
    uv_handle_t* endgame_handles;
    ......
    我们已经知道有一些handles会和loop关联在一起,字段refs就是和它相关的handle的数量。它还会有一个请求队列,pending_reqs_tail。对windows来说,它有一个io completion port。初始化loop的时候会创建一个completion port,之后其它handles可以加入,通过这个port来监控事件。比如,当有新的socket建立的时候,再调用一次CreateIOCompletionPort()可以使用这个port来监控socket事件。
    了解了数据结构以后,程序怎么运行应该就大致有数了。来看一下执行过程。

    #define UV_LOOP_ONCE(loop, poll)
    do {
    uv_update_time((loop));
    uv_process_timers((loop));

    /* Call idle callbacks if nothing to do. */
    if ((loop)->pending_reqs_tail == NULL &&
    (loop)->endgame_handles == NULL) {
    uv_idle_invoke((loop));
    }

    uv_process_reqs((loop));
    uv_process_endgames((loop));

    if ((loop)->refs <= 0) {
    break;
    }

    uv_prepare_invoke((loop));

    poll((loop), (loop)->idle_handles == NULL &&
    (loop)->pending_reqs_tail == NULL &&
    (loop)->endgame_handles == NULL &&
    (loop)->refs > 0);

    uv_check_invoke((loop));
    } while (0);

    #define UV_LOOP(loop, poll)
    while ((loop)->refs > 0) {
    UV_LOOP_ONCE(loop, poll)
    }
    函数poll()检查completion port,有事件发生的时候,产生一条request,插入到请求队列中。
    函数uv_process_reqs()从消息队列中取出请求,处理请求。此函数在处理请求的过程中可能会调用用户传入的callback。
    函数uv_process_endgames()清理已经关闭的handle,同时减掉refs。
    对libuv的介绍到此打住,不再详细展开了。

    nodejs异步调用的实现

    接下来分析一下nodejs是如何将libuv和v8拼接在一起从而实现javascript代码中的异步函数调用的。
    其实看一个类就一目了然了,它就是HandleWrap,参见文件handle_wrap.h/handle_wrap.cc

    HandleWrap::HandleWrap(Handle<Object> object, uv_handle_t* h) {
    unref = false;
    handle__ = h;
    if (h) {
    h->data = this;
    }

    HandleScope scope;
    assert(object_.IsEmpty());
    assert(object->InternalFieldCount() > 0);
    object_ = v8::Persistent<v8::Object>::New(object);
    object_->SetPointerInInternalField(0, this);
    }
    handle_ //通过它调用libuv。
    object_ //v8中的javascript对象。
    this //Wrap自己,C++对象
    从构造函数可以看出,HandleWrap的作用就像是一座桥,把this传给了handle_,同时也把this传给了object_。这个C++对象将v8中的javascript对象和libuv中的C对象(handle)联通了。C/C++/javascript,完美的组合,是不是很美妙?

    HandleWrap是所有Wrap类的父类。为了看得更清楚,我们举一个具体的例子。TCPWrap,参见tcp_wrap.cc

    // 这个函数会被注册到javascript世界,成为TCP()对象的一个方法,javascript代码调用TCP().listen()会来到这里。
    Handle<Value> TCPWrap::Listen(const Arguments& args) {
    HandleScope scope;

    UNWRAP

    int backlog = args[0]->Int32Value();

    // 使用handle_和OnConnection 函数调用libuv,connection事件的时候,libuv会回调OnConnection。
    int r = uv_listen((uv_stream_t*)&wrap->handle_, backlog, OnConnection);

    // Error starting the tcp.
    if (r) SetErrno(uv_last_error(uv_default_loop()));

    return scope.Close(Integer::New(r));
    }

    // OnConnection函数在这里
    void TCPWrap::OnConnection(uv_stream_t* handle, int status) {
    HandleScope scope;
    // 下面两行不用再解释了吧。从handle拿到C++对象this。
    TCPWrap* wrap = static_cast<TCPWrap*>(handle->data);
    assert(&wrap->handle_ == (uv_tcp_t*)handle);

    // We should not be getting this callback if someone as already called
    // uv_close() on the handle.
    assert(wrap->object_.IsEmpty() == false);

    Handle<Value> argv[1];

    if (status == 0) {
    // 建一个新的javascript TCP()对象,并用它去Accept一个新的连接。
    // Instantiate the client javascript object and handle.
    Local<Object> client_obj = Instantiate();

    // Unwrap the client javascript object.
    assert(client_obj->InternalFieldCount() > 0);
    TCPWrap* client_wrap =
    static_cast<TCPWrap*>(client_obj->GetPointerFromInternalField(0));

    // Accept新的连接。
    if (uv_accept(handle, (uv_stream_t*)&client_wrap->handle_)) return;

    // Successful accept. Call the onconnection callback in JavaScript land.
    argv[0] = client_obj;
    } else {
    SetErrno(uv_last_error(uv_default_loop()));
    argv[0] = v8::Null();
    }

    // 进入javascript世界,并将新建的TCP()对象作为一个参数传入。
    MakeCallback(wrap->object_, "onconnection", 1, argv);
    }

  • 相关阅读:
    bzoj-2748 2748: [HAOI2012]音量调节(dp)
    bzoj-2338 2338: [HNOI2011]数矩形(计算几何)
    bzoj-3444 3444: 最后的晚餐(组合数学)
    codeforces 709E E. Centroids(树形dp)
    codeforces 709D D. Recover the String(构造)
    codeforces 709C C. Letters Cyclic Shift(贪心)
    codeforces 709B B. Checkpoints(水题)
    codeforces 709A A. Juicer(水题)
    Repeat Number
    hdu 1003 Max Sum (动态规划)
  • 原文地址:https://www.cnblogs.com/wangyonglong/p/6180945.html
Copyright © 2011-2022 走看看