Node_深入浅出Node
NodeJs简介
Ryan Dahl项目命名为:web.js 就是一个Web服务器.
单纯开发一个Web服务器的想法,变成构建网络应用的一个基本框架.
Node发展为一个强制不共享任何资源的单线程,单进程系统。
每一个Node进程都构成这个网络应用中的一个节点,这是它名字所含意义的真谛。
Node的诞生历程
2009年3月,Ryan Dahl在博客宣布并创建
2009年5月,在GitHub上发布最初的版本
2009年12月和2010年4月,两届JSConf大会安排了Node的讲座
2010年底,Ryan Dahl加入Joyent全职负责Node的发展
2011年7月,发布Windows版本
2011年11月,成为GitHub上面关注度最高的项目
2012年1月底,Ryan Dahl 将掌门人身份交给NPM的作者Issac Z.Schlueter
2013年7月,发布稳定版V0.10.13
随后,Node的发布计划主要集中在性能上面,V0.14后正式发布V1.0版本
选择JavaScript
高性能(chrome的V8引擎的高性能)
符合事件驱动(JavaScript在浏览器中有广泛的事件驱动方面的应用)
没有历史包袱(为其导入非阻塞的I/O库没有而外阻力)
Node给JS带来的意义
Node结构与Chrome十分相似,基于事件驱动的异步架构
Node中JS可以访问本地文件,搭建服务器,连接数据库
Node打破了过去JS只能在浏览器中运行的局面,前后端编程环境统一
Node特点
异步I/O
事件与回调函数
单线程
child_progress:解决单线程中大量算量的问题
Master-Worker:管理各个工作进程
跨平台:兼容Windows和*nix平台
构建异步I/O,从文件读取到网络请求。
可以从语言层面很自然的进行并行I/O 操作。每个调用之间无序等待之前I/O调用结束。
事件编程方式:轻量级,松耦合,只会关注事务点。
单线程弱点:
无法利用多核CPU
错误会引起整个应用退出,应用的健壮性
大量计算占用CPU导致无法继续调用异步I/O。
浏览器中JavaScript与UI公用一个线程,JavaScript长时间执行会导致UI的渲染和响应被中断。
在Node中,长时间占用CPU到孩子后续的异步I/O发不出调用,已经完成的异步I/O的回调函数也会得不到执行。
解决:
child_progress:解决单线程中大量算量的问题
Master-Worker:管理各个工作进程 (管理子进程)
启用一个完全独立的进程,将需要计算的程序发送给进程。通过时间将结果传递回来。(消息传递的方式来传递运行结果)
采用消息传递的方式: 保持应用模型的简单和低依赖。
Node的应用场景
- I/O密集型
面向网络且擅长并行I/O,能够有效的组织起更多的硬件资源。利用事件循环的处理机制,资源占用极少。
- 不是很擅长CPU密集型业务,但是可以合理调度
通过编写C/C++扩展的方式更高效的利用CPU
- 与遗留系统问题和平共处
LinkeDin, 雪球财经
- 分布式应用
阿里的数据平台,对Node的分布式应用分布式应用要求:对可伸缩性要求高。具体应用:中间层应用NodeFox,ITer,将数据库集群做了划分和映射,查询调用一句是针对单张表进行SQL查询,中间层分解查询SQL,并行的去多态数据库中获取数据并合并。NodeFox作用:实现对多台MySQL数据的查询ITer作用:查询多个数据库(指的是不同数据库,MySQL,Oracle等)
Node的使用者
前后端编程语言环境统一:雅虎开放了Cocktail框架
Node带来的高性能的I/O用于实时应用:Voxer和腾讯
Voxer:实时语音腾讯:Node应用在长连接,实时功能
- 并行I/O使得使用者可以更高效地利用分布式环境:阿里巴巴和eBay
利用Node并行I/O的能力,更高校的使用已有的数据
并行I/O,有效利用稳定接口提升Web渲染能力:雪球财经和LinkedIn
云计算平台提供Node支持
游戏开发领域:网易的pomelo实时框架
工具类应用
模块机制
Node的模块机制
模块在引用过程中的编译,加载规则
CommonJs规范为JavaScript提供了一个良好基础,JavaScript能够在任何地方运行。
CommonJS规范
规范涵盖:模块,二进制,Buffer,字符集编码,I/O流,进程环境,文件系统,套接字,单元测试,Web服务器网管接口,包管理
JavaScript规范缺陷
没有模块系统
标准库较少
没有标准接口
缺乏包管理系统
Node借鉴CommonJS的Modules规范实现了一套非常易用的模块系统
NPM对Packages规范的完好支持使得Node在应用开发过程中更加规范
CommonJS的模块规范
模块应用
var math = require('math');
在CommonJs规范中,存在require();方法,这个方法接收模块标识,以此引入一个模块的API到当前上下文中。
模块定义
require(): 引入外部模块。
exports对象: 导出当前的方法或者变量,而且是唯一导出的出口
module对象:表示自身模块,而exports是module的属性。
Node中,一个文件就是一个模块,将方法挂载到exports对象作为属性定义导出方式.
模块标识
传递给require() 方法的参数,必须符合小驼峰命名的字符串,或者以 .,..开头的相对路径,或者绝对路径。 js文件可以没有后缀.js
模块的意义:
将类聚的方法和变量等限定在私有的作用域中,同时支持引入和导出的功能顺畅的连接上下游依赖。
Node模块的实现
Node中引入模块的步骤:
1:路径分析
2:文件定位
3:编译执行
模块分类
核心模块:Node提供的模块
文件模块:用户编写的模块
核心模块在Node源代码的编译过程中,编译进了二进制执行文件。
Node进程启动时,部分核心模块就直接被加载近内存中。
文件模块是在运行时动态加载,需要完成的路径分析,文件定位,编译执行的过程。
模块加载过程
优先从缓存加载
Node对引入过的模块都是进行缓存,减少二次引入时的开销。(Node缓存是编译和执行之后的对象)
不论是核心模块还是文件模块,require()方法对同模块的二次加载都采用缓存优先的方式。 不同之处,核心模块的缓存检查优先于文件模块的缓存检查
路径分析和文件定位
○ 模块标识符分析
核心模块
路径形式的文件模块
自定义模块
模块标识符分类:
核心模块:http,fs,path 等
.,..开始的相对路径的文件模块
以/开始的绝对路径文件模块
非路径形式的文件模块(自定义模块)
核心模块
核心模块的优先级仅此于缓存加载,在Node的源码编译过程中,已经编译为二进制代码,其加载过程最快。
如果想加载和核心模块标识相同的模块,必须选择修改标识或换用路径的方式。
路径形式的文件模块
.,..开始的相对路径的文件模块。require();方法会将路径转为真实路径,并以真实路径作为索引,将编译执行后的结果存放入缓存中。
文件模块指明了确切的文件位置,在查找过程中节约大量时间,其加载速度慢于核心模块。
自定义模块
特殊的文件模块,可能是一个文件或包的形式。这类模块查找最费时,也是所有方式最慢的一种。
模块路径:Node在定位文件模块的具体文件时定制的查找策略。表现为一个路径组成的数组。
node_modules 会按照类似JavaScript的原型链查查找方式,在加载过程中,Node会租个尝试模块路径中的路径。直到找到文件为止。(文件路径越深,模块查找耗时越多)
○ 文件定位
文件扩展名分析(.js,.node,.json次序补足扩展名)
目录分析和包
文件扩展名分析
require(); 在分析标识符的过程中,标识符不含有文件扩展名的情况,Node会按照 .js,.node,.json 补足扩展名,以此尝试
尝试过程:需要调用fs模块同步阻塞式判断文件是否存在。
因为Node是单线程,这边会引起性能问题。
避免方法:
方法1:如果是.node 和 .json文件,标识符加上扩展名
方法2:同步配合缓存,可以大幅度缓解Node中阻塞式调用的缺陷
目录分析和包
在分析文件扩展名之后,没有找到对应的文件,但却得到一个目录。当作一个包来处理。
Node在当前目录下查找package.json(CommonJS包规范定义的包描述文件),通过JSON.parse();解析出包描述对象,从中取出main属性指定的文件名进行定位。如果文件名缺少扩展名,将会进去扩展名分析的步骤。
如果main属性指定的文件名错误,或者更没有package.json的文件,Node会将index当作默认文件名,然后一次查找index.js, index.node , index.json
如果在目录分析的过程中没有定位成功任何文件,则自定义模块进入下一个模块路径进行查找。如果模块路径数组都遍历完毕,依然没有查找到目标文件,则会抛出查找失败的异常。
○ 编译模块
.js文件。通过fs模块同步读取文件后编译执行
.node文件
用C/C++编写的扩展文件通过dlopen()方法加载最后编译生成的文件
- .json文件
通过fs模块同步读取文件后,用JSON.parse()解析返回结果
- 其它扩展名文件。它们被当作.js文件载入
在Node中,每个文件模块都是一个对象。
每一个编译成功的模块都会将其目录作为索引缓存在Module._cache对象上.
JavaScript模块的编译
在编译过程中,Node对获取的JavaScript文件内容进行了头尾包装。
(function ( exprots, require, moduel, __filename, __dirname ) { });
包装之后,对每个文件之间进行了作用域隔离,包装之后的代码会通过vm原生模块的runIntThisContext();方法执行 (类似eavl,只是具有明确上下文,不污染全局). 返回一个具体的function 对象。 最后,将当前模块对象的exports属性,require()方法,module[模块对象自身],以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个funciton() 执行。
在执行之后,模块中的exports 属性被返回给了调用方。exports属性上的任何方法和属性在外界都可以被调用得到。
有了exports的情况下,为何还存在module.exports.
给exports重新赋值,exports对象是通过形参的方式传入的,直接赋值会改变形参的引用,但是不能改变作用域外的值
核心模块
Node的核心模块在编译成可执行文件的过程中编译近了二进制文件。
核心模块:
C/C++编写,存放在Node项目中src目录下
JavaScript编写,存放在lib目录下
JavaScript核心模块的编译过程
转存为C/C++代码
编译为JavaScript核心模块
转存为C/C++代码
Node采用V8的js2c.py工具,将所有内置的JavaScript代码('src/node.js 和 lib/*.js') 转换成C++里的数组,生成node_natives.h头文件
启动Node进程时,JavaScript代码直接加在进内存中。在加载过程中,JavaScript核心模块经历标识符分析后直接定位到内存中,比普通的文件模块从磁盘从查找快很多。
编译为JavaScript核心模块
JavaScript核心模块与文件模块区别:
获取源代码的方式(核心模块是从内存中加载的)以及缓存执行结果的位置。
源文件通过process.bingding('natives'); 取出,编译成功的模块缓存到NativeModuel._cache对象上。 文件模块缓存到Module._cache
function NativeModule(id) { this.filename = id + '.js'; this.id = id; this.exports = {}; this.loaded = false;}NativeModule._source = process.binding('natives');NativeModule._cache = {};
C/C++核心模块的编译过程
核心模块中,有些模块全部有C/C++编写,有些模块则由C/C++完成核心部分,其它部分由JavaScript实现包装或向外到处。
内建模块: 纯C/C++编写的部分
JavaScript主外实现封装的模式是Noe能够提高性能的常见方式.
Node的buffer,crypto,evals,fs,os等模块都是部分通过C/C++编写 (不直接被用户调用)
内建模块的组织形式
每一个内建模块定义之后,都通过NODE_MODULE宏将模块定义到node命名空间中。
内建模块的导出
在Node的所有模块类型中,存在依赖关系。
一般的,不推荐文件模块直接调用内建模块。如需调用,直接调用核心模块。
因为:核心模块中基本都封装了内建模块。
Node在启动时,会生成一个全局变量process,并提供Binding()方法谢祖加载内建模块。
转为C/C++数组存储,通过process.binding('natives'); 取出防止NativeModule.source中.
在加载内建模块时,先创建exports空对象,然后调用get_builtin_module() 方法取出内建模块对象,通过执行resister_func()填充exports 对象,最后将exports对象安模块名缓存,并返回给调用方完成导出。
核心模块的引入流程
1.NODE_MODULE(node_os,reg_func)
2.get_builtin_module("node_os")
3.process.binding("os")
4.NativeModule.require("os")
5.require("os")
编写核心模块
前提条件:
GYP项目生成工具
V8引擎C++库
libuv库
Node内部库
其他库,zlib、openssl、http_parser等
C/C++扩展模块的编写
C/C++扩展模块的编译
C/C++扩展模块的加载
C/C++ 内建模块属于最底层的模块,属于核心模块,主要提供API给JavaScript核心模块和第三方JavaScript文件模块调用。
JavaScript核心模块作用:
1: 作为C/C++内建模块的封装层和桥接层,供文件模块调用
2: 纯粹的功能模块,不需要跟底层交流,但有很重要。
文件模块通常由第三方编写,包括普通JavaScript模块和C/C++扩展模块,主要调用方向为普通JavaScript模块调用扩展模块。
包与NPM
包结构
package.json:包描述文件
bin:用于存放可执行二进制文件的目录
lib:用于存放JavaScript代码的目录
doc:用于存放文档的目录
test:用于存放单元测试用例的代码
包描述文件与NPM
必需字段:name,description,version,keywords,maintainers
必需字段:contributios,bugs,licences,repositories,dependencies
dependencies: 使用当前包所依赖的包列表,NPM会通过这属性帮助自动加载依赖的包
NPM常用功能
查看帮助: npm -v
安装依赖包
最常见: npm install express
全局安装:npm install express -g
从本地安装:npm install
从非官方源安装:npm install underscore --registry=http://registry.url
NPM钩子命令
package.json中的script字段:让包在安转或者卸载过程中提供钩子机制
"scripts": { "preinstall": "preinstall.js", "install": 'install.js', "uninstall": 'uninstall.js', "test": "test.js"}
在执行npm install 时, preinstall指向的脚本将会被加载执行,然后install执行的脚本会被执行。在执行npm install 时,unstall指向的脚本也许会做一些清理工作。
NPM潜在问题
NPM平台上面包质量良莠不齐
Node代码可以运行在服务端,需要考虑安全问题
前后端共用模块
模块的侧重点
Node的模块引入过程,几乎全部都是同步。
AMD规范
AMD规范:是CommonJS模块规范的一个延伸
//通过数组引入依赖 ,回调函数通过形参传入依赖define(['someModule1', ‘someModule2’], function (someModule1, someModule2) { function foo () { /// someing someModule1.test(); } return {foo: foo}});
CMD规范
CMD规范:玉伯提出,区别定义模块和依赖引入
//CMDdefine(function (requie, exports, module) { //依赖 就近书写 var a = require('./a'); a.test(); //软依赖 if (status) { var b = requie('./b'); b.test(); }});
1.对于依赖的模块AMD是提前执行,CMD是延迟执行。RequireJS从2.0开始,也改成可以延迟执行(根据写法不同,处理方式不通过)。
2.CMD推崇依赖就近,AMD推崇依赖前置。
UMD规范
兼容多种模块规范(Universal Module Definition)
UMD判断是否支持Node.js的模块(exports)是否存在且module不为undefiend,存在则使用Node.js模块模式。
判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。
(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. define(['exports'], factory); } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') { // CommonJS factory(exports, require('echarts')); } else if ( typeof module !== 'undefined' module.exports ) { // 普通Node模块 module.exports = factory(); } else { // Browser globals factory({}, root); }}(this, function (exports) { //module ...}));
异步I/O
事件循环是异步实现的核心,与浏览器中的执行模型基本保持一致。
古老的Rhino,是较早服务器上运行JavaScript,但是执行模型并不像浏览器采用事件驱动.而是使用其它语言一样采用同步I/O作为主要模型.
为什么要异步I/O
用户体验,消耗时间为max(M,N)
资源分配,让单线程远离阻塞,更好利用CPU
用户体验
异步概念: 浏览器中JavaScript在单线程上执行,还与UI渲染共用一个线程。 表示JavaScript在执行的时候UI渲染和响应是处于停止状态
资源分配
单线程同步会因为阻塞I/O导致硬件资源的不到更优使用。多线程编程中的死锁,状态同步等问题。
选择: 利用单线程,原理多线程死锁,状态同步等问题,利用异步I/O,让单线程原理阻塞,更好的使用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操作来确认是否完成。这种重复调用判断操作是否完成的叫做轮询;
轮询: CPU决策如何提供周边设备服务的方式。 又称为 “程控输出入” (Programmed I/O)。
轮询法: 由CPU定时发出询问,依次询问每一个周边设备是否需要其服务,有即给予服务。服务结束再询问下一个周边,重复询问。
非阻塞I/O缺点:轮询去确认是否完全完成数据获取,会让CPU处理状态判断,是对CPU资源的浪费。需要减小I/O状态判断的CPU损耗.
轮询技术
read
select
poll
epoll
kqueue
read : 重复调用来检查I/O的状态来完成完整数据的读取。
缺点:性能最低
select : 文件描述符上的事件状态来进行判断
缺点:最多可同时检查1024个文件描述符.
poll : 链表的方式避免数组长度的限制,能避免不需要的检查
缺点:文件描述符较多,性能低下。
epoll : I/O事件通知机制。
进入轮询的时候如果没有检测到I/O事件,将会进行休眠,知道事件发生将它唤醒。
事件通知,执行回调的方式。而不是遍历查询。
特点:不会浪费CPU,执行效率较高。
kqueue : 实现方式和epoll类似,存在FreeBSD系统下。
轮询缺点:
轮询对于应用程序,仍然是一种同步,应用程序让然需要等待I/O完全返回,依旧花费很多时间来等待。等待期间CPU要么用于遍历文件描述符的状态,要么用户休眠等待事件发生。
理想的非阻塞异步I/O
应用程序发起非阻塞调用,无需通过遍历或者事件唤醒等方式轮询,可以直接处理下一个任务。
在I/O完成后通过信号或回调将数据传递给应用程序。
现实中的异步I/O
*nix平台下采用libeio配合libev实现I/O部分
windows平台采用IOCP是实现异步I/O
部分线程阻塞I/O 或者 非阻塞I/O + 轮询技术 -> 完成数据获取。
一个线程计算处理
通过线程之间的通信将I/O得到的数据进行传递。
IOCP: 调用异步方法,等待I/O完成之后的通知,执行回调,用户无序考虑轮询。(实现原理:线程池原理)
*nix将计算机抽象:磁盘文件,硬件,套接字,等几乎所有计算机资源抽象为文件。
在Node中,无论是*nix还是Windows平台,内部完成I/O任务的另有线程池。
Node的异步I/O
事件循环
观察者
请求对象
执行回调
事件循环,观察者,请求对象,I/O线程池共同构成Node异步I/O模型的基本要素。
事件循环
Node 自身的执行模型 -- 事件循环。
在进程启动时,Node会穿件一个类似于while(true)的循环,每执行一次循环体的过程 称之为: Tick(标记,打勾) .每个Tick的过程就是查看是否有事件待处理,如果有,就取出事件及其相关的回调函数。如果存在关联的回调,就执行它们。然后进入下个循环,如果不再有事件处理,就退出进程。
观察者
每个事件循环中有一个或者多个观察者,而判断是否有事件要处理的过程就是想这些观察者询问是否有要处理的事件。
浏览器类似事件观察机制: 时间可能来自用户的点击或者加载某些文件时产生,而这些产生的事件都有对应的观察者。
事件循环是一个典型的生产者/消费者模型,$watch $digest 机制。 异步I/O,网络请求则是事件的生产者,远远不断为Node提供不同类型的事件,这些事件被传递到对应的观察者那里,事件循环则从观察则那里取出事件并处理。
请求对象
请求对象: 从JavaScript 发起调用到内核执行完I/O操作的过度过程中,存在一种中间产物.
请求对象是异步I/O过程中的重要中间产物,所有的状态都保存在这对象中,包括送入线程池等待执行以及I/O操作完毕后的回调处理。
Node中的异步I/O调用,回调函数不由开发者来调用。
从发出调用后,到回调函数被执行,中间发生了什么?
fs.open() 示例:
fs.open = function ( path, flags, mode, cb ) { binding.open(pathModule._makeLong(path), stringToFlags(flags), mode, cb); };
Node经典调用方式:从JavaScript调用Node的核心模块,核心模块调用C++内建模块,内建模块通过libuv进行系统调用。
调用的uv_fs_open() -> FsReqWrap (请求对象,作用:JavaScript层传入的参数和当前方法都封装对象中)
回调函数被设置在FsReqWrap.oncomlete_sym 属性上
reqwrap -> object -> Set(oncomlete_sym, callback);
包装完之后,Windows调用:QueueUserWorkItem() 将FsReqWrap对象推入线程池中等待执行
QueueUserWrokITem(&uv_fs_thread_proc, req, WT_EXTCUTEDEFAULT);
QueueUserWorkItem()
参数1:执行的方法引用 fs_open() 的引用 uv_fs_thread_proc
参数2:uv_fs_thread_proc() 方法执行时所需参数。
参数3:执行的标志。
调用完成之后,JavaScript调用立即返回,由JavaScript层面发起的异步调用的第一阶段结束。
JavaScript线程可以继续执行当前任务的后续操作。当前的I/O操作在线程池中等待执行,不管是否阻塞I/O,都不会影响到JavaScript线程的后续执行。
执行回调
回调通知,完成完整异步I/O的第二部分。
线程池中的I/O操作调用完毕后,会将结果存储在req -> reslut 属性上。然后调用 PostQueuedCompletionStatus(); 通知IOCP,告知当前对象操作已经完成:
PostQueuedCompletionStatus( (loop)->iocp, o, o, &(req)->overlappped )
PostQueuedCompletionStatus()作用: 向IOCP提交执行状态,并将线程归还给线程池。提交状态
GetQueuedCompletionStatus() 作用: 提取
每个Tick的执行中,会调用IOCP相关的GetQueuedCompletionStatus()方法检查线程池中是否有执行完的请求,如果存在,会将请求加入到I/O观察者的队列中,然后将其当作事件处理。
I/O观察者回调函数的行为: 取出请求对象的result属性作为参数,取出oncomlplete_sym属性作为方法,然后调用执行。以此达到调用JavaScript中传入的回调函数的目的。
关键字:JavaScript, node, node.js, javascript性能
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!