• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Nodejs组成和原理笔记 01 组成和原理

武飞扬头像
孟祥_成都
帮助1

前言

一直想了解nodejs的基本原理,但苦于首先确实c/c 不是很了解,其次本身主攻的也是前端,没有时间和精力去精进这块,长期处于一个主要nodejs模块原理都懂,但更进一步十分困难的底部, 后面发现国内一位nodejs大神theanarkh已经写了非常详细的Nodejs底层原理的电子书.

惊喜之余,发现有些描述自己可能不是特别理解,再多方查阅资料的基础上,再次把其语言白话一些,案例从c/c 代码被我映射为javascript代码,作为笔记,跟大家一起学习。

Nodejs组成和原理

Node.js简单架构

学新通技术网

将Node.js分成三层

  • 首先最上层是就是我们写代码require的模块,也就是标准库,如Http, Fs 模块。

  • 中间层 node bindings 主要是使 js 和 C/C 进行通信

    • Node.js的一个重要特性是它的扩展能力,可以通过C 编写插件来扩展Node.js的功能。
    • Node.js的Node API提供了一组C 类和函数,可以让开发者使用C 来编写Node.js扩展。这些扩展可以被称为“Node binding”,也被称为“native addons”。
    • 当然它也可以调用底层,比如V8和libuv提供的接口,从而访问v8和libuv
  • 最下面这一层是支撑 nodejs 运行的关键,主要由 v8libuvc-ares 等模块组成,向上一层提供 api 服务

    • V8是javacript的runtime,解析javascript代码的
    • libuv主要是提供跨平台的I/O能力的
    • http_parserL、zlib 等:提供包括 http 解析、数据压缩等能力

V8和libuv如何通信

学新通技术网

上面可以看到,我们js代码首先是被v8引擎解析的,然后才走到nodejs bindings层,也就是到这里才真正的开始调用底层的C 和C提供能给js的功能。

关键点是简单介绍一下libuv

在 Node.js 中,libuv 是一个跨平台的异步 I/O 库,负责实现事件循环机制。事件循环是 Node.js 的核心,它负责管理所有的异步操作,并且在它们完成时通知应用程序。事件循环可以理解为一个无限循环,每次循环处理一个事件。

libuv 提供了对底层操作系统的访问,包括文件 I/O、网络 I/O、定时器、进程管理等等,它能够利用操作系统提供的异步 I/O 机制来提高 I/O 操作的效率,从而使 Node.js 能够高效地处理大量并发请求。

libuv如何跟其他模块交互

也就是说libuv调用了http,zlib等底层模块去处理请求,libuv相当于一个调度器,调度器模块很常见,比如k8s的调度器(比如调度某个请求到底去哪个pod),rxjs的调度器(对于某个操作符改变其默认调度行为),react的调度器(实现比如任务优先级处理,中断和恢复fiber生成处理等)

例如,在处理 HTTP 请求时,Node.js 应用程序会使用 http 模块来创建一个 HTTP 服务器,当收到客户端请求时,http 模块会调用 libuv 实现的网络 I/O 接口来处理网络请求,然后将请求传递给应用程序。

如何在V8新增一个自定义的功能?

    // C  里定义  
    Handle<FunctionTemplate> Test = FunctionTemplate::New(cb);
    global->Set(String::New(“Test"), Test);  
    // JS里使用    
    const test = new Test();

我们解释下上面的代码:

这段代码在 V8 中自定义了一个功能,供 JavaScript 调用,通过如下步骤实现:

  1. 定义:在 C 代码中,使用 FunctionTemplate::New(cb) 定义一个 FunctionTemplate 类型的变量 Test,其中 cb 是回调函数,它将在 JavaScript 调用该函数时被执行。
  2. 关联:使用 global->Set(String::New("Test"), Test) 将变量 Test 关联到键为 "Test" 的值。
  3. 使用:在 JavaScript 代码中,通过使用语句 const test = new Test() 创建一个新的对象,该对象是由定义的 FunctionTemplate 创建的,因此它具有所有 FunctionTemplate 所指定的功能。

通过以上步骤,可以在 JavaScript 代码中访问在 C 代码中定义的函数并调用它,从而与 C 代码进行交互。 FunctionTemplate::New(cb)相当于常见了一个Test类,用的FunctionTemplate模板,具体实现我们不得而知。

你看了之后应该会很懵逼,没有关系,我也不会c ,遇到不会的语法,查一下就行了, 如果你不懂,你可能没有明白为什么c 还能实现javascript的语法功能?这两个不是不同的语言吗?

我简单说明一下

在V8引擎里,javascript跟c 的关系

首先,你要明白,javascript最终传给编译器的就是字符串,这个字符串你解析之后,如果能实现javascript这门语言要求的语法规则、词法规则等等,那么我就认为你就实现了javascript的运行时(runtime)。

假如我们用go,我们rust等语言,我们能解析这段字符串,并且把这段字符串表达出的整个运行过程和结果满足javascript这门语言所要求的的一切,那么我们也就是实现了javascript的运行时

V8引擎是用C 编写的,它将JavaScript代码解析为抽象语法树(AST),然后通过执行相应的C 代码来实现JavaScript代码的运行。因此,JavaScript代码本质上是在V8引擎中通过C 代码实现的。

所以说你不要奇怪,为啥c 跟javascript明明是不同的语言为什么js还能调用c 的代码。

简单总结为,在V8引擎中,要新增一个自定义的功能,通常需要编写一些C 代码来实现。C 代码可以使用V8提供的API来访问JavaScript对象、函数、属性等。同时,也可以使用C 来访问操作系统的底层API,从而实现一些高级的功能。

将C 代码编译为动态链接库:一旦编写好了C 代码,需要将其编译为动态链接库,以便在JavaScript中调用。

一旦加载了动态链接库,就可以在JavaScript中调用C 函数了。通常需要使用Node.js的N-API或nan库来编写绑定代码,将C 函数绑定到JavaScript对象上,并提供相应的接口。

libuv中的事件循环详解

学新通技术网

我们结合源码一起梳理一下

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  int timeout;
  int r;
  int ran_pending;

  r = uv__loop_alive(loop);
  if (!r)
    uv__update_time(loop);

  /* 这是一个while循环 */
  while (r != 0 && loop->stop_flag == 0) {
    
    /* 更新时间并开始倒计时, 对应上图的update loop time阶段*/
    uv__update_time(loop);
    /* 对应上面的run du timers阶段 */
    uv__run_timers(loop);
    /* 处理挂起的handle, 对应上面的 call pending callbacks阶段 */
    ran_pending = uv__run_pending(loop);
    /* 运行idle handle, 对应上面的 run idle callbacks阶段 */
    uv__run_idle(loop);
    /* 运行prepare handle, 对应上面 run prepare handles阶段 */
    uv__run_prepare(loop);

    timeout = 0;
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
      timeout = uv_backend_timeout(loop);
    
    /* 计算要阻塞的时间,开始阻塞, 对应上面的 poll阶段 */
    uv__io_poll(loop, timeout);

    /* 程序执行到这里表示被唤醒了,被唤醒的原因可能是I/O可读可写、或者超时了,检查handle是否可以操作, 对应 check阶段 */
    uv__run_check(loop);
    /* 看看是否有close的handle, 对应close阶段 */
    uv__run_closing_handles(loop);

    /* 单次模式 */
    if (mode == UV_RUN_ONCE) {
      /* UV_RUN_ONCE implies forward progress: at least one callback must have
       * been invoked when it returns. uv__io_poll() can return without doing
       * I/O (meaning: no callbacks) when its timeout expires - which means we
       * have pending timers that satisfy the forward progress constraint.
       *
       * UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from
       * the check.
       */
      uv__update_time(loop);
      uv__run_timers(loop);
    }

    /* handle保活处理 */
    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }

  /* The if statement lets gcc compile it to a conditional store. Avoids
   * dirtying a cache line.
   */
  if (loop->stop_flag != 0)
    loop->stop_flag = 0;

  return r;
}

Update loop time

  • 描述
    • libuv获取当前的时间,并在本轮事件循环的中使用这个时间(除非在必要的时候更新这个时间)
  • 优点
    • 目的是获取时间是一个系统调用的操作,减少过多系统调用的性能损耗
  • 缺点
    • 时间不那么精确

Loop alive

  • 描述
    • loop alive 是这一阶段(event loop)保持活动状态的一个重要部分,当为false的时候退出这个事件循环。

run due timers

  • 描述

    • run due timers 是指执行到期的定时器回调函数的过程。定时器是Node.js中常用的异步机制,可以让您在指定的时间间隔后执行代码
  • 优点:

    • 灵活:定时器提供了多种不同的选项,如间隔时间、重复次数、以及是否在其他事件处理完成后立即执行等,因此非常灵活。
  • 缺点:

    • 精度问题:由于Node.js的事件循环是单线程的,因此定时器的精度可能受到影响。如果事件队列中有大量的任务,定时器的回调函数可能会延迟较长的时间。
    • 注意事项:如果没有正确处理定时器,可能会造成内存泄漏。因此,在使用定时器时需要仔细考虑其使用情况,并确保能够及时清除不再需要的定时器。

Call pending callbacks

对应调用上面的uv__run_pending()函数,主要是用来执行之前加入到pending queue里的函数。

在 Node.js 中,pending queue(待处理队列)是一个存储待处理事件回调函数的队列。当某些事件的回调函数被添加到事件队列中,但是由于当前事件循环阶段正在执行,导致这些回调函数不能立即执行时,它们就会被添加到 pending queue 中等待处理。

Node.js 中有很多异步 API,例如 setTimeout()、setInterval()、setImmediate()、process.nextTick() 等,它们都可以用来将回调函数添加到事件队列中。如果在当前事件循环阶段结束之前,这些回调函数无法执行,它们就会被添加到 pending queue 中等待下一个事件循环执行。这些被添加到 pending queue 中的回调函数,通常被称为 pending callbacks(待处理回调函数)。

为什么这里会把setTimeout加入进来,不是setTimeout的执行在timers阶段吗?

如果在执行 timers 阶段时,发现有一个计时器的时间已经到了,那么对应的回调函数就会被立即执行,而不会被添加到 pending queue 中等待下一个事件循环执行。但如果在执行 timers 阶段时,所有的计时器事件都还没有到期,那么 setTimeout() 函数添加的回调函数就会被添加到事件队列的末尾,等待下一次事件循环执行

Run idle handles

  • 描述
    • "Run idle handles" 指的是执行已经注册在 Node.js 系统中的任何 "空闲" 任务的过程。这些任务旨在在系统空闲时执行,并且没有其他要做的工作。

大家肯定会有疑问,"空闲"任务是什么,比如说:

  • 清理未使用的资源:释放内存,关闭连接或释放任何不再需要的资源。
  • 执行后台计算:需要大量处理能力的任务,但可以在后台执行,不影响系统其他部分的性能。
  • 执行例行维护:如检查磁盘空间或其他例行系统检查的任务。

所以这个基本跟我们使用者没啥关系。

  • 优点
    • 它们允许后台任务在不影响系统其他部分性能的情况下有效地执行。这对于诸如清理资源、检查更新或执行任何其他低优先级工作的任务是有用的
  • 缺点
    • 使用空闲句柄也有一些潜在的缺点。如果不小心实现,空闲任务可以消耗大量的 CPU 和内存,从而影响系统其他部分的性能

Run prepare handles

在Idle阶段完成之后,事件循环会进入到Poll阶段。在Poll阶段中,Node.js会等待I/O事件发生。在等待的过程中,如果prepare阶段添加了回调函数,那么这些回调函数会被执行。执行完prepare回调函数之后,事件循环会检查是否有I/O事件发生。如果有,就会执行相应的I/O回调函数。如果没有,就会进入到Check阶段。

Poll for I/O

描述

  • 该阶段,如果事件队列中有回调函数,那么会立即执行它们,直到队列为空或者执行了一定数量的回调函数。
  • 如果事件队列中没有回调函数,那么 Node.js 将会等待新的事件加入到事件队列中。在这个阶段,Node.js 会暂停 JavaScript 执行环境,等待 I/O 事件完成。例如,如果应用程序正在等待从网络或磁盘读取数据,那么 Node.js 将会等待这些操作完成,并将读取的数据传递给应用程序。
  • 当 I/O 事件完成时,Node.js 会将事件的回调函数添加到事件队列中,等待下一次 Poll 阶段执行。

这个阶段跟Call pending callbacks有什么区别呢?

  • 在执行 Poll 阶段时,Node.js 会等待 I/O 事件完成,并执行相应的回调函数。如果事件队列中没有回调函数等待执行,那么 Node.js 将会在这个阶段等待新的事件加入到事件队列中。因此,Poll 阶段通常是用来处理 I/O 事件的,例如网络请求、文件读取等。

  • 而在执行 Call pending callbacks 阶段时,Node.js 会执行之前被延迟执行的回调函数,这些回调函数可能来自其他阶段,例如 timers、I/O callbacks 或者其他自定义阶段。这些回调函数通常是在当前事件循环中被添加到事件队列中,但是因为前面的阶段还没有执行完毕,导致它们没有得到及时执行。因此,Call pending callbacks 阶段通常是用来处理之前被延迟执行的回调函数。

Run check handles

  • 描述
    • 它的作用是检查是否有需要立即执行的任务,例如,setImmediate() 回调函数。
    • 有些任务是需要加入到check队列的,这些任务就是在这个阶段

Call close callbacks

  • 描述

    • 在 Node.js 中,Call close callbacks 阶段是事件循环的最后一个阶段,它的作用是处理一些与关闭事件相关的回调函数。这些回调函数通常与网络连接或文件描述符相关,包括 close、error、end 等事件的回调函数。

    • 在执行这个阶段时,Node.js 会按照添加顺序,依次调用所有添加到 close callback 队列中的回调函数。这个阶段的执行时机是在每个事件循环的最后,当事件队列中的所有事件处理完毕之后才会执行。因此,如果在事件循环过程中有某些资源(如网络连接或文件描述符)被关闭了,它们对应的 close callback 回调函数会被添加到 close callback 队列中,在下一个事件循环的 Call close callbacks 阶段得到执行。

    • 需要注意的是,Node.js 并不保证 close callback 队列中的回调函数一定会在当前事件循环中被执行。如果在执行该阶段时,还有一些其它阶段的回调函数没有得到执行,那么 close callback 回调函数就会被推迟到下一个事件循环的 Call close callbacks 阶段再执行。

下面我能通过一个例子来了解libuv的基本原理。

// libuv库导出
import { uv_idle_stop, uv_idle_t } from 'uv'
// 定义了一个全局变量 counter,用于记录事件循环的循环次数。
let counter = 0;

// 该函数会在事件循环的 idle 阶段被调用
function wait_for_a_while() {
    counter  ;
    if(counter >= 10e6) {
        // 停止 idle 阶段的事件循环,即结束 wait_for_a_while 函数的执行
         uv_idle_stop(handle); 
    }
}

function main(){
    var  idler = new uv_idle_t;
     // 获取事件循环的核心结构体。并初始化一个idle  
        uv_idle_init(uv_default_loop(), idler);  
        // 往事件循环的idle阶段插入一个任务  
        uv_idle_start(idler, wait_for_a_while);  
        // 启动事件循环  
        uv_run(uv_default_loop(), UV_RUN_DEFAULT);  
        // 销毁libuv的相关数据  
        uv_loop_close(uv_default_loop()); 
}

然后执行main(), 事件循环就启动了。

在原文中,作者把每个函数比如uv_idle_init列出来源码,但是我觉得只要了解其主要作用就行了,这部分源码没有什么必要细究。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanggiahh
系列文章
更多 icon
同类精品
更多 icon
继续加载