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->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.
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.h
和uv-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
mark!!
mark
mark!<br/><br/>来自mov1er的cnode!
mark
Nodejs代码初探
入门与背景
我对Nodejs产生兴趣的原因在于其脚本和异步特性。Nodejs不仅限于互联网应用,还可以在各种行业中发挥作用。V8引擎和Libuv的高性能和稳定性也值得深入学习。
V8引擎与javascript
V8引擎在执行JavaScript代码时会动态地识别类型并编译成机器码,同时进行高效的垃圾回收。这使得V8引擎非常高效,不用担心性能问题。熟悉V8的API有助于更好地理解Nodejs的内部机制。
Nodejs启动流程
Nodejs的启动过程如下:
- 初始化V8引擎:调用
V8::Initialize()
来初始化V8。 - 创建process对象:调用
SetupProcessObject()
来创建process对象。 - 加载引导脚本:调用
Load()
来加载执行引导脚本(通常是node.js
)。 - 开始异步事件循环:调用
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的异步机制实现高效的网络编程。