查看原文
其他

Node.js 如何利用 Libuv 实现事件循环和异步

标子 Nodejs技术栈 2022-07-06

本文主要从以下四个方面进行讲解:

  • 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。另外还实现了定时器,对进程、线程等使用进行了封装。

  1. 新建一个 uv_loop_t* loop。loop 中保存了各个阶段对应的数据结构。
  2. 执行 uv_run 函数进入死循环。
  3. 用户(nodejs)操作 loop 里的结构,注册事件和回调。
  4. libuv 在每一轮循环里处理各个阶段。

2.2 Libuv 的各个阶段(phase)

  1. 定时器(setTimeout)
  2. pending callback
  3. idle(自定义)
  4. prepare(自定义)
  5. poll i/o (网络和文件 IO)
  6. check(setImmediate)
  7. close callback (关闭一个 handle)

2.3 libuv 的实现

  • 最小堆(定时器)
  • 链表(check、idle 等)
  • 线程池(文件 io)
  • 操作系统提供的事件驱动模块(网络 io)

3. Nodejs 的启动流程

  1. 注册内置 c++ 模块(通过 process.binding 函数使用内置 c++ 模块)。
  2. 新建 process 的 c++ 对象,设置一系列属性(binding),然后挂载到全局。
  3. 执行 bootstrap_node.js,初始化和挂载 nextTick,setTimeout 等函数,然后加载用户 js,编译执行。
  4. 调用 libuv 开始事件循环。

3.1 注册内置c++模块

  1. 每个 c++ 模块由一个 node_module 结构体管理。
  2. 用链表的方式把各个模块的 node_module 连接起来。
  3. 运行时,js 通过 process.binding 函数从链表中找到对应的模块,从而使用 c++ 模块功能。

3.2 process 对象的生成和作用

  1. 新建一个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);
  1. nodejs 如何访问 global?

通过给 global 增加一个 global 属性

Local<Object> global = env->context()->Global();
global->Set("global", global);
  1. nodejs 如何设置 process 对象?

编译 node_bootstrap.js 成 c++ 代码,执行时传入 c++ 的 process 对象,执行 global.process = process; 从 js 层面来看,是多了一个全局变量 process

  1. 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

  1. 挂载全局变量 setTimeout,Buffer 等,给 process 对象挂载 nexTick 函数等初始化工作。

  2. 执行用户 js

3.4 调用 Libuv 开始事件循环。

4. Nodejs 如何利用 libuv 实现异步和事件循环?

如何生成任务给事件循环系统消费?

  1. setTimeout
  2. setImmediate
  3. 文件io
  4. 网络io

4.1 setTimeout 的实现

setTimeout 对应的数据结构
  1. 用户调用 setTimeout,设置时间是 s 秒
  2. 找到 s 对应的链表 L。
  3. 如果 L 不存在则新建一个,并在 libuv 最小堆里新增一个超时节点。
  4. 往链表 L 头部插入一个 Timeout 节点。返回。(最早超时在链表末尾)
  5. uv_run 执行 uv__run_timers 判断是否有超时节点。
  6. 从后往前遍历链表 L,如果当前节点没有超时则全部没有超时,设置新的超时时间,否则执行超时回调。

4.2 setImmediate 实现

  1. nodejs 启动的时候注册了 check 阶段的一个 c++ 层回调是 CheckImmediate,该函数再执行 js 回调 processImmediate
  2. 用户调用 setImmediate,生成一个节点插到双向链表。返回。
  3. uv_run 在 check 阶段。执行回调。setImmediate 和 setTimeout 的关系这两个其实没什么关系,对应的阶段也不一样。

4.3 文件 io

为啥用线程池实现文件操作的异步?

因为文件的异步操作在各操作系统中兼容性不好。libuv 线程池默认打开 4 个,最多打开 128个 线程。所有线程共享一个任务队列,当有任务的时候,添加到任务队列,线程的工作函数在死循环里不断处理队列里的任务。Libuv 初始化的时候,注册了一个异步的 io 观察者 A,用于子线程和主线程间通信的。io 观察者 A设置了一个管道文件描述符和回调。子线程完成任务后设置该任务的标记位,然后通过管道通知主线程,主线程在 uv_run 的 poll io 阶段会执行观察者 A 的回调,观察者的回调会判断每个异步任务的状态。然后执行用户的回调。

文件操作的过程

  1. 打开一个文件,新建一个 c++ FSReqWrap 对象。设置用户回调。调用 FSReqWrap 对象的 Open,接着调用 libuv 层 uv_fs_open。uv_fs_open。Libuv 生成一个任务放到线程池的任务队列,返回 Nodejs。Nodejs 可以继续做其他事情。

  2. 线程池处理该任务,线程会阻塞直到任务完成。比如读写文件,dns 查询,然后设置任务的完成标记,可以通过管道写端通知主线程。主线程执行 c++ 层回调,再执行 js 层回调。

4.4 网络 io

网络 io 的实现方案。利用操作系统提供的事件驱动模块。

var http = require('http');
http.createServer(function (request, response) { response.end('Hello World\n'); }).listen(9297);
本文经作者 “标子” 授权转载,原文地址 https://cloud.tencent.com/developer/article/1453103
往期精彩回顾
JavaScript 浮点数之迷:大数危机
你需要了解的有关 Node.js 的所有信息
ServerLess, Nodejs, MongoDB Atlas 构建 REST API
Node.js 是什么?我为什么选择它?
分享 10 道 Nodejs 进程相关面试题
Node.js进阶之进程与线程
Node.js 中的缓冲区(Buffer)究竟是什么?
Node.js 内存管理和 V8 垃圾回收机制
浅谈 Node.js 模块机制及常见面试问题解答
不容错过的 Node.js 项目架构


在看点这里


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存