深入理解 Node

E5qn7d.png

# 前言

本文是深入理解 Node.js:核心思想与源码分析 (opens new window) 的读书笔记。一些难理解的和必要重要的点先暂时记录在这里了。

# 架构

# 四大部分

E5qLEd.jpg Node.js 主要分为四大部分,Node Standard Library,Node Bindings,V8,Libuv

  • Node Standard Library 是我们每天都在用的标准库,如 Http, Buffer 模块。
  • Node Bindings 是沟通 JS 和 C++的桥梁,封装 V8 和 Libuv 的细节,向上层提供基础 API 服务。这一层是支撑 Node.js 运行的关键,由 C/C++ 实现。
  • V8 是 Google 开发的 JavaScript 引擎,提供 JavaScript 运行环境,可以说它就是 Node.js 的发动机。
  • Libuv 是专门为 Node.js 开发的一个封装库,提供跨平台的异步 I/O 能力。
  • C-ares:提供了异步处理 DNS 相关的能力。
  • http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、数据压缩等其他的能力。

# V8 架构

EI6npR.png

现在 JS 引擎的执行过程大致是:源代码 --->抽象语法树 --->字节码 --->JIT--->本地代码。

V8 更加直接的将抽象语法树通过 JIT 技术转换成本地代码,放弃了在字节码阶段可以进行的一些性能优化,但保证了执行速度。 在 V8 生成本地代码后,也会通过 Profiler 采集一些信息,来优化本地代码。虽然,少了生成字节码这一阶段的性能优化, 但极大减少了转换时间。

# GC (Garbage Collection)

垃圾回收器解决基本问题就是,识别需要回收的内存。一旦辨别完毕,这些内存区域即可在未来的分配中重用,或者是返还给操作系统。一个对象当它不是处于活跃状态的时候它就死了。一个对象处于活跃状态,当且仅当它被一个根对象或另一个活跃对象指向。根对象被定义为处于活跃状态,是浏览器或 V8 所引用的对象。比如说全局对象属于根对象,因为它们始终可被访问;浏览器对象,如 DOM 元素,也属于根对象,尽管在某些场合下它们只是弱引用。

# 模块加载

# 四种模块类型

  • builtin module: Node 中以 c++ 形式提供的模块,如 tcp_wrap、contextify 等
  • constants module: Node 中定义常量的模块,用来导出如 signal, openssl 库、文件访问权限等常量的定义。如文件访问权限中的 O_RDONLY,O_CREAT、signal 中的 SIGHUP,SIGINT 等。
  • native module: Node 中以 JavaScript 形式提供的模块,如 http,https,fs 等。有些 native module 需要借助于 builtin module 实现背后的功能。如对于 native 模块 buffer , 还是需要借助 builtin node_buffer.cc 中提供的功能来实现大容量内存申请和管理,目的是能够脱离 V8 内存大小使用限制。
  • 3rd-party module: 以上模块可以统称 Node 内建模块,除此之外为第三方模块,典型的如 express 模块。

# 模块加载原理

// lib/module.js
Module.prototype.require = function (path) {
  assert(path, 'missing path')
  assert(typeof path === 'string', 'path must be a string')
  return Module._load(path, this)
}
Module._load = function (request, parent, isMain) {
  if (parent) {
    debug('Module._load REQUEST %s parent: %s', request, parent.id)
  }

  var filename = Module._resolveFilename(request, parent)

  var cachedModule = Module._cache[filename]
  if (cachedModule) {
    return cachedModule.exports
  }

  if (NativeModule.nonInternalExists(filename)) {
    debug('load native module %s', request)
    return NativeModule.require(filename)
  }

  var module = new Module(filename, parent)

  if (isMain) {
    process.mainModule = module
    module.id = '.'
  }

  Module._cache[filename] = module

  var hadException = true

  try {
    module.load(filename)
    hadException = false
  } finally {
    if (hadException) {
      delete Module._cache[filename]
    }
  }

  return module.exports
}
  • 如果模块在缓存中,返回它的 exports 对象。
  • 如果是原生的模块,通过调用 NativeModule.require() 返回结果。
  • 否则,创建一个新的模块,并保存到缓存中。

# Global 对象

# 全局对象和全局变量

# 全局对象

  • global:表示 Node 所在的全局环境,类似于浏览器的 window 对象。
  • process:该对象表示 Node 所处的当前进程,允许开发者与该进程互动。
  • console:指向 Node 内置的 console 模块,提供命令行环境中的标准输入、标准输出功能。

# 全局函数

  • setTimeout():用于在指定毫秒之后,运行回调函数。
  • setImmediate(): 下一次事件循环调用。
  • clearTimeout():用于终止一个 setTimeout 方法新建的定时器。
  • setInterval():用于每隔一定毫秒调用回调函数。
  • clearInterval():终止一个用 setInterval 方法新建的定时器。
  • require():用于加载模块。

# 伪全局变量

有一些对象实际上是模块内部的局部变量,指向的对象根据模块不同而不同,但是所有模块都适用,可以看作是伪全局变量

  • __filename:指向当前运行的脚本文件名。
  • __dirname:指向当前运行的脚本所在的目录。
  • module
  • exports

# 事件循环

事件循环的职责,就是不断得等待事件的发生,然后将这个事件的所有处理器,以它们订阅这个事件的时间顺序,依次执行。当这个事件的所有处理器都被执行完毕之后,事件循环就会开始继续等待下一个事件的触发,不断往复。 当同时并发地处理多个请求时,以上的概念也是正确的,可以这样理解:在单个的线程中,事件处理器是一个一个按顺序执行的。

# Node.js 中的事件循环

Node 采用 V8 作为 JavaScript 的执行引擎,同时使用 libuv 实现事件驱动式异步 I/O。其事件循环就是采用了 libuv 的默认事件循环。

# Buffer

在 Node.js 中,Buffer 类是随 Node 内核一起发布的核心库。Buffer 库为 Node.js 带来了一种存储原始数据的方法,可以让 Nodejs 处理二进制数据,每当需要在 Nodejs 中处理 I/O 操作中移动的数据时,就有可能使用 Buffer 库。原始数据存储在 Buffer 类的实例中。一个 Buffer 类似于一个整数数组,但它对应于 V8 堆内存之外的一块原始内存。

# Event

事件中心,发布订阅模式。

# Domain

捕获异步回调中的异常,基于 Event 模块实现。已废弃。

# Stream

var http = require('http')
var fs = require('fs')

var server = http.createServer(function (req, res) {
  var stream = fs.createReadStream(__dirname + '/data.txt')
  stream.pipe(res)
})
server.listen(8000)

.pipe() 方法会自动帮助我们监听 data 和 end 事件。

# 网络

# 网络模型

VUrZAe.gif

# Socket 抽象

Socket 是对 TCP/IP 协议族的一种封装,是应用层与 TCP/IP 协议族通信的中间软件抽象层。它把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面,对用户来说,一组简单的接口就是全部,让 Socket 去组织数据,以符合指定的协议。

Socket 还可以认为是一种网络间不同计算机上的进程通信的一种方法,利用三元组(ip 地址,协议,端口)就可以唯一标识网络中的进程,网络中的进程通信可以利用这个标志与其它进程进行交互。 Socket 起源于 Unix ,Unix/Linux 基本哲学之一就是“一切皆文件”,都可以用“打开(open) –> 读写(write/read) –> 关闭(close)”模式来进行操作。因此 Socket 也被处理为一种特殊的文件。

# 进程

# 子进程 (child_process)

child_process 是 Node 的一个十分重要的模块,通过它可以实现创建多进程,以利用单机的多核计算资源。虽然,Nodejs 天生是单线程单进程的,但是有了 child_process 模块,可以在程序中直接创建子进程,并使用主进程和子进程之间实现通信。

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核, 在内核中开辟一块缓冲区,进程 1 把数据从用户空间拷到内核缓冲区,进程 2 再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。