编程解决方案:回调函数Node

var querystring = require(‘querystring’);

//监听服务器的请求事件

http.createServer(function (req, res) {

var postData = ”;

req.setEncoding(‘utf8’);

// 监听请求的数据事件

req.on(‘data’, function (trunk) {

postData += 主干;

});

// 监听请求的结束事件

req.on(‘end’, function () {

res.end(postData);

});

}).listen(8080);

console.log(‘服务器启动完成’);

发出请求后,只需要关心请求成功时执行相应的业务逻辑即可。

要求({

网址:’/网址’,

方法:’POST’,

数据: {},

成功:函数(数据){

// 成功事件

}

});

事件的编程方式具有轻量级、松耦合、只关注事务点的优点。但是,在多个异步任务的场景下,事件和事件是相互独立的。如何配合是个问题,未来也出现了一系列异步任务。编程解决方案:

“打回来”

单线程

Node 在浏览器中保持了 JavaScript 的单线程特性

JavaScript 和其他线程不能共享任何状态。最大的好处就是不用像多线程编程那样担心状态的同步。性能开销

跨平台

起初,Node 只能在 Linux 平台上运行。如果你想在Windows平台上学习和使用Node,你必须通过Cygwin/MinGW。后来微软投资实现了基于libuv的跨平台架构。

在操作系统和Node上层模块系统之间搭建了一个平台架构

有了好的架构,Node 的第三方 C++ 模块也可以和 libuv 跨平台

节点模块机制——CommonJS

背景:

在其他高级语言中,Java 有类文件,Python 有导入机制,Ruby 有 require,PHP 有 include 和 require。JavaScript 通过 script 标签引入代码的方式很混乱。为了安全性和易用性,必须人为地使用名称空间等来约束代码。

直到后来 CommonJS 出现…

想象

希望 JavaScript 可以在任何地方运行

出发点

对于 JavaScript 本身来说,它的规范还很薄弱,存在以下缺陷:

CommonJS 的提出主要是为了弥补当前 JavaScript 标准的不足,从而达到开发 Python、Ruby、Java 等大型应用程序的基本能力,而不是停留在小脚本程序的阶段,希望使用 JavaScript发展:

CommonJS 规范涵盖:

Node 与浏览器的关系,以及 W3C 组织、CommonJS 组织、ECMAScript 共同构成了一个蓬勃发展的生态系统

模块规格

上下文提供了exports对象,用于导出当前模块的方法或变量,是export的唯一export

在模块中,还有一个模块对象,代表模块本身,exports是模块的属性

在 Node 中,文件是一个模块,您可以通过将导出方法作为属性挂载到导出对象上来定义导出方法。

// 数学.js

出口.add = 函数(a,b){

返回 a + b;

}

const math = require(‘./math’);

const res = math.add(1, 1);

控制台.log(res);

// 2

在 CommonJS 规范中,有一个 require 方法,它接受一个模块标识符来将一个模块的 API 引入到当前上下文中

模块标识符是传递给 require 方法的参数,可以是:

模块的定义很简单,接口也很简单

每个模块都有独立的空间,互不干扰,引用时显得干净利落

将聚类方法和变量限制在私有范围内,支持导入导出功能,平滑连接上下游依赖

模块实现

在Node中引入一个模块,需要经过以下三个步骤

Node中的模块分为两类:

在编译过程中,二进制可执行文件被编译成

Node进程启动时,部分核心模块是直接加载到内存中的,所以在引入这部分核心模块时,可以省略文件定位和编译执行这两个步骤,在路径分析中优先考虑,所以它的加载速度是最快的。

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

首先从缓存中加载

正如浏览器缓存静态脚本文件以提高性能一样,Node 两次缓存导入的模块以减少二次引入的开销。区别在于:

无论是核心模块还是文件模块,require方法都会一直使用cache-first方式对同一个模块进行二次加载。

路径分析和文件定位

“标识符分析(路径)”

如前所述,require方法接受一个参数作为标识符,分为以下几类:

优先级仅次于缓存加载。在Node的源码编译过程中已经编译成二进制代码,加载过程是最快的。

“注意:加载与核心模块具有相同标识符的自定义模块不会成功,只能通过选择不同的标识符/切换路径来实现”

以 ./ 和 ../ 开头的标识符被视为文件模块

require方法会将路径转换为真实路径,并以真实路径为索引,将编译执行的结果存储在缓存中,使二次加载更快

文件模块向Node指示了准确的文件位置,因此在搜索过程中可以节省大量时间,而且它的加载速度只比核心模块慢

是一个特殊的文件模块,以文件或包的形式

这种类型的模块查找是最耗时的,也是最慢的

我们先介绍一下模块路径的概念,这也是定位文件模块时制定的搜索策略,具体表现为路径数组。

[‘/home/bytedance/reasearch/node_modules’,

‘/home/bytedance/node_modules’,

‘home/node_module’, /node_modules’]

可以看出,规则如下:

它的生成方式与查找 JavaScript 原型链/作用域链的方式非常相似

在加载过程中,Node 会一一尝试模块路径中的路径,直到找到目标文件

文件路径越深,模块搜索越耗时,这也是自定义模块加载速度最慢的原因

“文件位置”

要求解析标识符将出现没有文件扩展名

扩展名会按照.js、.json、.node的顺序进行补充,一次尝试

过程中需要调用fs模块同步阻塞判断文件是否存在。因此节点单线程会导致性能问题。

如果是带扩展名的.node/.json文件,可以加快速度,配合缓存机制,可以大大缓解Node单线程阻塞调用的缺陷

在解析标识符的过程中,可能找不到文件,但是得到了一个目录,该目录会被当作一个包处理

通过将 package.json 文件解析为包的 main 属性指定的文件名

如果main对应文件解析错误/没有package.json文件,node会使用index作为文件名

查找 index.js index.json index.node 一次

如果目录没有定位成功,会搜索下一个模块路径

如果在遍历模块路径数组之前没有找到目标文件,则抛出异常

模块编译

在 Node 中,每个文件模块都是一个对象

功能模块(ID,父){

这个.id = id;

this.exports = {};

this.parent = 父级;

如果(父母&&父母。孩子){

parent.children.push(this);

}

this.filename = null;

this.loaded = false;

this.children = [];

}

每个编译成功的模块都会将其文件路径作为索引存储在 Module.cache 对象上,以提高二次引入的性能

包和 npm

Node把自己的核心模块组织起来,也让第三方文件模块能够有序的编写和使用

但是在第三方模块中,模块仍然到处都是hash,不能直接相互引用。

在模块之外,包和 NPM 是一种将模块链接在一起的机制

一定程度上解决了变量依赖、依赖关系等代码组织问题

封装结构

该包实际上是一个归档文件,即直接将一个目录打包成一个.zip/tar.gz格式的文件,安装后解压恢复到一个目录下。

包描述文件

包.json

CommonJS 为 package.json 定义了以下必填字段

包规范的定义可以帮助Node解决依赖包安装的问题线程标识符 有什么用,NPM就是基于这个规范实现的

NPM 常用函数

CommonJS 包规范是理论,NPM 是实践之一

NPM在Node中,相当于Ruby中的gem和PHP中的pear

帮助完成了第三方模块的发布、安装和依赖等。

查看帮助安装依赖项

npm 安装 {packageName}

执行该命令后,npm会在当前目录下的node_modules目录下创建一个包目录,然后将对应的包解压到该目录下

npm install {packageName} -g

全局模式并不意味着将模块包安装为全局包,这并不意味着它可以从任何地方reuqire

全局模式这个名字是不准确的,-g实际上是一个执行命令,将一个包安装为全局可用

它根据包描述文件中的bin字段配置将实际脚本链接到与Node可执行文件相同的路径

对于一些没有在 NPM 上发布的包,或者由于网络原因无法直接安装的包

可以在本地下载包,然后在本地安装

npm 安装

npm 安装

npm 安装文件夹>

如果不能从官方源安装,可以从镜像源安装

npm install –registry={urlResource}

如果在使用过程中几乎使用了所有镜像源,可以指定默认源

npm 配置设置注册表 {urlResource}

npm 钩子命令

package.json 中的 scripts 字段的提议是为了让包在安装或卸载时提供一个钩子机制。

“脚本”:{

“preinstall”: “preinstall.js”,

“安装”:“安装.js”,

“卸载”:“卸载.js”,

“测试”:“test.js”,

}

本地 npm

企业的局限在于,一方面需要享受模块开发带来的低耦合和项目组织的好处线程标识符 有什么用,但另一方面需要考虑模块保密的问题。因此,通过 NPM 共享和发布存在潜在风险。

为了同时享受 NPM 上的众多包,同时对自己的包保密和限制,现有的解决方案是企业建立自己的 NPM 仓库。NPM 的服务器和客户端都是开源的。

本地 NPM 仓库的构建方法与构建镜像站的方式几乎相同。与镜像仓库的区别在于可以选择不同步官方源仓库中的包

异步 I/O

为什么需要异步 I/O?

浏览器中的 JavaScript 在单个线程上执行,它还与 UI 渲染共享一个线程。如果脚本执行时间超过100ms,用户会觉得页面卡住了

如果一个网页暂时需要获取网络资源,并以同步的方式获取,JS需要等待从服务器完全获取资源,才能继续执行。在此期间,UI 将停止并且不会响应用户交互。可以想象这样的用户体验会有多糟糕。

使用异步请求,JavaScript和UI的执行不会处于等待状态,给用户一个全新的页面

I/O 很贵,分布式 I/O 更贵

只有后端能够快速响应资源,才能提升前端体验

在计算机开发过程中,组件被抽象出来,分为I/O设备和计算设备

假设业务场景有一组不相关的任务需要完成,主流的方法有两种:

1.多线程并行完成

多线程的成本是创建线程和执行线程上下文切换的开销。

在复杂的业务中经常会遇到锁和状态同步等问题。但是,多线程可以有效提高多核 CPU 上的 CPU 利用率

2.单线程串行执行

任务的单线程顺序执行更符合程序员的顺序思维方式,仍然是主流的编程方式

串行执行的缺点是性能,任何稍微慢一点的任务都会导致后续执行代码被阻塞

在计算机资源中,通常 I/O 和 CPU 计算可以并行化。同步编程模型带来的问题是I/O的进行会导致后续的任务等待,导致资源不能得到更好的利用。

节点有它的答案介于两者之间

使用单线程,避免多线程死锁、状态同步等问题;

充分利用异步I/O,让单线程远离阻塞,更好的利用CPU

为了弥补单线程无法利用多核CPU的劣势,Node在前端浏览器中提供了类似Web Workers的子进程,可以通过worker进程高效利用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返回后,CPU的时间片可用于处理其他事务,此时性能提升明显

存在的问题:

主要轮询技术

最原始、性能最低的一种,通过反复调用检查I/O状态来完成数据的读取

CPU一直在等待,直到获得最终数据

是基于read的改进方案,在文件描述符上判断事件状态

限制:需要一个1024长度的数组来存储状态,最多可以同时检查1024个文件描述符

与select相比,通过使用链表进行了改进,避免了数组长度的限制,二来可以避免不必要的检查

当有很多文件描述符时,它的性能仍然很低

该方案是Linux下最高效的I/O事件通知机制。如果进入轮询时没有检测到 I/O 事件,它将休眠直到事件唤醒它。真正使用了事件通知和执行回调的方式,而不是遍历查询,所以不会浪费CPU,执行效率高

非阻塞异步 I/O 的理想选择

epoll虽然使用了时间来降低CPU消耗,但是睡眠时CPU几乎是有限的,对于当前线程来说利用率还不够

完美的异步 I/O 应该是应用程序在不通过遍历或时间唤醒进行轮询的情况下发起非阻塞调用的地方

可以直接处理下一个任务,只需在 I/O 完成后通过信号或回调将数据传递给应用程序

Linux下原生提供的一种异步I/O方法(AIO)是通过信号或回调的方式传输数据

缺点:

现实的异步 I/O

通过让部分线程进行阻塞I/O或非阻塞I/O加轮询技术完成数据获取,让一个线程进行计算处理,通过线程间通信将I/O得到的数据传输出去,很容易实现异步I /O 已实现

Node 的异步 I/O

Node用事件循环、观察者和请求对象等完成整个异步I/O链接。

事件循环

强调Node自己的执行模型——事件循环

当 Node 进程启动时,会创建一个类似 while(true) 的循环

每个循环体的过程称为Tick,每个Tick的过程就是检查是否有事件需要处理

获取事件及其关联的回调函数(如果有)并执行它们

观察者

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

概括

事件循环、观察者、请求对象、I/O线程池共同构成了NOde异步I/O模型的基本要素

既然我们知道JavaScipt是​​单线程的,尝试一下就很容易理解了,它不能充分利用多核CPU

事实上,在 Node 中,除了 JavaScript 是单线程的,Node 本身其实也很喜欢昵称,只是 I/O 线程占用的 CPU 更少

还有一点需要注意的是所有的I/O都可以并行执行,除了不能并行执行的用户代码

注:图为Node整个异步I/O流程

事件驱动和高性能服务器

前面对异步的解释也基本概括了事件驱动的本质,即通过主循环运行程序加事件触发

以下是几种经典的服务器模型:

事件驱动带来的效率逐渐开始被业界看好

著名的服务器 Nginx 也抛弃了多线程的方式,采用了和 Node 一样的事件驱动方式。

不同的是,Nginx是纯C写的,性能很高,但只适合做web服务器,用于反向代理或者负载均衡服务,业务处理方面比较欠缺。

Node是一个高性能的平台,可以用来搭建和Nginx一样的功能,也可以处理各种具体的服务

Node在web服务器方面不如Nginx专业,但场景更大,自身性能也不错

在实际项目中可以结合各自的优势,达到应用的最佳性能

JavaScript 在服务器端几乎是空白,所以 Node 没有任何历史包袱,而 Node 在性能优化上的表现,一下子在社区中火了起来~

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论