Nodejs代码初探

Nodejs代码初探

之所以对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 ® SetErrno(uv_last_error(uv_default_loop()));

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

// 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-&gt;InternalFieldCount() &gt; 0);
TCPWrap* client_wrap =
    static_cast&lt;TCPWrap*&gt;(client_obj-&gt;GetPointerFromInternalField(0));

// Accept新的连接。
if (uv_accept(handle, (uv_stream_t*)&amp;client_wrap-&gt;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.


15 回复

Nodejs代码初探

之所以对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

对于用户编写的模块,系统是通过模块module.js来管理和装载用户模块,装载使用Module.prototype.require(),包括node_modules的路径处理等等细节参见module.js文件。

异步调用的实现

接下来分析一下Nodejs是如何将Libuv和V8拼接在一起从而实现JavaScript代码中的异步函数调用的。

libuv 简介

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

Nodejs异步调用的实现

从构造函数可以看出,HandleWrap的作用就像是一座桥,把C++对象(this)传给了handle_,同时也把this传给了object_。这个C++对象将V8中的JavaScript对象和Libuv中的C对象(handle)联通了。C/C++/JavaScript,完美的组合,是不是很美妙?

例如,TCPWrap中的Listen方法会被注册到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);

  Handle<Value> argv[1];

  if (status == 0) {
    // 建一个新的JavaScript TCP()对象,并用它去Accept一个新的连接。
    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);
}

这样,Nodejs就实现了异步调用,使得JavaScript代码能够以同步的方式编写异步逻辑。


很棒

欣赏这种精神

果断关注楼主了!

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

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

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

mark!!

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

Nodejs代码初探

入门与背景

我对Nodejs产生兴趣的原因在于其脚本和异步特性。Nodejs不仅限于互联网应用,还可以在各种行业中发挥作用。V8引擎和Libuv的高性能和稳定性也值得深入学习。

V8引擎与javascript

V8引擎在执行JavaScript代码时会动态地识别类型并编译成机器码,同时进行高效的垃圾回收。这使得V8引擎非常高效,不用担心性能问题。熟悉V8的API有助于更好地理解Nodejs的内部机制。

Nodejs启动流程

Nodejs的启动过程如下:

  1. 初始化V8引擎:调用V8::Initialize()来初始化V8。
  2. 创建process对象:调用SetupProcessObject()来创建process对象。
  3. 加载引导脚本:调用Load()来加载执行引导脚本(通常是node.js)。
  4. 开始异步事件循环:调用uv_run()来开始异步事件的监听和处理。
V8::Initialize();              // 初始化V8
SetupProcessObject();          // 创建process对象
Load();                        // 加载执行node.js
uv_run();                      // 开始异步事件循环

模块装载

Nodejs通过延迟加载策略来管理模块,即只有使用时才会加载模块。系统模块包括C++模块和JavaScript模块。

  • C++模块:通过NODE_MODULE宏注册到V8引擎中。
void register_func(Handle<Object> target) {
  // 模块注册函数
}  
NODE_MODULE(node_test, register_func) // 注册C++模块
  • JavaScript模块:通过js2c.py工具将JavaScript文件编译为C数组,存储在node_natives.h中。使用process.binding('natives')加载这些模块。

异步调用实现

Nodejs通过Libuv来实现异步调用。Libuv是一个跨平台的异步I/O库,负责事件循环和异步操作的处理。

  • 关键数据结构
    • uv_handle_s:表示各种类型的句柄,例如TCP、文件等。
    • uv_req_s:表示请求,例如读写请求等。
struct uv_handle_s {
  uv_loop_t* loop;
  uv_handle_type type;
  uv_close_cb close_cb;
  void* data;
};

struct uv_req_s {
  uv_req_type type;
  void* data;
};
  • 核心流程
    • 调用uv_run()开始事件循环。
    • uv_process_reqs()处理请求,调用相应的回调函数。
    • uv_process_endgames()清理关闭的句柄。
#define UV_LOOP_ONCE(loop, poll) ...
#define UV_LOOP(loop, poll) ...

while ((loop)->refs > 0) {
  UV_LOOP_ONCE(loop, poll)
}

示例代码:TCP模块

下面是一个TCP模块的示例,展示了如何在Nodejs中通过Libuv和V8的结合实现异步操作。

class TCPWrap : public HandleWrap {
public:
  TCPWrap(Handle<Object> object, uv_tcp_t* handle) : HandleWrap(object, (uv_handle_t*)handle) {}

  Handle<Value> Listen(const Arguments& args) {
    HandleScope scope;

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

    int r = uv_listen((uv_stream_t*)&handle_, backlog, OnConnection);

    if (r) SetErrno(uv_last_error(uv_default_loop()));

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

private:
  static void OnConnection(uv_stream_t* handle, int status) {
    HandleScope scope;

    TCPWrap* wrap = static_cast<TCPWrap*>(handle->data);

    Handle<Value> argv[1];

    if (status == 0) {
      Local<Object> client_obj = Instantiate();
      TCPWrap* client_wrap = static_cast<TCPWrap*>(client_obj->GetPointerFromInternalField(0));
      if (uv_accept(handle, (uv_stream_t*)&client_wrap->handle_)) return;
      argv[0] = client_obj;
    } else {
      SetErrno(uv_last_error(uv_default_loop()));
      argv[0] = v8::Null();
    }

    MakeCallback(wrap->object_, "onconnection", 1, argv);
  }
};

通过以上示例代码,可以看到Nodejs如何通过C++和JavaScript的结合,利用Libuv的异步机制实现高效的网络编程。

回到顶部