node(14.2)

本章介绍node api 中的主要模块,简析每个模块的功能以及定位,大致分为常用模块和进阶模块

常用API

常用模块主要包括 基础模块和常用的工具模块

模块

node 遵循commonjs规范,每个文件被视为一个独立模块,通过exports导出模块,通过require引入模块

  • 模块分类

    node的模块主要分为四种:

    • builtin module:c/c++ 模块,如tcp_wrap 等,一般提供给native module调用
    • constants module:定义常量的模块,用力啊导出openssl,文件访问权限等常量的定义,如O_RDONLY
    • native module:原生模块,node 中的js模块,如http,fs,buffer
    • third-party module: 除了以上模块之外的,统称为第三方模块,如koa,egg等
  • Module 主要结构

    apimodule

    • builtinModules: 存放node提供的原生模块名称
    • _cache:缓存模块信息
    • wrapper:模块装饰器
  • 模块封装器

    可以从上面的Module结构中看到wrapper, 内容为:'(function (exports, require, module, __filename, __dirname) { ', '\n});',wrapper就是node的模块封装器结构,它保持了顶层的变量(用 var、 const 或 let 定义)作用在模块范围内,而不是全局对象,并且可以通过exports和module导出一些对象

    • exports:exports 是对于module.exports的简写,在模块执行之前赋值给 module.exports
    • module: module是对当前模块对象的引用,包含模块的路径,子模块,父模块等信息
    • require: 用于引入模块、 JSON、或本地文件,被引入的模块将被缓存require.cache
    • __filename: 当前模块的文件名。是当前的模块文件的绝对路径
    • __dirname: 当前模块的目录名
  • 模块查找

    模块查找的规范就是require的实现 详见伪代码,总结如下

    • 优先从缓存中获取已加载到内存中的模块
    • 优先加载核心模块,也就是node lib目录下的模块
    • 模块首次加载后会被缓存到上面提到的require.cache中,已经被缓存的模块,不会被二次加载
    • 对于文件模块,一般以’/’,’./’,’…/'开头来表示文件,优先以确切的文件名查找模块,如果没有找到,则会为文件补偿后缀.js、.json、.node再查找
    • 目录作为模块:在模块查找的过程中,如果没有找到文件名描述的文件,但找到了相同这样名称的目录,则会优先查找目录下的package.json, 通过main属性作为入口文件进行加载; 如果不存在package.json解析无效,则尝试查找目录下的index.js,index.node文件
    • node_modules:要加载的模块标识符不是一个核心模块,也没有以 ‘/’ 、 ‘…/’ 或 ‘./’ 开头 ,node会从单枪目录的父目录开始,尝试从该目录下的node_modules目录加载,并逐级向上一层查找,直到找到,这些node_modules目录被存储在当前module的paths属性上,如下:
      paths: [ 'D:\\selfProject\\letCode\\node\\util-src\\node_modules', 'D:\\selfProject\\letCode\\node\\node_modules', 'D:\\selfProject\\letCode\\node_modules', 'D:\\selfProject\\node_modules', 'D:\\node_modules' ] }
    • 全局模块:如果paths中的node_modules目录下都没有找到该模块,则尝试在全局目录中加载,也就是NODE_PATH 环境变量中配置的目录,全局目录存储在module模块的globalPaths属性中
  • 模块管理工具

    node的核心模块数量不多,但在其上衍生出来的第三方模块数以百万计,为了解决大量模块管理的问题,相继出现了npm,yarn,pnpm等优秀的工具详见node版本管理,包管理

全局对象

global是指node中的全局变量的宿主,主要包含以下对象:Buffer,global,timers,console,process,URL,queueMicrotask,TextDecoder,WebAssembly等,可以通过在global上挂载变量来自定义全局变量,如global.config = {“db”:“mysql”}

进程

如上所述,process是一个全局对象,它提供了对当前node进程管理的能力

  • 事件
    process 是EventEmitter 的实例,node为process定义了很多事件来捕获进程生命周期内的通信和异常情况,提供给开发者处理,常用的如’exit’,‘message’,‘uncaughtException’,'信号事件’等

  • 属性和方法
    process 提供了一些属性和方法,用来获取和管理当前进程的信息,状态。如’process.cwd()’,'process.argv’等。通过这些属性和方法可以进行node命令行脚本开发,进程状态管理等

os

os 模块提供了与操作系统相关的实用方法和属性,如os.cpus(),os.freemem(),os.loadavg()等,通过这些方法可以获取进程使用资源的情况,一般这些数据都是存储在进程号对应的目录下,也可以通过其他系统工具(如ps,ss)获取

events

大多数 Node.js 核心 API 构建于惯用的异步事件驱动架构,其中某些类型的对象(又称触发器,Emitter)会触发命名事件来调用函数(又称监听器,Listener),所有能触发事件的对象都是 EventEmitter 类的实例。

  • event 原理

    node的EventEmitter 是对观察者模式的实现详见:设计模式-观察者模式

    1. 什么是观察者模式

      观察者模式是这样一种设计模式。一个被称作被观察目标的对象,维护一组被称为观察者的对象,这些观察者依赖于观察目标,被观察者自动将自身的状态的任何变化通知给它们。

      它定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。

    2. 逻辑关系

      观察者模式,包括如下角色,Subject: 目标,ConcreteSubject: 具体目标,Observer: 观察者,ConcreteObserver: 具体观察者,关系如下:

      观察者

  • event 使用及注意事项

    • 事件回调同步和异步

      当 EventEmitter 对象触发一个事件时,所有绑定在该事件上的函数都会被同步地调用。 被调用的监听器返回的任何值都将会被忽略并丢弃

          const EventEmitter = require('events');
      
          class MyEmitter extends EventEmitter {}
          const myEmitter = new MyEmitter();
          myEmitter.on('event', () => {
              console.log('嘤嘤嘤');
          });
          myEmitter.emit('event');
          console.log('111')
      

      但监听器函数可以使用 setImmediate() 和 process.nextTick() 方法切换到异步的操作模式:

          const EventEmitter = require('events');
          class MyEmitter extends EventEmitter {}
          const myEmitter = new MyEmitter();
          myEmitter.on('event', () => {
              setImmediate(() => {
                  console.log('异步地发生');
              });
          });
          myEmitter.emit('event');
          console.log('111')
      
    • 避免事件递归

      不当的事件递归调用会让程序陷入死循环导致栈溢出

          const EventEmitter = require('events');
      
          class MyEmitter extends EventEmitter {}
          const myEmitter = new MyEmitter();
          myEmitter.on('event', () => {
              console.log('嘤嘤嘤');
               myEmitter.emit('event');
          });
          myEmitter.emit('event');
          console.log('111')
      
    • 错误处理

      当 EventEmitter 实例出错时,应该触发 ‘error’ 事件。 这些在 Node.js 中被视为特殊情况。如果没有为 ‘error’ 事件注册监听器,则当 ‘error’ 事件触发时,会抛出错误、打印堆栈跟踪、并退出 Node.js 进程。所以应始终为’error’ 事件注册监听器,捕获并处理错误。

  • 延申阅读

    前面提到观察者模式,通过建立对象间的一种依赖关系,在一个对象发生改变时自动通知其他对象,其他对象将做出反应。这和平时用到的mq,redis等中间件的发布-订阅很像,也和我们常用的事件委托,信号槽等机制很像,关于这些区别,可以看下面的延申阅读,这里也总结下:
    事件委托是观察者模式的一种特殊实现;发布订阅是通过消息代理进行通知,观察对象和观察者之间互相不知道对方的存在,从而实现两者的完全解耦。

timer

node中 timer是全局的API,用于安排函数在未来某个时间点被调用。

  • Immediate

    此对象在 setImmediate() 内部创建并返回, 它可以传给 clearImmediate() 以取消已安排的行动

  • Timeout

    此对象在 setTimeout() 和 setInterval() 内部创建并返回。 它可以传给 clearTimeout() 或 clearInterval() 以取消已安排的行动。

  • clear 和 unref

    Immediate 和 Timeout 对象都有对应的 clear 方法,可用于取消定时器并阻止其触发
    Immediate 和 Timeout 都有unref方法,有一些博文将其也解释为取消定时器。实质上,当unref被调用时,活动的定时器对象将不会要求 Node.js 事件循环保持活动状态, 如果没有其他的活动保持事件循环运行,则进程可能会在 定时器 对象的回调被调用之前退出,定时器对象没有被取消,其回调之所以会不被执行,是因为事件循环结束,进程退出了

error

Error API 是node中进行错误报告的api,错误报告方式有throw机制,回调错误(常说的错误优先),错误事件三种,针对不同的错误报告方式,需要分别采用对应的错误捕获方式,分别为try…catch;错误判断;错误事件捕获 ,除此之外,Error Api 提供了一些ErrorCode 来方便定位错误的范围

child_process

之前的process模块提供了对当前运行进程管理的能力,但不能创建新进程,child_process 提供了创建多进程的能力,可以通过这种方式利用单机的多核资源

  • 进程

    进程是运行中的程序,是系统资源分配和调度的最小单位;多进程就是对进程进行复制,每一个子进程都拥有独立的用户空间地址,数据栈,进程之间无法访问进程里定义的全局变量,数据结构,只能通过进程间通信(IPC)的方式进行数据共享

  • 用户空间和内核空间

    每一个进程都拥有独立的用户空间地址,那什么是用户空间地址呢

    现在操作系统都是采用虚拟存储器,对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核,保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。

  • 进程间通信

    进程所拥有的资源都是独立的,那进程之间进行资源共享,需要通过IPC的方式进行数据共享,IPC有一下几种:

    • 信号: 信号是Unix系统中的最古老的进程间通讯方式。它们用来向一个或多个进程发送异步事件信号。如kill -9 就是发送SIGKILL事件信号
    • 管道:管道是单向的字节流,它将某个进程的标准输出连接到另外进程的标准输入
    • 消息队列:消息队列是一个消息的链表,它允许一个或者多个进程向它写入与读取消息
    • 信号量:是一个同步变量,用于保持在0至指定最大值之间的一个计数值,该变量初始化后只能进行P/V操作
    • 共享存储:共享内存允许一个或多个进程通过同时出现在它们虚拟地址空间中的内存来通讯
    • 套接字:网络通信接口

    IPC详见

  • 创建进程方式

    • spawn/spawnSync 启动子进程执行命令
    • exec/execSync 启动子进程 衍生shell并在shell中执行命令
    • execFile/execFileSync 启动子进程执行可执行文件
    • fork fork方法是spawn的一个特例,专门用于衍生新的 Node.js 进程,返回的 ChildProcess 将会内置一个额外的通信通道,允许消息在父进程和子进程之间来回传递

    以上sync后缀的同步方法会阻塞Node事件循环,等到进程完全退出后才返回

Buffer

Buffer是一个典型的Javascript和C++结合的模块,性能相关部分用C++实现,非性能相关部分用javascript实现,Buffer 类是一个全局变量,用于直接处理二进制数据。 它可以使用多种方式构建。同过buffer可以使用V8堆内存之外的原始内存

  • 创建buffer

    buffer 提供了 alloc,from,allocUnsafe 三个方法来创建buffer

    
    
  • buffer 编码

    buffer和字符串转换时可以指定编码,如果没有指定,默认使用UTF-8作为默认值,node支持的编码如下:

    • ‘uft-8’: 多字节编码的 Unicode 字符。 许多网页和其他文档格式都使用 UTF-8。 这是默认的字符编码。 当将 Buffer 解码为不专门包含有效 UTF-8 数据的字符串时,则会使用 Unicode 替换字符 U+FFFD � 来表示这些错误。
    • ‘utf16le’:多字节编码的 Unicode 字符。 与 ‘utf8’ 不同,字符串中的每个字符都会使用 2 个或 4 个字节进行编码。 Node.js 仅支持 UTF-16 的小端序变体。
    • ‘latin1’:Latin-1 代表 ISO-8859-1。 此字符编码仅支持从 U+0000 到 U+00FF 的 Unicode 字符。 每个字符使用单个字节进行编码。 超出该范围的字符会被截断,并映射成该范围内的字符。
    • ‘base64’:Base64 编码。 当从字符串创建 Buffer 时,此编码也会正确地接受 RFC 4648 第 5 节中指定的 “URL 和文件名安全字母”。
    • ‘hex’:将每个字节编码成两个十六进制的字符。 当解码仅包含有效的十六进制字符的字符串时,可能会发生数据截断
    • ‘ascii’:仅适用于 7 位 ASCII 数据。 当将字符串编码为 Buffer 时,这等效于使用 ‘latin1’
    • ‘binary’:‘latin1’ 的别名
    • ‘ucs2’:‘utf16le’ 的别名

    编码本身就是一门神奇的学问,避免编码问题在日常项目中出问题,就要尽可能统一规范,包括机器的采集,系统的选择

  • 内存分配

    slab机制先申请再分配,具有如下三种状态

    • 满的:slab 的所有对象标记为使用。
    • 空的:slab 上的所有对象标记为空闲。
    • 部分:slab 上的对象有的标记为使用,有的标记为空闲。

    node buffer 在初次加载时就会初始化 1 个 8KB 的内存空间,创建buffer对象时 根据申请的内存大小分为 小 Buffer 对象 和 大 Buffer 对象,如果是小 Buffer,会继续判断这个 slab 空间是否足够,如果空间足够就去使用剩余空间同时更新 slab 分配状态,偏移量会增加;如果空间不足,slab 空间不足,就会去创建一个新的 slab 空间用来分配。大 Buffer 情况,则会直接调用 createUnsafeBuffer(size) 函数,不论是小 Buffer 对象还是大 Buffer 对象,内存分配是在 C++ 层面完成,内存管理在 JavaScript 层面,最终还是可以被 V8 的垃圾回收标记所回收

    内核内存分配延申阅读:伙伴系统,slab机制

  • 碎片整理

    内存分配和释放的过程中,会产生内存碎片,内存碎片即“碎片的内存”描述一个系统中所有不可用的空闲内存,这些碎片之所以不能被使用,是因为负责动态分配内存的分配算法使得这些空闲的内存无法使用。所以需要对这些碎片内存进行整理,

  • 使用场景

    • 流处理
    • 加解密
    • 缓冲(缓冲(buffers)是根据磁盘的读写设计的,把分散的写操作集中进行,减少磁盘碎片和硬盘的反复寻道,从而提高系统性能)

stream

流是对输入输出设备的抽象,具有方向性,通过流可以方便的处理二进制数据

  • 流分类

    • 可读流 可以读取数据的流

    readstream
    readevent

    • 可写流 可以写入数据的流

    write-stream

    • 双工流 : 可读又可写的流
    • 转换流 :在读写过程中可以修改或转换数据的 Duplex 流
  • 流应用

    • (大)文件操作

    node提供了fs模块用于文件操作,而该模块常见的api均基于内存直接存取文件,对于大文件进行这样的操作会导致进程崩溃,所以fs又提供了基于流操作文件的类,这样可以通过缓冲(背压,流控)解决大文件读写的问题

    • 网络数据传输

    http 模块中req,和 res 对象基于流操作
    net 模块用于创建基于流的 TCP 或 IPC 的服务器(net.createServer())与客户端(net.createConnection())

    • 文件加解密,解压缩

      继承自转换流 的zlib ,crypto 模块

    • 自定义流

      browserify gulp webpack等工具中的流处理 详见

fs

fs模块提供了与文件系统进行交互的API,node提供了同步和异步两种API,由于同步API会阻塞进程,所以我们应该尽可能在生产使用异步API

  • api

    fs分别提供了同步和异步操作文件系统的api,也提供了对流对象的封装,通过流的方式操作文件,成为文件流,node不断迭代的过程中,也逐步提供了promise化api

  • 文件系统

    文件系统是一套实现了数据的存储、分级组织、存取和获取等操作的抽象数据类型;虚拟文件系统是对文件系统的抽象,屏蔽了不同文件系统协议的差异,方便用户程序调用操作文件系统

    filesys

  • IO模型

    • 为什么会有IO模型 详见

    IO操作是应用程序通过向内核发起系统调用完成对I/O的间接访问,一次IO包括两个阶段:

    1. 系统调用阶段:应用进程向内核发起系统调用
    2. IO执行阶段:内核等待IO设备准备好数据;将数据从内核缓冲区拷贝到用户空间缓冲区

    应用程序发起IO调用到内核执行IO返回之前,应用进程/线程所处的状态可能是阻塞或非阻塞的,根据此状态将IO区分为阻塞IO,非阻塞IO,所以阻塞和非阻塞关心的是应用程序发起内核调用是否立即返回
    iostep

    • 阻塞Io

    阻塞IO是指发起内核调用后,用户进程一直阻塞等待数据返回,而不进行其他操作
    但应用程序阻塞等待,会浪费cpu资源

    • 非阻塞IO

    非阻塞IO是指发起内核调用后,内核收到请求后立即返回,用户进程通过轮询的方式获取内核的处理结果。
    但是轮询依旧需要进行系统调用,高并发下,频繁无效的系统调用会浪费cpu性能

    • IO多路复用

    IO多路复用是内核提供的能力,可以同时等待多个IO描述符,其中一个数据准备就绪后(此处阻塞),就可以返回,有select/poll/epoll 三种实现方式

    • 信号驱动IO

    信号驱动IO在系统调用发起后,会注册一个信号,本次调用立即返回,内核中的数据准备好之后,会立即给应用程序发送信号,此时应用程序再发起系统调用复制数据

    • 异步IO(AIO)

    异步IO将所有工作都交给内核,应用程序发起系统调用后,立即返回,内核会准备好数据并完成数据拷贝,此时再通知应用进程IO已完成,在此期间应用进程和内核之间是并行的

  • 总结

    iocatlog

net

net 模块提供了创建TCP通信 或 IPC的能力,node中的http模块基于net模块实现

dgram

dgram 模块提供了 UDP 数据包 socket 的实现
  • UDP

    udp是无连接,不可靠的简易的数据传输协议,工作在传输层。由于其不需要建立连接,不保证数据传输的可靠性,所以无需复杂的三次握手,四次握手,拥塞控制等处理过程

  • dgram 创建服务端

    const dgram = require('dgram');
    const server = dgram.createSocket('udp4');
    
    server.on('error', (err) => {
        console.log(`服务器异常:\n${err.stack}`);
        server.close();
    });
    
    server.on('message', (msg, rinfo) => {
        console.log(`服务器接收到来自 ${rinfo.address}:${rinfo.port}${msg}`);
    });
    
    server.on('listening', () => {
        const address = server.address();
        console.log(`服务器监听 ${address.address}:${address.port}`);
    });
    
    server.bind(41234);
  • dgram 创建客户端
    const dgram = require('dgram');
    const buf1 = Buffer.from('node');
    const buf2 = Buffer.from('test');
    const client = dgram.createSocket('udp4');
    client.send([buf1, buf2], 41234, (err) => {
        client.close();
    });
  • socket应用

    socket编程可以建立长连接,完成点对点即时通信(IM)和实时通信(RTC)的场景的需求

  • socket.io

    socket.io 是一个为实时应用提供跨平台实时通信的库。socket.io 旨在使实时应用在每个浏览器和移动设备上成为可能,模糊不同的传输机制之间的差异。

http/https/http2

http相关的模块提供了基于Http协议通信的API,将消息解析为消息头和消息体
  • http

    HTTP 超文本传输​​协议是位于 TCP/IP 体系结构中的应用层协议,它是万维网的数据通信的基础。

    • Agent:管理Http客户端连接的重用和持久性
    • ClientRequest: 管理正在进行的请求
    • Server: 继承自net.Server,用来管理Http 服务端
    • ServerResponse: 此对象由 HTTP 服务器在内部创建,而不是由用户创建。 它会作为第二个参数传给 ‘request’ 事件。
    • IncomingMessage: IncomingMessage 对象由 http.Server 或 http.ClientRequest 创建,并分别作为第一个参数传给 ‘request’ 和 ‘response’ 事件。 它可用于访问响应状态、消息头、以及数据
  • https

    HTTPS 是基于 TLS/SSL 的 HTTP 协议。在 Node.js 中,其被实现为一个单独的模块。

    • Agent 管理Https客户端连接的重用和持久性

    • Server 继承自 tls.Server,管理https 服务端的连接

  • http 协议报文

    https-message

  • https 加解密原理

    • 通信过程

    http-process

    • 原理详解

    https 结合了非对称加密和对称加密,通过非对称加密验证证书的可靠性,并完成对称加密 密钥的生成和传递,然后通过密钥进行对称加密的通信

    非对称加密的过程如图

    httpsca

    详见

  • http2 简介
    HTTP/2 是 HTTP/1.x 的扩展,TTP 2.0 增加了新的二进制分帧数据层,而这一层并不兼容之前的 HTTP 1.x。

querystring

querystring 模块提供用于解析和格式化 URL 查询字符串的实用工具

path

path 模块提供了一些实用工具,用于处理文件和目录的路径,封装了linux和windows平台差异

url

url 模块用于处理与解析 URL,具体解析的url结构如图

url

crypto

crypto 模块提供了加密功能,包括对 OpenSSL 的哈希、HMAC、加密、解密、签名、以及验证功能的一整套封装。

  • Cipher 加密
  • Decipher 解密
  • Hash 常用生成md5
  • Sign 签名
  • Verify 验签
  • 加密和签名的区别 详细见

加密是为了防止信息泄漏(比如密码),签名是为了防止信息被篡改(比如交易金额)

util

util 模块用于支持 Node.js 内部 API 的需求

  • 主要Api

    • inherits 实现函数之间的"继承"
    • promisify 将一个错误优先的回调风格的函数改造为promise化的函数
    • types 类型判断函数集

console

console 模块提供了一个简单的调试控制台,类似于 Web 浏览器提供的 JavaScript 控制台。

  • log/info 输出到stdout
  • error/warn 输出到stderr
  • trace 打印调用位置的堆栈信息

console.log的注意事项

console.log默认输出到process.stdout(可以通过Console指定输出的流),由于stdout流在各平台上实现的差异性,导致console.log的输出可能是异步或者同步的,写操作是否为同步,取决于连接的是什么流以及操作系统是 Windows 还是 POSIX :

  • 文件:在 Windows 和 POSIX 上是同步的。
  • TTY(终端):在 Windows 上是异步的,在 POSIX 上是同步的。
  • 管道(和 socket):在 Windows 上是同步的,在 POSIX 上是异步的。

参考资料

node中文文档

https加密详解

为什么会有IO模型

部分图片来自网络文章

github

github地址

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐