Node.js 如何利用 Libuv 实现事件循环和异步
本文主要从以下四个方面进行讲解:
Nodejs 是什么? Libuv 的工作原理 Nodejs 的工作原理 Nodejs 如何使用 Libuv 实现事件循环和异步
正文从下面开始~
1. Nodejs 是什么?
Nodejs 是对 js 功能的拓展。提供了网络、文件、dns 解析、进程线程等功能。
1.1 Nodejs 是如何拓展 js 功能的?
利用 v8 提供的接口。
1.2 如何在 v8 新建一个自定义的功能?
C++ 里定义
Handle<FunctionTemplate> Test = FunctionTemplate::New(cb);
global->Set(String::New(“Test"), Test);
Nodejs 里使用
var test = new Test();
1.3 Nodejs 是如何实现拓展的
但 Nodejs 不是给每个功能拓展一个对象,而是拓展一个 process 对象,再通过 process.binding 拓展 js 功能。Nodejs 定义了一个 js 对象 process,映射到一个 c++ 对象 process,底层维护了一个 c++ 模块的链表,js 通过调用 js 层的 process.binding,访问到 c++ 的 process 对象,从而访问 c++ 模块(类似访问 js 的 Object、Date 等)。
2. Libuv 的工作原理
2.1 Libuv 是什么?为什么 Nodejs 需要它?
libuv 是一个跨平台异步 IO 库。因为 Nodejs 是单线程的,作为服务器,他涉及到 IO,而 IO 是会阻塞的,从而影响性能。所以 Nodejs 把 IO 操作交给 libuv,保证主线程可以继续处理其他事情。Libuv做了什么?Libuv 主要是,利用系统提供的事件驱动模块解决网络异步 IO,利用线程池解决文件 IO。另外还实现了定时器,对进程、线程等使用进行了封装。
新建一个 uv_loop_t* loop。loop 中保存了各个阶段对应的数据结构。 执行 uv_run 函数进入死循环。 用户(nodejs)操作 loop 里的结构,注册事件和回调。 libuv 在每一轮循环里处理各个阶段。
2.2 Libuv 的各个阶段(phase)
定时器(setTimeout) pending callback idle(自定义) prepare(自定义) poll i/o (网络和文件 IO) check(setImmediate) close callback (关闭一个 handle)
2.3 libuv 的实现
最小堆(定时器) 链表(check、idle 等) 线程池(文件 io) 操作系统提供的事件驱动模块(网络 io)
3. Nodejs 的启动流程
注册内置 c++ 模块(通过 process.binding 函数使用内置 c++ 模块)。 新建 process 的 c++ 对象,设置一系列属性(binding),然后挂载到全局。 执行 bootstrap_node.js,初始化和挂载 nextTick,setTimeout 等函数,然后加载用户 js,编译执行。 调用 libuv 开始事件循环。
3.1 注册内置c++模块
每个 c++ 模块由一个 node_module 结构体管理。 用链表的方式把各个模块的 node_module 连接起来。 运行时,js 通过 process.binding 函数从链表中找到对应的模块,从而使用 c++ 模块功能。
3.2 process 对象的生成和作用
新建一个c++的process对象
// 利用v8新建一个函数
auto process_template = FunctionTemplate::New(isolate());
// 设置函数名
process_template->SetClassName(FIXED_ONE_BYTE_STRING(isolate(), "process"));
// 利用函数new一个对象 auto process_object = process_template->GetFunction()->NewInstance(context()).ToLocalChecked();
// 设置env的一个属性,类型是Object,val是process_object
set_process_object(process_object);
nodejs 如何访问 global?
通过给 global 增加一个 global 属性
Local<Object> global = env->context()->Global();
global->Set("global", global);
nodejs 如何设置 process 对象?
编译 node_bootstrap.js 成 c++ 代码,执行时传入 c++ 的 process 对象,执行 global.process = process; 从 js 层面来看,是多了一个全局变量 process
process 的 Binding 函数
process.Binding 加载 c++ 模块。实现 js 使用 c++ 模块功能。
const { TCP } = process.binding('tcp_wrap');
const tcp = new TCP();
tcp.listen();
js 里通过 process.binding 加载一个 c++ 模块的时候,这段 js 在编译后执行,首先访问 js 层的 process 对象,v8 知道 js 的 process 对象对应是 c++ 的 process 对象,再通过底层的 Binding,就可以使用 c++ 模块的功能了。
3.3 执行 bootstrap_node.js
挂载全局变量 setTimeout,Buffer 等,给 process 对象挂载 nexTick 函数等初始化工作。
执行用户 js
3.4 调用 Libuv 开始事件循环。
4. Nodejs 如何利用 libuv 实现异步和事件循环?
如何生成任务给事件循环系统消费?
setTimeout setImmediate 文件io 网络io
4.1 setTimeout 的实现
用户调用 setTimeout,设置时间是 s 秒 找到 s 对应的链表 L。 如果 L 不存在则新建一个,并在 libuv 最小堆里新增一个超时节点。 往链表 L 头部插入一个 Timeout 节点。返回。(最早超时在链表末尾) uv_run 执行 uv__run_timers 判断是否有超时节点。 从后往前遍历链表 L,如果当前节点没有超时则全部没有超时,设置新的超时时间,否则执行超时回调。
4.2 setImmediate 实现
nodejs 启动的时候注册了 check 阶段的一个 c++ 层回调是 CheckImmediate,该函数再执行 js 回调 processImmediate 用户调用 setImmediate,生成一个节点插到双向链表。返回。 uv_run 在 check 阶段。执行回调。setImmediate 和 setTimeout 的关系这两个其实没什么关系,对应的阶段也不一样。
4.3 文件 io
为啥用线程池实现文件操作的异步?
因为文件的异步操作在各操作系统中兼容性不好。libuv 线程池默认打开 4 个,最多打开 128个 线程。所有线程共享一个任务队列,当有任务的时候,添加到任务队列,线程的工作函数在死循环里不断处理队列里的任务。Libuv 初始化的时候,注册了一个异步的 io 观察者 A,用于子线程和主线程间通信的。io 观察者 A设置了一个管道文件描述符和回调。子线程完成任务后设置该任务的标记位,然后通过管道通知主线程,主线程在 uv_run 的 poll io 阶段会执行观察者 A 的回调,观察者的回调会判断每个异步任务的状态。然后执行用户的回调。
文件操作的过程
打开一个文件,新建一个 c++ FSReqWrap 对象。设置用户回调。调用 FSReqWrap 对象的 Open,接着调用 libuv 层 uv_fs_open。uv_fs_open。Libuv 生成一个任务放到线程池的任务队列,返回 Nodejs。Nodejs 可以继续做其他事情。
线程池处理该任务,线程会阻塞直到任务完成。比如读写文件,dns 查询,然后设置任务的完成标记,可以通过管道写端通知主线程。主线程执行 c++ 层回调,再执行 js 层回调。
4.4 网络 io
网络 io 的实现方案。利用操作系统提供的事件驱动模块。
var http = require('http');
http.createServer(function (request, response) { response.end('Hello World\n'); }).listen(9297);