nodejs代码初探
发布于 6 年前 作者 newcoder 28823 次浏览 最后一次编辑是 1 年前

之所以对nodejs产生兴趣,是据我以往的经验,脚本和异步是随处可见的。可以说nodejs提供了构建一大类应用的基础设施,它的应用潜力非常大,绝不会局限于互联网领域,在行业应用领域也应该也大有作为。就算不使用nodejs,v8和libuv本身如此的优秀,也非常值得做一番了解。 花了两天时间读了一些nodejs代码,就当这些文字是一篇游记,像刚爬过一座小山或者穿过一片小树林,留一点记录,如能对同行者有点启发,更好。

javascript 和 v8

做一个应用就是构建一套类型系统。javascript是动态语言,我们可以在程序的运行过程中增加类型,添加属性方法,甚至改变继承关系。Javascript提供了这些能力,但这并不意味我们创建的应用中的类型系统在运行时是不断变化,琢磨不定的。事实上,大部分情况下,javascript程序运行一段时间以后,类型就基本稳定了。v8在执行javascript代码的过程中动态地识别出类型,将类型直接编译成机器码,运行。同时,v8一直动态地优化代码,并且有高效的GC打扫运行场地。一句话,v8是可以信任的,不要去担心性能问题。

如何使用v8,官方有些例子,基本够用了。提供的API是C++风格的(参看v8.h/api.cc)。在阅读nodejs代码之前,熟悉v8的使用方式是很必要的。

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) { ',
'\n});'
];

上面介绍的模块都是静态的,其实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);
}

//Over.

13 回复

欣赏这种精神

果断关注楼主了!

多谢各位指教,如果你们正在看nodejs代码,这篇东西可能有些提示。以后有时间写点应用,有时我回顾之前做过的一些产品,发现基于nodejs去实现会简洁快速很多。总有许多的idea困扰着我,没时间啊,哈哈。

好文章,正愁没有详细介绍nodejs源码的文章呢。

这么好的文章,赞一个。 不知道楼主能否再详细点介绍libuv这个异步操作库。

mark!<br/><br/>来自mov1er的cnode!

回到顶部