# 概念

# 介绍

node.js 是一个异步的事件驱动的 JavaScript 运行时,node.js 的特性其实是 JS 的特性: 非阻塞I/O事件驱动 。这种基于事件的编程方式具有 轻量级松耦合只关注事务点 等优势。

# 发展

Node 的出现将前后端的壁垒再次打破,JavaScript 这门最初就能运行在服务端的语言,在经历了前端的辉煌和后端的低迷后,借助事件驱动和 V8 的高性能,再次成为了服务端的佼佼者。在 Web 应用中,JavaScript 将不再仅仅出现在前端浏览器中,因为 Node 的出现,前端将会被重新定义。

# 核心

Node 带来的最大特性莫过于:基于事件驱动的非阻塞 I/O 模型,它可以使 CPU 与 I/O 并不相互依赖等待,让资源得到更好的利用

# 架构

node 的核心相当于: v8引擎 + libuv库 + 各种native api模块 的集合
事件循环
node 是利用 v8 引擎打造的基于事件循环实现的异步 I/O 框架
优势
Node 在高并发、高性能后端服务程序上也有着极大的优势:

  • 非阻塞 IO 模型(使得 node.js 适用于高交互的场景,可以在等待 IO 操作完成时继续处理其他请求,从而提高性能和吞吐量)
  • 事件驱动(不创建新的线程的情况下处理大量并发连接,减少了资源消耗并提升了效率)
  • 单线程(因为 js 运行在单线程上,通过事件循环和回调函数来处理并发,从而避免了多线程编程中的复杂性和同步问题)
  • 轻量级线程(node.js 使用 libuv 来管理轻量级线程,用于执行实际的 IO 操作)
  • 跨平台良好

# 认识 node

# 异步 IO

在 Node 中,绝大多数的操作都是以异步的方式进行调用,这样可以使得操作并行处理,大大节省运行时间。

# 单线程

单线程概念

node 单线程指的是:node.js 并没有给我们创建一个线程的能力,所有我们自己写的代码都是单线程执行的,在同一时间内,只能执行我们写的一句代码。但是宿主环境 node.js 并不是单线程的, 它会维护一个回调队列 ,libuv 中的线程池在完成一项任务之后 会将回调函数放入回调队列中 ,后面 libuv 会调度 JS 线程来进行执行。因此: 单线程操作和并发执行并不冲突,node采用回调队列+事件循环的模式来完成并发操作
误区
除了你的代码是单线程,其余都是多线程(线程池),nodejs 本身是事件驱动,一个 io 事件完成会被放到一个事件队列中,主线程负责轮询这个队列,然后执行相应的回调函数。
单线程优势
node 中:JS 与其余线程是无法共享状态的,单线程最大的优势就在于:不用像多线程那样处处在意同步问题,这里没有死锁的存在,也没有线程上下文交换所带来的性能上的消耗。
单线程的劣势

  • 无法充分利用多核 CPU
  • 错误会引起整个应用退出
  • 大量计算占用 CPU 会导致无法继续调用异步 IO

# 跨平台

node.js 的跨平台特性得益于 libuv,和 libuv 的底层屏蔽了各个操作系统之间的差异。

# IO 密集型

node 很擅长 IO 密集型应用场景,因为 node 面向网络且擅长并行 IO, 能够有效的组织起更多的硬件资源,从而提供更好的服务。IO 密集的优势主要在于 Node 利用事件循环的处理能力,而不是启动每一个线程为每一个请求服务,资源占用极少

# CPU 密集型

由于 v8 引擎的高性能,使得 node.js 在 CPU 密集型应用中仍然有不俗的表现。
CPU 密集型应用带给 Node.js 最大的挑战是:
由于 JS 单线程的原因,如果有长时间的运行的计算,将会导致 CPU 时间片不能释放,使得后续 IO 无法发起,但是适当调整和分解大型运算任务为多个小任务就可以使得运算能够适时释放,而不阻塞 IO 调用的发起,这样既可以同时享受到并行异步 IO 的好处又能充分利用 CPU
node 可以通过编写 C/C++ 扩展的方式更高效的利用 CPU
node 可以通过子进程的方式将一部分 Node 进程当作常驻服务进行用于计算,然后利用进程间的消息来传递结果,将计算与 IO 分离,这样还能充分利用 CPU

# 分布式应用

Node 非常适用于 BFF 中间层,例如:分布式应用,而非单纯的后端开发
比如一个数据平台去数据库集群中查询数据,node 编写的中间层可以并行的去多台数据库中获取数据并合并,这个过程中,对于 node 来说只是一次普通的 IO 操作,但对于数据库而言是一次复杂的计算,所以也是进而充分压榨硬件资源的过程。

# 实时应用

得益于 node 的高性能 IO

# 并行 IO,有效提升 web 渲染能力

摒弃同步等待方式,大胆采用并行 IO,加速数据的获取进而提升 Web 的渲染速度

# 模块

# Commonjs

Nodejs 采用 Commonjs 的模块规范,在 Node 中,模块分为两类:核心模块和文件模块

# 核心模块

核心模块部分在 Node 源码编译过程中,编译进了二进制执行文件,在 Node 进行启动时,部分核心模块就被加载进了内存中

# 文件模块

文件模块则是在运行时动态加载的,需要完整的路径分析、文件定位和编译执行过程,速度比核心模块要慢

# 模块缓存

Node 像浏览器一样,对于引入过的模块都会进行缓存,浏览器仅仅缓存文件,而 Node 缓存的是编译和执行之后的对象,核心模块的缓存检查先于文件模块的缓存检查

# 文件查找规则

# 路径查找

  • 缓存加载
  • 核心模块,如:fs 模块
  • 路径形式的文件模块 m
  • 自定义模块 (node_modules 中)
    • 自定义模块在查找时,会现在当前文件目录下的 node_modules 目录中向上级依次进行查找,当前文件的路径越深,模块的查找速度就会越慢

# 文件定位

  • CommonJS 规范允许文件标识符中省略文件后缀,Node 会按照 .js、.json 和.node 的顺序依次补足扩展名,其余扩展名会被当作 js 文件进行载入
  • 如果查找到是一个文件夹,node 就会查找文件夹中 package.json 的入口文件
  • 如果还没有找到,Node 就会依次查找 index.js、index.json 和 index.node 文件
  • 如果还没有找到,node 就会抛出一个异常

# 模块全局变量

  • 模块全局变量并不是当 node.js 执行时,声明在全局的,而是类似回调的方式进行的一种首位包装

# 模块的编译

# js 模块

  • 编译过程中,Node 对获取的 js 文件进行了头尾包装,在头部添加了 (function (exports, require, module, __filename, __dirname) {\n,在尾部添加了 \n});
  • 有了 exports 还需要 module.exports 的原因在于:避免 exports 直接赋值导致改变了形参的引用

# C/C++ 模块

  • 对于 C/C++ 模块,Node 调用 process.dlopen () 方法进行加载和执行
  • C/C++ 模块主要用于提升执行效率

# JSON 文件

  • JSON 文件的编译会调用 JSON.parse () 方法得到对象,然后赋值给 exports

# 核心模块

  • 核心模块分为 C/C++ 编写的和 JS 编写的两部分
  • 对于核心模块,Node 采用 V8 附带的工具:js2c.py 工具,将所有内置的 JS 代码转换成 C++ 里面的数组,生成 node_natives.h 头文件
  • 这个过程中,js 代码以字符串的形式存储在 node 命名空间中,当启动 Node 进行时,js 代码直接加载进内存中,在加载时,js 核心模块经历标识符分析之后直接定位到内存中,比普通文件模块从磁盘中一处一处查找要快很多

# 内建模块

  • 那些工作在 node 底层、由 C/C++ 编写的模块被称为内建模块
  • 内建模块的优势在于:首先由于是 C/C++ 编写的,所以性能会优于脚本语言,其次在进行文件编译时,他们被编译为二进制文件,一旦 Node 开始执行,它们就会被加载进内存中,无须再次做标识符定位、文件定位和编译等过程,直接执行即可

# 总结

  • CommonJs 提出的规范均十分简单,但是现实意义却十分强大,node 通过模块规范,组织了自身的原生模块,弥补了 JS 弱结构性的问题,形成了稳定的结构,并向外提供服务

# 异步 I/O

# 介绍

与 Node 的事件驱动、异步 I/O 设计理念比较相近的一个知名产品是 Nginx, 它采用 C 编写,性能表现优异,他们的区别在于:Nginx 具备面向客户端管理连接的强大能力,但是它的背后仍然受限于各种同步方式的编程语言,但 Node 却是全方位的,既可以作为服务器端去处理客户端带来的大量并发请求,也可以作为客户端向网络中的各个应用进行并发请求

# 设计理念

为什么需要异步 I/O ?, 原因是 I/O 是昂贵的,而分布式 I/O 是更昂贵的,只有后端能快速响应资源,才能让前端的体验更好

# 资源分配

如果业务场景中有一组互不相关的任务需要完成,目前主流方法有两种:

  • 单线程串行依次执行

单线程中通常 I/O 与 CPU 计算之间是可以并行进行的,但是同步的编程模型导致的问题是:I/O 的进行会让后续任务等待,造成资源不能更好的去利用

  • 多线程并行完成

多线程的代价子啊与创建线程和执行期线程上下文切换开销大,并且复杂业务中,多线程编程经常面临锁、状态同步等问题

# Node 单线程 & 多线程

Node 单线程是指:js 执行是单线程的,Node 多线程是指:Node 通过 libuv 中多线程队列去接收和响应 I/O 操作。Node 资源分配的方案为:利用单线程执行 js, 远离多线程死锁、状态同步等问题,利用异步 I/O 使得单线程远离阻塞,以便于能够更好的去利用 CPU。为了弥补单线程无法利用多核 CPU 的缺点,Node 提供了子进程,可以通过工作进程高效地利用 CPU 和 I/O

# 异步 I/O 与非阻塞 I/O

  • 操作系统内核对于 I/O 有两种方式:阻塞与非阻塞,在调用阻塞 I/O 时,应用程序需要等待 I/O 完成之后才返回结果。阻塞 I/O 的一个特点就是:调用之后一定要等到系统内核层面完成所有操作之后,调用才结束,系统内核在完成磁盘寻道、读取数据和复制数据到内存中之后,这个调用才结束

非阻塞 I/O 和阻塞 I/O 之间的差别在于调用之后是否会立即返回

  • 操作系统对计算机进行了抽象,将所有输入输出设备抽象为文件,内核在进行文件 I/O 操作时,通过文件描述符进行管理,而问及教案描述符类似于应用程序和系统内核之间的凭证,应用程序如何需要进行 I/O 调用,需要先打开文件描述符,然后再根据文件描述符去实现文件的数据读写
  • 非阻塞 I/O 也存在一些问题:由于完整的 I/O 并没有完成,立即返回的并不是业务层期望的数据,而仅仅是当前调用的状态,为了获取完成的数据,应用程序需要重复调用 I/O 操作来确认是否完成,这种重复调用判断操作是否完成的技术叫做轮询
  • 阻塞 I/O 会造成 CPU 等待的浪费,非阻塞 I/O 带来的麻烦是需要通过轮询去确认是否完全地获取到了数据,会使 CPU 处理状态判断,从而导致 CPU 资源浪费
  • 经过不断的发展,I/O 事件通知机制现在通常使用 epoll, 它在进入轮询的时候如果没有检查到 I/O 事件,将会进行休眠,指导实践发生将它唤醒,它真实利用了事件通知、执行回调的方式,而不是遍历查询,所以不会浪费 CPU, 执行效率较高,但是休眠期间 CPU 几乎是闲置的
  • Node 使用 libuv 中的线程池来完成异步 I/O 的目标,通过让部分线程进行阻塞 I/O 或者非阻塞 I/O 加轮询技术来完成数据获取,让一个线程进行计算处理,通过线程之间的通信,将 I/O 得到的数据进行传递,就轻松实现了异步 I/O。它的思想就是:调用异步方法,等待 I/O 完成之后的通知,执行回调,用户无需考虑轮询,但是它的内部仍然是线程池的原理,不同之处在于这些线程池由系统内核接手管理

# Node 的异步 I/O

# 事件循环

  • node 中的事件循环是基于 libuv 的线程池,每一个线程池都有一个 watcher 观察者和一个任务队列,当满足条件时将回调放入任务队列中等待 libuv 调度放入执行栈执行
  • 事件循环是一个典型的:生产者 / 消费者模型,异步 I/O、网络请求等则是事件的生产者
  • 进程启动时,Node 便会创建一个类似 while 的循环,每执行一次循环体的过程称为 Tick, 每个 Tick 的过程就是查看是否有事件待处理,如果有就取出事件及相关的回调函数,如果存在关联的回调函数,就执行他们,然后进入下个循环,如果不再有事件处理,就退出进程

事件循环流程

  • 外部输入数据
  • 轮询阶段
  • check 阶段 (process.nextTick)
  • 关闭事件回调阶段 (close callback)
  • 定时器检查阶段 (timer)
  • I/O 回调
  • 闲置阶段 (setImmediate)
  • 轮询阶段

node 中的微任务队列

  • node 中微任务队列有两个:nextTickQueue 和 microTaskQueue
  • 两个微任务队列的优先级相同,按照代码执行顺序进行执行

node 中的宏任务队列

  • timer,setImmediate,I/O 回调等

setImmediate 会在当前 tick 执行,而 settimeout 不传参数时,默认很小,可能在当前 tick 被轮询到,也可能当前 tick 没有找到

setTimeout(() => {
    console.log('当前tick执行');
});
setImmediate(() => {
    console.log('immediate当前tick执行');
})
setTimeout(() => {
    console.log('下一轮tick执行');
},0);

# 观察者

  • 每个事件循环中有一个或者多个观察者,而判断是否有事件要处理的过程就是向这些观察者询问是否有要处理的事件

# 请求对象

  • Node 中的异步 I/O 调用而言,回调函数却不由开发者来调用,那么从发出调用之后,到回调函数被执行的过程中,存在一种中间产物,叫做请求对象
  • 请求对象是异步 I/O 过程中重要的中间产物,所有的状态都保存在这个对象中,包括送入线程池等待执行以及 I/O 操作完毕后的回调处理

# 非 I/O 的异步 API

  • Node 中还存在一些与 I/O 无关的异步 API, 分别是:setTimeout、setInterval、setImmediate 和 process.nextTick

定时器

  • 定时器与浏览器中的 API 是一致的,分别用于单次和多次定时执行任务,他们的实现原理与异步 I/O 较为类似,只是不需要 I/O 线程池的参与,调用 setTimeout 或者 setInterval 创建的当时其会被插入到定时器观察者内部的一个红黑树中,每次 Tick 执行时,会从该红黑树中迭代取出定时器对象,检查是否超过定时时间,如果超过就形成一个事件,它的回调函数将会立即执行
  • 定时器的问题在于不够精确,如果依次循环占用的时间较多,那么下次循环时,也许已经超时了一会

process.nextTick

  • 每次调用 process.nextTick 时,只会将回调函数放入队列中,在下一轮 Tick 时取出执行

setImmediate

  • setImmediate 方法与 process.nextTick 方法十分类似,都是将回调函数延迟执行,但是两者有些细微的差别
  • process.nextTick 中的回调函数执行的优先级要高于 setImmediate, 原因是因为事件循环对观察者的检查是有先后顺序的,会先检查 nextTick 的 idle 观察者,后检查 setImmediate 的 check 观察者
  • 事件循环检查观察者的顺序:idle 观察者 ---I/O 观察者 ---check 观察者
  • 具体实现上: process.nextTick 的回调函数保存在一个数组中,setImmediate 的结果保存在链表中,在行为上,process.nextTick 在每轮循环中会将数组中的回调函数全部执行完,而 setImmediate 在每轮循环中执行链表中的一个回调函数
  • 也就是说:setImmediate 的回调在一个 Tick 中只会执行一次

# 事件驱动与高性能服务器

事件驱动的实质是:通过主循环加事件触发的方式来运行程序

# 服务器模型
  • 同步式,对于同步式的服务,一次只能处理一个请求,并且其余请求都处于等待状态
  • 每进程 / 每请求,为每个请求启动一个进程,这样可以处理多个请求,但是不具备扩展性,因为系统资源是固定的
  • 每线程 / 每请求,为每个请求启动一个线程来处理。尽管线程比进程要轻量,但是由于每个线程都占用一定内存,当大并发请求到来时,内存将会很快用光,1 导致服务器缓慢
  • Node 通过事件驱动的方式处理请求,无需为每一个请求创建额外的对应先册灰姑娘,可以省略掉创建线程和销毁线程的开销,这是 Node 高性能的一个原因

异步回调会被放入到对应的线程中,被相应的观察者进行观察,如果观察者观察到回调需要被执行了,就会放入到相应的任务队列中,从而被事件循环所执行

# 异步编程

# 特性

Node 带来的最大特性莫过于:基于事件驱动的非阻塞 I/O 模型,它可以使 CPU 与 I/O 并不相互依赖等待,让资源得到更好的利用

# 处理异常

  • 尝试对异步方法进行 try/catch 操作只能捕获当次事件循环内的异常,对 callback 执行时抛出的异常将无能为力
  • Node 在处理异常上形成了一种约定,江宜常作为回调函数的第一个实参传回,如果为空值则表明异步调用没有异常抛出

# 观察者模式

  • node 中的 event 是一种观察者的设计模式,可以实现一个事件与多个回到函数的关联,这些回调函数又称为事件侦听器.
  • 观察者模式和发布 / 订阅模式广泛应用于异步编程,通用来解耦业务逻辑
  • 事件侦听器模式也是一种 Hook 机制,利用钩子导出内部数据或状态给外部的调用者

同步 I/O 因为每个 I/O 都是彼此阻塞的,在循环体中,总是一个接着一个的调用,不会耗用文件描述符太多的情况,同时性能也是低下的,对于异步 I/O, 虽然并发容易实现,但是由于太容易实现,依然需要进行控制

# 内存控制

# V8

  • 在 V8 中,所有的 javaScript 对象都是通过堆来进行分配的
  • V8 限制堆大小的原因是因为:V8 最初为浏览器而设计,不太可能遇到用大量内存的场景,而深层原因是因为 V8 垃圾回收机制的限制
  • 取消 V8 内存限制的方式可以在启动 Node 时传入 --max-old-space-size 或者 --max-new-space-size 来调整内存限制的大小
  • V8 垃圾回收机制将内存空间分为新生代和老生代,新生代通过分区、赋值和同一清除的方式销毁旧的数据,老生代通过标记清除法配合清除碎片空间的算法进行清除
  • V8 新生代复制和清除的方式只能使用到一半的堆内存,采用的是典型的牺牲空间换取时间的算法,无法大规模的应用到所有的垃圾回收机制中
  • 垃圾回收是影响性能的因素之一,想要高性能的执行效率就需要注意让垃圾回收尽量少地进行
  • node 启动时,添加 --trace_gc 参数就可以查看垃圾回收日志
  • node 启动时,添加 --prof 参数就可以得到 V8 执行时地性能分析数据,包含了垃圾回收执行时占用的时间
  • V8 中通过 delete 删除对象的属性有可能会干扰到 V8 地优化,所以将属性值赋值为 undefined 或 null 去解引用更好

Node 地内存构成主要是通过 V8 进行分配的部分和 Node 自行分配的部分,受 V8 地垃圾回收限制的主要是 V8 的堆内存

# 内存泄漏

  • Node 对于内存泄漏十分敏感,内存泄露的实质只有一个就是应当回收的对象和出现意外而没有被回收,从而变成了常驻在老生代中的对象
  • 造成内存泄露的原因主要有几个:缓存、队列消费不及时和作用域未释放
  • 慎将内存当作缓存,缓存在引用的作用举足轻重,可以十分有效的节省资源,因为它的访问效率要比 I/O 的效率高,一旦命中缓存,就可以节省一次 I/O 的时间
  • 但是在 Node 中,缓存并不是物美价廉,一旦一个对象被当作缓存来使用,就将会导致垃圾回收机制在机型扫描和整理时,对这些对象做无用功
  • 在 Node 中,任何试图拿内存当缓存的行为都应该被小心使用
  • 另一个需要考虑的事情就是:进程之间无法共享内存,如果在进行中使用缓存,这些缓存就不可避免地有重复,对物理内存的使用是一种浪费
  • 目前使用大量缓存最好的方案是使用进程外的缓存,以减少常驻内存的对象数量,让垃圾回收更高效,并且进程之间可以共享缓存,市面上目前较好的有 Redis

# 内存泄漏排查

工具:v8-profiler、node-heapdump 等

# 大内存应用

由于 Node 的内存限制,操作大文件时要十分小心,Node 提供了 stream 模块用于处理大文件

  • 由于 V8 内存的限制,对于大文件无法通过 fs.readFile 和 fs.writeFile 进行大文件的操作,而是使用 fs.createReadStream 和 fs.createWriteStream 方法通过流的方式对大文件进行操作

如果不需要进行字符串层面的操作,则不需要借助 V8 来处理,可以尝试进行纯粹的 Buffer 操作,这将不会受到 V8 堆内存的限制 (不过物理内存仍然有限制)

# Buffer

Buffer 是一个类似 Array 的对象,但它主要用于操作字节,Buffer 是一个典型的 JavaScript 和 C 结合的模块,它将性能相关部分用 C 实现,将非性能相关的部分用 JavaScript 来实现,Node 中 Buffer 是一个全局对象而无需导入

  • Buffer 占用的内存不是通过 V8 分配的,属于堆外内存
  • Buffer 和 Array 类似,可以通过 length 得到长度,也可以通过下标访问元素,也有如 concat 等原型方法,两者的构造方式也相似

# Buffer 的转换

  • Buffer 对象可以和字符串之间相互进行转换
  • 字符串转 Buffer:
new Buffer(str,[encoding]);
  • Buffer 转字符串:
buf.toString([encoding],[start],[end]);
//buf.toString 默认以 UTF-8 的编码形式
  • Buffer 不支持某些编码类型,可以通过 Buffer.isEncoding (encoding) 来判断,对于不支持的编码类型可以使用第三方工具库进行转换
  • 可写流可以通过 setEncoding 设置编码方式
let rs = fs.createReadStream('test.md',{ highWaterMark:11 });
rs.setEncoding("utf8");

# Buffer 与性能

Buffer 在文件 I/O 和网络 I/O 中的运用十分广泛,尤其在网络传输中,它的性能举足轻重,一旦在网络中传输,都需要转换为 Buffer, 以进行二进制数据的传输,提高字符串的转换效率,可以很大程度地提高网络的吞吐率

  • 通过预先转换静态内容为 Buffer 对象,可以有效减少 CPU 地重复使用,节省服务器资源,静态内容部分可以通过预先转换为 Buffer 的方式,使得性能得到提升

# 网络编程

利用 Node 可以十分方便地搭建网络服务器,Node 中提供了 TCP、UDP、HTTP 和 HTTPS 等,适用于服务器端和客户端

# TCP

  • TCP 全名传输控制协议,在 OSI 网络模型中属于传输层协议
  • TCP 是面向连接的协议,显著特征是在传输之前需要进行三次握手形成会话,只有会话形成之后,服务端和客户端之间才能互相发送数据,在创建会话的过程中,服务端和客户端分别提供一个套接字,两个套接字共同形成一个连接
  • 在 Node 中通过 net 模块可以创建 TCP 服务

# TCP 服务的事件

对于通过 net 创建的服务器实例而言,这个实例是一个 EventEmitter 实例,它有几种自定义事件:

# 服务器事件
  • listening: 在调用 server.listen 绑定端口后触发
  • connection: 每个客户端套接字连接到服务端时触发,简洁写法为通过 net.create-Server, 最后一个参数传递
  • close: 当服务器关闭时触发,在调用 server.close 之后,服务器将停止接收进的套接字连接
  • error: 当服务器异常时,会触发该事件
# 连接事件
  • data: 当一端调用 write 发送数据时,另一端会触发 data 事件,事件传递的数据是 write 发送的数据
  • end: 当连接中的任意一端发送了 FIN 数据时,将会触发该事件
  • drain: 当任意一端调用 write 发送数据时,当前这端会触发该事件
  • 等等

# UDP 服务

UDP 又称用户数据包协议,与 TCP 一样属于网络传输层,UDP 与 TCP 最大的不同就在于:UDP 不是面向连接的,TCP 中连接一旦建立,所有的会话都基于连接完成,客户端如果要与另一个 TCP 服务通信,需要再创建一个套接字来完成连接,但是在 UDP 中,一个套接字可以与多个 UDP 服务通信

  • 要在 Node.js 中创建 UDP 服务器端,需要调用 dgram 模块

# HTTP 模块

Node 的 HTTP 模块包含对 HTTP 处理的封装,在 Node 中 HTTP 服务器继承自 TCP 服务器 (net 模块), 他可以与多个客户端保持连接,由于其采用事件驱动的形式,并不会为每一个连接创建额外的线程或者进程,保持很低的内存占用,所以能实现高并发

  • 开启 keepalive 之后,一个 TCP 会话可以用于多次请求和响应
# HTTP 服务的事件

服务器也是一个 EventEmitter 实例,具有以下事件:

  • connection 事件:这个连接可能因为开启了 keep-alive, 可以在多次请求响应之间使用,当这个连接建立时,服务器触发一次 connection 事件
  • request 事件:解析出 HTTP 请求头之后,将会触发该事件,在 res.end 之后,TCP 连接可能将用于下一次请求响应
  • close 事件:与 TCP 服务器行为一致,调用 server.close 方法传递一个回调函数来快速注册该事件
  • checkContinue 事件和 connect 事件等

# WebSocket 服务

  • WebSocket 实现了客户端与服务器端之间的长连接,而 Node 事件驱动的方式十分擅长与大量的客户端保持高并发连接
  • 相比于 HTTP,WebSocket 更接近于传输层协议,它并没有在 HTTP 的基础上模拟服务器端的推送,而是在 TCP 上定义独立地协议
  • WebSokcet 协议主要分为两个部分:握手和传输数据
  • 客户端将会校验 Sec-WebSocket-Accept 的值,如果成功将会开始接下来的数据传输
  • 为了安全考虑,客户端需要对发送的数据帧进行掩码处理,服务器一旦收到无掩码帧 (比如中间拦截破坏), 连接就会关闭
  • 基于事件驱动的方式使得 Node 对于 WebSocket 这类长连接的应用场景可以轻松地处理大量并发请求

# 网络服务与安全

  • SSL: 安全套接层 SSL 作为一种安全协议,他在传输层提供了对网络连接加密的功能,对于应用层而言,它是透明的,最初的 SSL 应用在 Web 上,被服务端和浏览器端同时支持,随后被标准化为 TLS (安全传输层协议)
  • Node 在网络安全上提供了 3 个模块:crypto、tls 和 https, 其中 crypto 主要用于加密解密,真正用于网络的是 tls 模块和 https 模块
# TLS/SSL

TLS 和 SSL 是一个公钥 / 私钥的结构,他是一个非对称的结构,每个服务器端和客户端都有自己的公钥和私钥,公钥用于加密要传输的数据,私钥用于解密接收到的数据

  • 公私钥的非对称加密性虽好,但是网络中依然可能存在窃听的情况,比如:中间人攻击
  • 为了解决这个问题,TLS/SSL 引入了数字证书来进行认证
  • CA (数字证书认证中心) 的作用就是为站点颁发证书
  • 通过 CA 机构颁发证书通常是一个繁琐的过程,需要付出一定的精力和费用,服务器端需要向 CA 机构进行申请

# 中间件

中间件的作用是用于简化和隔离基础设施与业务逻辑之间的细节,让开发者能够关注在业务的开发上,以达到提升开发效率的目的

  • 中间件的意义在于:封装了底层细节,为上层提供更方便的服务

# Node 多进程

  • 如何充分利用多核 CPU 一直是服务端关心和探讨的话题之一。

# child_process

  • spawn: 启动一个子进程来执行命令
  • exec: 启动一个子进行执行命令,他有一个回调函数获知子进程的状况
  • execFile: 启动子进程执行可执行文件
  • fork:fork 创建的 Node 子进程只需要指定要执行的 JavaScript 文件模块即可

# Cluster 模块

为了创建单机 Node 集群,只使用 child_process 的话有许多细节需要进行处理

# 原理

cluster 模块就是 child_process 和 net 模块的组合应用,当 cluster 启动时,他会在内部启动 TCP 服务器,当 cluster.fork 子进程时,会将这个 TCP 服务器端 socket 的文件描述符发送给工作进程,如果进程是通过 cluster.fork 复制出来的,那么它的环境变量中就存在 NODE_UNIQUE_ID, 如果工作进程中存在 listen 侦听网络端口的调用,它将拿到文件描述符,通过端口重用实现多个子进程共享端口

# 进程间通信

# 浏览器端
  • 浏览器端 WebWorker 允许创建工作线程并在后台运行,使得一些阻塞较为严重的计算不会影响主线程上的 UI 渲染
  • 主线程通过 onmessage 和 postMessage 进行发送和接收数据,子进程对象通过 send 发送数据,通过 message 事件接收数据
# 进程间通信原理

IPC 全称为进程间通信,进程间通信的目的是为了让不同的进程能够互相访问资源并进行协调工作

# 实现进程间通信的技术
  • 命名管道、匿名管道 (文件描述符)
  • socket (通用通信接口)
  • 信号量 (进程间通过发布订阅的模式监听事件)
  • 共享内存 (映射同一内存地址)
  • 消息队列等等 (一个进程创建消息队列,另一个进程向其消息队列发送消息)

Node 中实现 IPC 通道的是管道技术,这种技术很适合父子进程之间的状态通信

# Node 中 IPC 通道

Node 中父进程在创建子进程之前,会创建 IPC 通道并监听它,然后才真正创建出子进程,并通过环境变量告诉子进程这个 IPC 通道的文件描述符

# 后端技术方案

  • 为了解决性能问题和 Session 数据无法跨进程共享的问题,常常将 Session 集中化,将原本可能分散在多个进程里的数据,同意转移到集中的数据存储中,目前常用的工具是 Redis、Memcached 等,通过这些高效的缓存,Node 进程无须在内部维护数据对象,垃圾回收问题和内存限制问题都可以迎刃而解,并且这些高速缓存设计的缓存过期策略更加合理更加高效,比在 Node 中自定设计缓存策略更好
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

dmq 微信支付

微信支付

dmq 支付宝

支付宝