Node使用的事件驱动编程:
事件驱动模型主要包含3个对象:事件源、事件和事件处理程序。
- 事件源:产生事件的地方(html元素)
- 事件:点击/鼠标操作/键盘操作等等
- 事件对象:当某个事件发生时,可能会产生一个事件对象,该时间对象会封装好该事件的信息,传递给事件处理程序
- 事件处理程序:响应用户事件的代码
在I/O操作完之后会有一个回调事件,这个事件会放在一个事件处理队列里头,在进程启动时node会创建一个event loop的循环,它的每一次轮询都会去查看是否有事件需要处理,是否有事件关联的回调函数需要处理,如果有就处理,然后加入下一个轮询,如果没有就退出进程,这就是所谓的“事件驱动”。这也从Node的角度解释了什么是”事件驱动”。
NODE的架构:
模块角度
分为了三大块:
- your code 为编辑代码
- node.js 核心
- Host environment 为宿主环境(提供各种服务,如文件管理,多线程,多进程,IO etc)
其中重点 很显然是nodejs
v8 engine | 主要有两个作用 1.虚拟机的功能,执行js代码(自己的代码,第三方的代码和native modules的代码)。 2.提供C++函数接口,为nodejs提供v8初始化,创建context,scope等。 |
libuv | 它是基于事件驱动的异步IO模型库,我们的js代码发出请求,最终由libuv完成, 而我们所设置的回调函数同样是在libuv触发。 说白了, libuv负责异步调用工作(event loop) |
builtin modules | c++写成的模块 包含了crypto,zlib, file stream etc 基础功能。 (v8提供了函数接口,libuv提供异步IO模型库,以及一些nodejs函数,为builtin modules提供服务) |
native modules | 它是由js写成的模块 同时这些模块又依赖builtin modules来获取相应的服务支持 |
如果把nodejs看做一个黑匣子,它暴露给开发者的接口则是native modules
当我们发起请求时,请求自上而下,穿越native modules,通过builtin modules将请求传送至v8,libuv和其他辅助服务
请求结束,则从下回溯至上,最终调用我们的回调函数。
当我们执行node xxx.js的时候,node会先做一些v8初试化,libuv启动的工作,然后交由v8来执行native modules以及我们的js代码。
也可以说:
Nodejs就是由由v8 engine,libuv和内置模块(c++写的和js写的两种)组成的,可以将v8 engine和 libuv看成一个库,两者是以源码的方式直接编译执行node中去的。接下来再详细的分析一下这三个部分:
V8
#include "include/v8.h" #include "include/libplatform/libplatform.h" using namespace v8; int main(int argc, char* argv[]) { // V8初试化. V8::InitializeICU(); Platform* platform = platform::CreateDefaultPlatform(); V8::InitializePlatform(platform); V8::Initialize(); // 创建isolate. Isolate* isolate = Isolate::New(); { Isolate::Scope isolate_scope(isolate); // 创建HandleScope. HandleScope handle_scope(isolate); // 创建context 环境. Local<Context> context = Context::New(isolate); // 引入环境. Context::Scope context_scope(context); // 创建字符串. Local<String> source = String::NewFromUtf8(isolate, "'Hello' + ', World!'"); // 编译字符串. Local<Script> script = Script::Compile(source); // Run,并且获取返回值. Local<Value> result = script->Run(); // 转换为utf8,并且打印出来. String::Utf8Value utf8(result); printf("%s\n", *utf8); } // 关掉v8. isolate->Dispose(); V8::Dispose(); V8::ShutdownPlatform(); delete platform; return 0; }
isolate 代表一个V8实例,各个isolate是独立的(隔绝的),isolate中的obj无法再另一个isolate中被使用。 |
context 代表执行js代码的虚拟机,在这个虚拟机中,集成了一些功能,例如math,json,date(),RegExp()等。一个isolate里可以同时存在多个context,这些context可以自由切换。如下图所示。 |
在V8 engine中,通过handle访问存在与heap上的js obj,如local,persistent,eternal,在V8中,有一个handle stack用于管理这些handles,如下图所示。 |
libuv
libuv通过epoll,kqueue,event ports和IOCP来实现异步network IO。file,dns的操作则依赖于thread pool 来实现,正如上图所示。
上图中的代码分为两个部分:
1. server.listen()是用来创建TCP server时,通常放在最后一步执行的代码。主要指定服务器工作的端口以及回调函数。
2. fs.open()是用异步的方式打开一个文件。
右半部分又分为两个部分:
1. 主线程:主线程也是node启动时执行的线程。node启动时,会完成一系列的初始化动作,启动V8 engine,进入下一个循环。
2. 线程池:线程池的数量可以通过环境变量UV_THREADPOOL_SIZE配置,最大不超过128个,默认为4个。
2. 线程池:线程池的数量可以通过环境变量UV_THREADPOOL_SIZE配置,最大不超过128个,默认为4个。
libuv的重点是IO和event loop,loop负责调度callback,运行idle function,执行轮询等操作。
模块
Nodejs本身也做了很多的工作,它提供了js模块,c++模块等核心模块供开发者使用。
Nodejs 将这些核心js模块命名为native module,c++模块命名为builtin module
当 node test.js 执行时,node_main.cc首先被调用,这一步初始化了V8 engine和libuv执行环境。
uv_run(env->event_loop,UV_RUN_ONCE)用于启动libuv event loop,而我们的js代码则会被传递到creatEnvironment(),最后交由V8处理。
在js中,类似基于prototype继承的方式来实现的,通过V8提供的c++ API来完成这些操作。
函数角度:
以建立http server为例
v8执行js代码 server.listen()时,会通过一些基础服务到TCPWrap::listen(),TCPWrap是nodejs的內建模块,其通过libuv的api uv_listen()的方式,由libuv来完成异步调用。 |
图中1,2,3,4,5步骤标明了调用和返回的路径 这几步都是同步调用,始于server.listen() 停于第5步 留下callback TCPWrap::OnConnection()等着所需要的数据准备好后被调用。 |
libuv在得到所需要的请求后,发起一个逆向的callback,始于libuv中的event loop,终于server中设置的callback函数 而这时,V8可能已经完成了多条任务 |
分析一个http serve的例子:
var http=require("http"); http.createServer(function(req,res){ res.writeHead(200,{ "content-type":"text/plain" }); res.write("hello nodejs"); res.end(); }).listen(3000);结合下面这张图片分析:
从上至下的看:
第一部分:创建一个server实例,这部分还是集中在js部分,其中http.js,http_server.js以及net.js为Nodejs库中的代码,这部分比较重要的细节是net.js,集成了若干API,这些API会为之后提供服务。
第二部分:从调用this.listen()开始。在这一部分,直接调用net.js提供的各种api,reateTCP()通过 process.binding('tcp_wrap').TCP 进入到Nodejs C/C++部分,也就是 TCPWrap部分。在回来net.js的倒数第二步,handle.open(fd),这部分代码最终调用到C/C++部分的TCPWrap::Open()。
重点分析一下TCPWrap这个模块:
在执行函数createTCP()将会调用process.binding('tcp_wrap'),在tcp_wrap模块真正导出的是TCP函数
流程如下图所示: