内容简介:本文基于2015年所写的一篇读书笔记整理,彼时NodeJS开源项目于2009年由Google Brain团队的软件工程师Ryan Dahl发起创建,后被美国云计算企业
本文基于2015年所写的一篇读书笔记整理,彼时 node.js 的版本号还停留在 v0.12.x
,社区也还未完成与 io.js 的最终合并,文中出现的部分API时至今日已经被废弃或者迁移。但是这些API层面的变化都可以对照 官方文档 找到相应说明以及替代API,总体上并不影响通过本文快速了解整套技术栈的特性。
NodeJS开源项目于2009年由Google Brain团队的软件工程师Ryan Dahl发起创建,后被美国云计算企业 Joyent 招入麾下,2015年后正式被 NodeJS基金会 接管,三星公司于2016年完成了对Joyent的收购。经过将近10年的发展,NodeJS已经成为现代化前端开发过程中不可或缺的基础架构,即可以作为页面渲染的分布式服务器,也可以作为前端自动化的宿主环境。
简单介绍
NodeJS是让JavaScript 运行在浏览器之外的平台,它实现了诸如文件系统、模块、包、操作系统API、网络通信等原生JavaScript没有或不完善的功能。并且内建了对HTTP服务器的支持,充分考虑实时响应、超大规模数据要求下架构的可扩展性。NodeJS摒弃依靠多线程实现高并发的设计思路,采用了 单线程 、 异步式I/O 、 事件驱动式 程序设计模型,从而带来了可观的性能提升。
CommonJS规范
CommonJS规范试图拟定一套完整的JavaScript规范,以弥补普通应用程序所需API,包括 模块(modules)
、 包(packages)
、 系统(system)
、 二进制(binary)
、 控制台(console)
、 编码(encodings)
、 文件系统(filesystems)
、 套接字(sockets)
、 单元测试(unit testing)
等部分。
NodeJS和 MongoDB 都是CommonJS的实现,由于这几种技术都处于快速变化期,所以它们并不完全遵循CommonJS规范。
NodeJS的特点
异步式I/O
NodeJS使用的是单线程模型,对于所有I/O都采用异步式的请求方式,避免了频繁的上下文切换。
多线程在处理耗时较长的 SQL 语句时,线程会阻塞等待结果返回。高并发访问的情况下,一方面线程长期阻塞等待,另一方面为应付新请求需要不断增加线程,线程的增加会占用CPU时间处理内存上下文切换,因此会浪费大量CPU资源。
这种场景下,NodeJS不会等待结果返回,而是直接继续执行后续语句,直到进入事件循环。当数据库查询结果返回时,会将事件发送到事件队列,等到线程进入事件循环以后,才会调用之前的回调函数继续执行后续逻辑。
事件驱动
NodeJS在执行的过程中会维护一个事件队列,程序在执行时进入事件循环等待下一个事件到来,每个异步式I/O请求完成后会被推送到事件队列,等待程序进程进行处理。
NodeJS的异步机制是基于事件的,所有磁盘I/O、网络通信、数据库查询都以非阻塞的方式进行,返回的结果由事件循环来处理。
NodeJS进程在同一时刻只会处理一个事件,完成后立即进入事件循环检查并处理后面的事件,让CPU和内存在同一时间集中处理一件事,同时尽可能让耗时的I/O操作并行执行。
NodeJS与PHP+Nginx性能对比
在3000并发连接30秒的测试下,输出 hello world
请求:
- PHP每秒响应请求数为
3624
,平均每个请求响应时间为0.39
秒; - NodeJS每秒响应请求数为
7677
,平均每个请求响应时间为0.13
秒。
同样的测试对 MySQL 的查询操作:
- PHP每秒响应请求数为
1293
,平均每个请求响应时间为0.82
秒; - NodeJS每秒响应请求数为
2999
,平均每个请求响应时间为0.33
秒。
NodeJS架构简介
NodeJS除了使用V8作为JavaScript引擎以外,还使用高效的 libev 和 libeio 库支持事件驱动和异步式I/O( Windows下使用了IOCP ),并在此基础上抽象出 libuv 层。
快速开始
新建1个test.js文件,编写代码 console.log('Hello NodeJS!');
,然后保存文件。打开命令行执行 node test.js
语句,得到结果如下:
➜ node test.js Hello NodeJS!
查看帮助
通过在控制台输入 node --help
命令获得关于node命令行的帮助信息。
➜ blog git:(master) ✗ node --help Usage: node [options] [ -e script | script.js | - ] [arguments] node inspect script.js [arguments] Options: -v, --version print Node.js version -e, --eval script evaluate script -p, --print evaluate script and print result -c, --check syntax check script without executing -i, --interactive always enter the REPL even if stdin does not appear to be a terminal -r, --require module to preload (option can be repeated) - script read from stdin (default; interactive mode if a tty) ... ...
REPL模式
输入-求值-输出循环( Read-eval-print loop )可直接输入并运行JavaScript代码,在控制台执行 node
即可进入该模式,连续按2次 Ctrl+C 可退出该模式。
➜ blog git:(master) ✗ node > console.log("Hello node v8.9.4!"); Hello node v8.9.4! > (To exit, press ^C again or type .exit) >
如果运行的是JavaScript函数,会在命令行最后显示该函数返回值,上面例子中 undefined
是 console.log()
方法的返回值。
建立HTTP服务器
新建app.js文件,然后编写如下代码:
var http = require("http"); http.createServer(function(req, res) { res.writeHead(200, { "Content-Type": "text/plain" }); res.end("Hello World\n"); }) .listen(5000, "127.0.0.1"); console.log("HTTP服务运行于http://127.0.0.1:5000/");
运行 node app.js
命令,提示信息如下:
➜ /workspace node app.js HTTP服务运行于http://127.0.0.1:5000/
打开浏览器访问 http://127.0.0.1:5000
。
服务启动后,NodeJS会一直监听 5000
端口,按下 Ctrl+C 可以结束HTTP监听服务。
使用supervisor或nodemon
NodeJS只在第1次执行时解析脚本,以后都会直接访问内存,从而避免重复载入。为方便开发和调试,可以通过安装 supervisor 或者 nodemon 来实时载入代码。
- 首先,执行命令
npm install -g supervisor nodemon
安装Supervisor或者Nodemon模块到当前目录。 - 然后,使用命令
supervisor app.js
或nodemon app.js
启动服务。 - 最后,
app.js
中代码被改动时,运行的脚本会被终止,然后重新启动。
生产环境下推荐使用商业化支持更加良好的 pm2 进程管理工具。
异步式I/O与事件编程
NodeJS最大特点是异步式I/O( 或者非阻塞I/O )与事件紧密结合的编程模式,该模式与传统同步式I/O的线性编程思路有很大不同,因为控制流需要靠事件和回调函数来组织,一个逻辑要拆分为若干个单元。
- 同步式IO ( 阻塞式IO ):线程在执行中如果遇到磁盘读写、网络通信、长事务查询等I/O操作,通常需要耗费较长时间,这时操作系统会剥夺该线程CPU控制权,将资源让给其它工作线程。当I/O操作完毕时,操作系统解除该线程阻塞状态,恢复CPU控制权,使其继续执行。
- 异步式IO ( 非阻塞式IO ):当线程遇到I/O操作时,不会以阻塞方式等待I/O操作完成,而只是将I/O请求发送给操作系统,然后继续执行下一条语句。当操作系统完成I/O操作时,以事件形式通知执行I/O操作的线程,线程会在特定时间处理该事件。处理异步I/O的时候,线程必须有事件循环,不断检查有无未处理的事件,然后依次进行处理。
多线程同步式IO与单线程异步式IO的比较
- 阻塞模式下,1个线程只能处理1项任务,提高吞吐量必须依靠多线程。非阻塞模式下,1个线程永远在执行操作,该线程CPU占用率保持100%,I/O完成时以事件方式进行通知。
- 阻塞模式下,多线程能够提高系统吞吐量,因为1个线程阻塞时还有其它线程在工作,多线程可以使CPU资源不被阻塞中的线程浪费。非阻塞模式下,线程不会被I/O阻塞,永远在利用CPU。
异步式I/O的优缺点
异步式I/O优点在于免去了多线程的开销。对操作系统来说,创建线程的代价十分昂贵( 需要给分配内存、列入调度,线程切换时需要执行内存换页、清空CPU缓存,切换回来的时候再重新从内存中读取数据 )。
异步式编程缺点在于不符合一般的程序设计思维,容易让控制流变得晦涩难懂,给编码调试都带来一定困难。
同步式 I/O(阻塞式) | 异步式 I/O(非阻塞式) |
---|---|
利用多线程提供吞吐量 | 单线程即可实现高吞吐量 |
通过事件片分割和线程调度利用多核CPU | 通过功能划分利用多核CPU |
需要由操作系统调度多线程使用多核CPU | 可以将单进程绑定到单核CPU |
难以充分利用CPU资源 | 可以充分利用CPU资源 |
内存轨迹大,数据局部性弱 | 内存轨迹小,数据局部性强 |
符合线性的编程思维 | 不符合传统编程思维 |
异步与同步API
NodeJS异步读取文件的例子: readFile()
函数调用时只是将异步式I/O请求发送给操作系统,然后立即返回并执行后面的语句,执行完后进入事件循环监听事件。当fs接收到I/O请求完成的事件,事件循环会主动调用回调函数以完成后续工作。因此会先看到 结束!
再看到文件内容。
var fs = require("fs"); // readFile接收3个参数,第1是文件名,第2是编码方式,第3是回调函数 fs.readFile("file.txt", "utf-8", function(err, data) { if (err) { console.error(err); } else { console.log(data); } }); console.log("结束!");
➜ node app.js 结束! 文件内容...
NodeJS同步读取文件的例子: readFileSync()
函数以文件名作为参数,阻塞等待读取完成后,将文件内容作为返回值赋给变量 data
,接下来控制台输出 data
的内容,最后输出 结束!
。
var fs = require('fs'); var data = fs.readFileSync('file.txt', 'utf-8'); console.log(data); console.log('结束!');
➜ node app.js 文件内容... 结束!
NodeJS并不是所有API都提供同步和异步版本,NodeJS不鼓励使用同步I/O。
异步事件
NodeJS异步I/O操作完成时会发送一个事件到事件队列,该事件由 EventEmitter
对象提供。前面例子中 readFile()
和 createServer()
的回调函数都是通过 EventEmitter
实现,下面例子简要说明了 EventEmitter
的用法:
var EventEmitter = require("events").EventEmitter; var event = new EventEmitter(); // event 对象注册了事件some_event的监听器 event.on("some_event", function() { console.log("some_event occured!"); }); //1000毫秒后setTimeout向event对象发送事件some_event调用监听器 setTimeout(function() { event.emit("some_event"); }, 5000);
➜ node app.js some_event occured!
NodeJS事件循环机制
NodeJS程序从事件循环开始到结束,所有逻辑都是事件回调函数。NodeJS从第1个事件的回调函数开始进入事件循环,始终处于事件循环中。
事件回调函数在执行的过程中,可能会发出I/O请求或直接发射( emit )事件,执行完毕后再返回事件循环,事件循环会检查事件队列中有没有未处理的事件,直到程序结束。
NodeJS事件循环对开发者不可见,由 libev 库实现并支持多种类型事件( ev_io、ev_timer
、 ev_signal、ev_idle
等)。NodeJS中这些事件均被 EventEmitter
封装, libev事件循环 的每次迭代在NodeJS中就是一次 Tick , libev 不断检查是否有活动的、可供检测的事件监听器,当检测不到时将退出事件循环并结束进程。
Module模块
模块是NodeJS应用程序的基本组成部分,文件与模块一一对应,一个NodeJS文件就是一个模块,该文件可以是JavaScript代码、JSON、编译过的C/C++扩展。NodeJS提供 exports
对象来公开模块,以及 require
对象获取模块。
创建模块
创建1个 module.js
文件。
// module.js var name; exports.setName = function(targetName) { name = targetName; }; exports.sayHello = function() { console.log("Hello " + name); };
相同目录下再创建 getmodule.js
文件。
// getmodule.js var myModule = require('./module'); myModule.setName('Hank'); myModule.sayHello();
命令行执行结果:
➜ node getmodule.js Hello Hank
单次加载
require
不会重复加载模块,无论调用多少次 require
,获得的模块都是同一个。
// loadmodule.js var helloHank = require("./module"); helloHank.setName("Hank"); var helloUinika = require("./module"); helloUinika.setName("Uinika"); helloUinika.sayHello();
输出结果为”Hello Uinika”,因为变量 helloHank
和 helloUinika
指向同一个实例,所以 helloHank.setName()
的结果被 helloUinika.setName()
覆盖,最终输出结果由后者决定。
➜ node loadmodule.js Hello Uinika
覆盖exports
接下来将1个对象封装到模块中。
//singleobject.js function Hello() { var name; this.setName = function(targetName) { name = targetName; }; this.sayHello = function() { console.log("Hello " + name); }; } exports.Hello = Hello;
如果直接通过 require('./single object').Hello
获取 Hello
对象会显得冗余,可使用如下方法进行简化。
//hello.js function Hello() { var name; this.setName = function(targetName) { name = targetName; }; this.sayHello = function() { console.log("Hello " + name); }; } module.exports = Hello;
模块接口唯一变化是使用 module.exports = Hello
代替了 exports.Hello= Hello
,外部引用该模块时,接口对象就是输出的 Hello
对象本身,而非之前的 exports
。
// gethello.js var Hello = require("./hello"); hello = new Hello(); hello.setName("Hank"); hello.sayHello();
exports是一个普通的空对象 {}
,通过它在模块内部建立访问接口。但不能通过对 exports
直接赋值代替对 module.exports
赋值, exports
实际上只是与 module.exports
指向同一对象,它本身会在模块执行结束后释放,而 module
不会,因此只能通过指定 module.exports
来改变访问接口。
Package包
包在模块基础上更进一步抽象,NodeJS包类似于 Java 类库,它将某个独立功能封装起来,用于发布、更新、依赖管理、版本控制。NodeJS根据CommonJS规范实现了包机制,通过npm来解决包的发布和获取需求。
NodeJS包是一个目录,其中包含一个说明文件 package.json
,严格符合CommonJS规范的包应具备这些特征: package.json
必须放在包的根目录、二进制文件保存在 bin
目录、JavaScript代码保存在 lib
目录、文档放置在 doc
目录、单元测试保存在 test
目录 。
作为文件夹的模块
最简单的包,就是一个作为文件夹的模块。下面的例子中,首先建立名为 mypackage
的文件夹,并在其中创建 index.js
文件。
// mypackage/index.js exports.hello = function() { console.log('Hello Node v8.9.4 !'); };
然后在 mypackage
文件夹外建立一个 getpackage.js
文件。
// getpackage.js var somePackage = require('./mypackage'); somePackage.hello();
通过上面的操作就可以将 mypackage
文件夹封装为1个包,包通常是许多模块的集合,并在模块基础上提供更高层的抽象。
包描述文件package.json
通过定制 package.json
,可以创建更复杂、更完善、更符合规范的包。接下来在 mypackage
文件夹下,继续创建一个 package.json
描述文件。
{ "main" : "./lib/interface.js" }
然后将 index.js
重命名为 interface.js
并放入 lib
子文件夹,最后以同样方式调用该包,得到的结果相同:
➜ node getpackage.js Hello Node v8.9.4 !
NodeJS调用某个包时,会首先检查包中 package.json
文件的 main
字段,如果 package.json
或 main
字段不存在,就会默认尝试寻找 index.js
或 index.node
作为包的入口点。
package.json
是CommonJS规范的包描述文件,完全符合规范应包含如下字段:
name description version keywords maintainers contributors bugs licenses repositories dependencies
包管理器npm
npm是NodeJS官方提供的包管理工具,可用于NodeJS包的发布、传播、依赖控制,npm拥有 本地 和 全局 两种安装模式。
本地模式
npm默认会从 Naughty programmer’s madness ( ^﹏^ )搜索并下载依赖包,并将其安装至当前目录的 node_modules
子目录下。 require
加载模块时会尝试搜寻 node_modules
子目录,因此本地模式安装的包可以被直接引用。
命令格式: npm [install/i] [package_name]
npm会自动解析包的依赖并自动进行相应下载。
全局模式
npm也可以通过全局模式进行安装,但是该模式下安装的包不能直接在JavaScript文件中使用。
命令格式: npm [install/i] -g [package_name]
全局链接
npm还可以在本地和全局包之间创建符号链接,从而打破全局模式安装的包不能直接通过require使用的限制,但是该命令不支持Windows。
命令格式: npm link (in package dir)
发布npm包
npm可以方便地发布一个包,这里可以使用 npm init
根据交互式问答产生一个符合标准的package.json。
➜ npm init package name: (test) version: (1.0.0) description: entry point: (index.js) test command: git repository: keywords: author: license: (ISC) About to write to /workspace/test/package.json: { "name": "test", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" }
接下来需要通过 npm adduser
获取一个帐号( 需要提前在npm官网注册帐号 )。
➜ npm adduser Username: uinika Password: Email: (this IS public) uinika@outlook.com Logged in as uinika on https://registry.npmjs.org/.
完成后可以使用 npm whoami
测试是否正确创建了帐号。
➜ npm whoami uinika
如果包将来有更新,只需要修改 package.json
文件中的 version
字段,然后重新使用 npm publish
命令即可。如果对已经发布的包不满意,可以使用 npm unpublish
命令取消发布。
➜ npm publish + uinika@1.0.0
命令行调试
NodeJS支持命令行下的单步调试。
命令格式: node inspect app.js
➜ node inspect app.js < Debugger listening on ws://127.0.0.1:9229/b16af064-40bb-4d78-99d4-6f5485f8a923 < For help see https://nodejs.org/en/docs/inspector < Debugger attached. Break on start in app.js:1 > 1 (function (exports, require, module, __filename, __dirname) { function app() { 2 console.log("Hello node v8.9.4"); 3 } debug>
NodeJS调试命令:
run
执行脚本,在第一行暂停
restart
重新执行脚本
cont,c
继续执行,直到遇到下一个断点
next,n
单步执行
step,s
单步执行并进入函数
out,o
从函数中步出
setBreakpoint(),sb()
在当前行设置断点
setBreakpoint('f()'),sb(...)
在函数f的第一行设置断点
setBreakpoint('script.js', 20),sb(...)
在script.js 的第20行设置断点
clearBreakpoint,cb(...)
清除所有断点
backtrace,bt
显示当前的调用栈
list(5)
显示当前执行到的前后5行代码
watch(expr)
把表达式expr加入监视列表
unwatch(expr)
把表达式expr从监视列表移除
watchers
显示监视列表中所有的表达式和值
repl
在当前上下文打开即时求值环境
kill
终止当前执行的脚本
scripts
显示当前已加载的所有脚本
version
显示V8的版本
远程调试
V8引擎的调试功能基于TCP协议,因此NodeJS可以轻松实现远程调试。
- 脚本会正常执行不会暂停,执行过程中调试客户端可以连接至调试服务器。
node --debug[=port] script.js
- 调试服务器启动后将立刻暂停执行脚本,等待调试客户端连接。
node --debug-brk[=port] script.js
第3方 工具 调试
- Eclipse下使用插件
Google Chrome Developer Tools
。 - Node在线调试工具 node-inspector 。
Global模块
浏览器JavaScript当中 window
是全局对象, NodeJS中全局对象是 global
, global
最根本的作用是作为全局变量的宿主(即所有的全局变量都是 global
对象的属性),因此在所有模块中都可以直接使用而无需包含。
NodeJS中不可能在代码最外层定义全局变量,因为所有自定义代码都是属于当前模块的, 而模块本身不是NodeJS最外层的上下文。
尽量显式的使用 var
、 let
、 const
关键字声明变量,避免直接声明将变量引入全局,污染命名空间,提高耦合风险。
global
:全局变量的宿主。
Class: Buffer
:用来与TCP数据流、文件系统操作等八位二进制流进行交互的类型。
__filename
:表示当前正在执行的脚本的 文件名 。
__dirname
:表示当前执行脚本所在的 目录 。
console
: debug信息打印控制台。
require()
:获取NodeJS模块。
exports()
:导出NodeJS模块,该方法是引用了 module.exports
的快捷类型。
module
:当前NodeJS模块的引用。
process
:用于描述当前NodeJS进程状态的对象。
setImmediate(callback[, ...args])
/ clearImmediate(immediateObject)
:将一些需要长时间运行的操作放在回调函数内,在浏览器完成后面的其他语句后,立刻执行该回调函数。
setInterval(callback, delay[, ...args])
/ clearInterval(intervalObject)
:在指定的毫秒数后重复执行指定回调函数,除非显示调用 clearInterval
关闭。
setTimeout(callback, delay[, ...args])
/ clearTimeout(timeoutObject)
:在指定的毫秒数后执行一次指定的回调函数。
Process模块
process
是全局变量( 即global对象的属性 ),用于描述当前NodeJS进程状态。
process.argv
是命令行参数数组,第1个元素是node,第2个元素是脚本文件名,从第3个元素开始每个元素是1个运行参数。
➜ /workspace node app.js 1985 name=Hank --v uinika [ '/opt/nodejs/bin/node', '/workspace/app.js', '1985', 'name=Hank', '--v', 'uinika' ]
process.stdout
是标准输出流,通常使用console.log()向标准输出打印字符,而process.stdout.write()函数则提供了更为底层的接口。
process.stdin
是标准输入流,初始时是被暂停的,要想从标准输入读取数据,必须恢复流,并手动编写流的事件响应函数。
process.stdin.resume(); process.stdin.on('data', function(data) { process.stdout.write('从控制台读取到的输入内容:' + data.toString()); });
➜ /workspace node app.js uinika@outlook.com 从控制台读取到的输入内容: uinika@outlook.com
process.nextTick(callback)
的功能是为事件循环设置一项任务,NodeJS会在下次事件循环调响应时调用callback。process.nextTick()可以把复杂的工作拆散,变成一个个较小的事件。
NodeJS适合I/O密集型的应用,不适合计算密集型应用。如果1个NodeJS进程只有1条线程,因此在任何时刻都只有1个事件在执行。如果该事件占用大量CPU时间,执行事件循环中的下个事件需要等待很久,因此NodeJS的一个编程原则就是 尽量缩短每个事件的执行时间 。
function doSomething(args, callback) { somethingComplicated(args); // 比较耗时的函数 callback(); } doSomething(function onEnd() { compute(); // 比较耗时的函数 });
上面代码在调用 doSomething()
时会先执 somethingComplicated()
,然后立即调用回调函数,在 onEnd()
中又会执行 compute()
。
接下来改写上面的程序,将耗时的操作拆分为2个事件,减少每个单独事件的执行时间,提高事件响应速度:
function doSomething(args, callback) { somethingComplicated(args); process.nextTick(callback); } doSomething(function onEnd() { compute(); });
Console模块
console用于提供控制台标准输出。
console.log()
:向标准输出流打印字符并以换行符结束( 如果只有1个参数,则输出该参数的字符串形式;如果有2个参数,则以类似于C语言 printf()
的格式化输出 )。
console.log('Hello Node'); console.log('Hank'); console.log('Uinika %d', 2018);
➜ node app.js Hello Node Hank Uinika 2018
console.error()
:与 console.log()
的用法相同,只是向标准错误流进行输出。
console.trace()
:向标准错误流输出当前的调用栈。
➜ node app.js Trace at Object.<anonymous> (/workspace/app.js:1:71) at Module._compile (module.js:643:30) at Object.Module._extensions..js (module.js:654:10) at Module.load (module.js:556:32) at tryModuleLoad (module.js:499:12) at Function.Module._load (module.js:491:3) at Function.Module.runMain (module.js:684:10) at startup (bootstrap_node.js:187:16) at bootstrap_node.js:608:3
Util模块
util提供常用函数集合,用于弥补核心JavaScript功能方面的不足。
util.inherits(constructor, superConstructor)
用于实现对象之间的原型继承。
var util = require("util"); function Base() { this.name = "base"; this.base = 1985; this.sayHello = function() { console.log("Hello " + this.name); }; } Base.prototype.showName = function() { console.log(this.name); }; function Sub() { this.name = "sub"; } util.inherits(Sub, Base); var objBase = new Base(); objBase.showName(); objBase.sayHello(); console.log(objBase); var objSub = new Sub(); objSub.showName(); //objSub.sayHello(); console.log(objSub);
➜ node app.js base Hello base Base { name: 'base', base: 1985, sayHello: [Function] } sub Sub { name: 'sub' }
Sub
只继承 Base
在原型中定义的函数,而构造函数内部创建的 base
属性和 sayHello()
函数都没有被Sub继承。而原型中定义的属性不会被 console.log()
作为对象属性输出。现在如果去掉objSub.sayHello()的注释,将会出现如下错误:
➜ node app.js base Hello base Base { name: 'base', base: 1985, sayHello: [Function] } sub /workspace/app.js:22 objSub.sayHello(); ^ TypeError: objSub.sayHello is not a function at Object.<anonymous> (/workspace/app.js:22:8) at Module._compile (module.js:643:30) at Object.Module._extensions..js (module.js:654:10) at Module.load (module.js:556:32) at tryModuleLoad (module.js:499:12) at Function.Module._load (module.js:491:3) at Function.Module.runMain (module.js:684:10) at startup (bootstrap_node.js:187:16) at bootstrap_node.js:608:3
util.inspect(object,[showHidden],[depth],[colors])
用于将任意对象转换为字符串,通常用于调试和错误输出。
- 参数object:需要转换的目标对象。
- 参数howHidden:如果值为true,将会输出更多隐藏信息。
- 参数depth:表示最大递归的层数,默认递归2层,指定为null将完整遍历对象。
- 参数color:如果为true,输出格式以ANSI颜色编码,通常用于控制台显示效果。
util.inspect
并非直接将对象转换为字符串,即使该对象定义了 toString()
方法也不会被调用。
var util = require("util"); function Person() { this.name = "Hank"; this.toString = function() { return this.name; }; } var obj = new Person(); console.log(util.inspect(obj)); console.log(util.inspect(obj, true));
➜ node app.js Person { name: 'Hank', toString: [Function] } Person { name: 'Hank', toString: { [Function] [length]: 0, [name]: '', [arguments]: null, [caller]: null, [prototype]: { [constructor]: [Circular] } } }
Events模块
events
是NodeJS最重要的模块,因为NodeJS本身就是基于事件式的架构,该模块提供了唯一接口,所以堪称NodeJS事件编程的基石。 events
模块不仅用于与下层的事件循环交互,还几乎被所有的模块所依赖。
events
模块只提供1个 events.EventEmitter
对象, EventEmitter
对象封装了事件发射和事件监听器。每个 EventEmitter
事件由1个事件名和若干参数组成,事件名是1个字符串。 EventEmitter
对每个事件支持若干监听器,事件发射时,注册至该事件的监听器依次被调用,事件参数将作为回调函数参数传递。
下面例子中, emitter
为事件 targetEvent
注册2个事件监听器,然后发射 targetEvent
事件,结果2个事件监听器的回调函数被依次先后调用。
var events = require("events"); var emitter = new events.EventEmitter(); emitter.on("targetEvent", function(arg1, arg2) { console.log("listener1", arg1, arg2); }); emitter.on("targetEvent", function(arg1, arg2) { console.log("listener2", arg1, arg2); }); emitter.emit("targetEvent", "Hank", 2018);
➜ /workspace node app.js listener1 Hank 2018 listener2 Hank 2018
EventEmitter常用API
EventEmitter.on(event, listener) EventEmitter.emit(event,[arg1],[arg2],[...]) EventEmitter.once(event, listener) EventEmitter.removeListener(event, listener) EventEmitter.removeAllListeners([event])
EventEmitter
包含1个定义错误语义的 error
事件,通常遇到异常时会发射 error
事件。当 error
被发射时,如果没有相应的监听器,NodeJS会将其当作异常,退出程序并打印调用栈。通常情况下,需要为会发射 error
事件的对象设置监听器,避免程序遇到错误后崩溃。
var events = require("events"); var emitter = new events.EventEmitter(); emitter.emit("error");
➜ node app.js events.js:188 throw err; ^ Error: Unhandled "error" event. (undefined) at EventEmitter.emit (events.js:186:19) at Object.<anonymous> (/workspace/app.js:3:9) at Module._compile (module.js:643:30) at Object.Module._extensions..js (module.js:654:10) at Module.load (module.js:556:32) at tryModuleLoad (module.js:499:12) at Function.Module._load (module.js:491:3) at Function.Module.runMain (module.js:684:10) at startup (bootstrap_node.js:187:16) at bootstrap_node.js:608:3
继承EventEmitter
通常情况下,不会直接使用 EventEmitter
,而是在对象中继承,这样做的原因有2点:
EventEmitter
包括 fs
、 net
、 http
在内,只要是支持事件响应的核心模块都是 EventEmitter
的子类。
File System模块
fs
模块封装了文件操作,提供了文件读取、写入、更名、删除、遍历、链接等POSIX文件系统操作,该模块中所有操作都提供了异步和同步2个版本。
fs.readFile(filename,[encoding],[callback(err,data)])
用于读取文件,第1个参数 filename
表示要读取的文件名。第2个参数 encoding
表示文件的字符编码,第3个参数 callback
是回调函数,用于接收文件内容。
回调函数提供 err
和 data
两个参数, err
表示有无错误发生, data
是文件内容。如果指定 encoding
, data
将是1个解析后的字符串,否则 data
将会是以 Buffe
r`形式表示的二进制数据。
下面的例子当中,从 content.txt
( 包含汉字”遵义会议” )中读取数据,不指定编码将输出乱码。
var fs = require("fs"); fs.readFile("content.txt", function(err, data) { if (err) { console.error(err); } else { console.log(data); } });
➜ node app.js <Buffer e9 81 b5 e4 b9 89 e4 bc 9a e8 ae ae>
指定 encoding
编码后,文本正常编码并输出。
var fs = require("fs"); fs.readFile("content.txt", "utf-8", function(err, data) { if (err) { //当读取文件出现错误时,err即是Error对象 console.error(err); } else { console.log(data); } });
➜ node app.js 遵义会议
NodeJS异步编程通常以函数最后1个参数作为回调,通常1个函数只有1个回调。回调函数第1个参数是 err
,如果没有发生错误, err
值为 null
或 undefined
;如果有错误发生, err
通常是 Error
对象的实例。
fs.readFileSync()
NodeJS提供的 fs.readFileSync()
函数是 readFile()
的同步版本,两者接受的参数相同,读取到的文件内容会以函数返回值形式返回。如果有错误发生 fs
将会抛出异常,需要使用 try...catch
捕捉并处理异常。
与同步I/O函数不同,NodeJS中异步函数大多没有返回值。
fs.open()
fs.open(path,flags,[mode],[callback(err,fd)])
封装了POSIX的 open()
函数,与 C语言 标准库中 fopen()
函数类似。该函数接受2个必选参数,第1个参数 path
为文件路径,第2个参数 flags
代表文件打开模式,第3个参数 mode
用于创建文件时给文件指定权限( 默认0666 ),第4个参数是 回调函数 ,函数中需要传递文件描述符 fd
。
fs.read()
fs.read(fd,buffer,offset,length,position,[callback(err,bytesRead,buffer)])
封装了POSIX的read函数,相比 fs.readFile()
提供了更底层的接口。
fs.read()
的功能是从指定的文件描述符fd中读取数据并写入 buffer
指向的缓冲区对象。 offset
是 buffer
的写入偏移量。 length
是要从文件中读取的字节数。 position
是文件读取的起始位置,如果 position
的值为 null
,则会从当前文件指针的位置读取。回调函数传递 bytesRead
和 buffer
,分别表示读取的 字节数 和 缓冲区对象 。
下面的例子综合使用了 fs.open()
和 fs.read()
:
var fs = require("fs"); fs.open("content.txt", "r", function(err, fd) { if (err) { console.error(err); return; } var buf = new Buffer(8); fs.read(fd, buf, 0, 8, null, function(err, bytesRead, buffer) { if (err) { console.error(err); return; } console.log("读取的byte: " + bytesRead); console.log(buffer); }); });
➜ node app.js 读取的byte: 8 <Buffer e9 81 b5 e4 b9 89 e4 bc>
Http模块
NodeJS标准库提供的 http
模块封装了一个高效的HTTP服务器 http.Server
和一个简易的HTTP客户端 http.request
。
http
模块中的HTTP服务器对象,核心由NodeJS底层依靠C++实现,接口使用JavaScript封装,兼顾了高性能与简易性。
下面代码中, http.createServer()
创建了一个 http.Server
实例,并将一个匿名函数作为HTTP请求处理函数。该函数接受两个参数,分别是请求对象 req
和响应对象 res
。函数体内, res
显式的写入响应代码 200
( 表示请求成功 ),并指定了响应头和响应体,然后通过 res.end()
结束并发送。最后调用 listen()
函数,启动服务器并监听 3000
端口。
var http = require("http"); http.createServer(function(req, res) { res.writeHead(200, { "Content-Type": "text/html" }); res.write("<h1>Hank</h1>"); res.end("<p>Hello Node v8.9.4</p>"); }) .listen(3000); console.log("HTTP正在监听3000端口!");
➜ node app.js HTTP正在监听3000端口!
http.Server的事件
http.Server
是基于事件的HTTP 服务器,所有请求都被封装为独立的事件,开发者只需要对相应事件编写函数即可实现HTTP服务器的所有功能。它继承自 EventEmitter
,主要提供了以下几个事件:
request connection close
事件 request
较常用,因此 http
提供了快捷方法 http.createServer([requestListener])
来创建HTTP服务器,其中 requestListener
作为 request
事件的监听函数。
var http = require("http"); var server = new http.Server(); server.on("request", function(req, res) { res.writeHead(200, { "Content-Type": "text/html" }); res.write("<h1>Hank</h1>"); res.end("<p>Hello Node v8.9.4 again!</p>"); }); server.listen(3000); console.log("HTTP正在监听3000端口!");
除此之外还有 checkContinue
、 upgrade
、 clientError
事件,通常不需要开发人员关心,只有在实现复杂HTTP服务器时才会使用。
http.serverRequest
http.ServerRequest
是HTTP请求的信息,通常由 http.Server
的 request
事件发送,作为第1个参数传递,通常简称 request
或 req
, ServerRequest
提供了如下属性:
名 称 | 含 义 |
---|---|
complete |
客户端请求是否已经发送完成 |
httpVersion |
HTTP协议版本,通常是 1.0 或 1.1 |
method |
HTTP请求方法,如 GET 、 POST 、 PUT 、 DELETE 等 |
url |
原始的请求路径,例如 /static/avatar.png 或 /user?name=Hank |
connection |
当前HTTP连接套接字,为 net.Socket 的实例 |
socket |
connection 属性的别名 |
client |
client 属性的别名 |
trailers |
HTTP请求尾 |
HTTP请求分为 请求头 、 请求体 两部分,请求体可能相对较长,需要一定时间传输,因此 http.ServerRequest
提供以下3个事件用于控制请求体传输。
data end close
获取GET请求
GET请求的参数放置在查询参数中,需要使用url模块的parse函数手动进行解析。
var http = require("http"); var url = require("url"); var util = require("util"); http.createServer(function(req, res) { res.writeHead(200, { "Content-Type": "text/plain" }); res.end(util.inspect(url.parse(req.url, true))); }).listen(3000);
获取POST请求
POST请求的内容全部都在请求体中,NodeJS默认不会解析请求体,需要借助 querystring
的 parse()
方法进行解析。 但这种方式不可用于生产环境,因为存在严重的效率和安全问题 。
var http = require("http"); var util = require("util"); var querystring = require("querystring"); http.createServer(function(req, res) { var post = ""; req.on("data", function(chunk) { post += chunk; }); req.on("end", function() { post = querystring.parse(post); res.end(util.inspect(post)); }); }).listen(3000);
http.ServerResponse
该对象封装了返回给客户端的信息,通常简称为 response
或 res
,主要有3个重要的成员函数,用于返回响应头、响应内容、结束请求。
-
response.writeHead(statusCode,[headers])
:向请求的客户端发送响应头。statusCode
是HTTP状态码,headers
对象表示响应头的每个属性。该函数在1个请求内最多只能调用`次,如果不显式调用,则会自动生成一个响应头。 -
response.write(data, [encoding])
:向请求的客户端发送响应内容。data
是Buffer
或字符串,表示要发送的内容。如果data
是字符串,那么需要通过encoding
说明其编码方式(默认是utf-8)。在response.end()
调用之前,response.write()
可以被多次调用。 -
response.end([data],[encoding])
:结束响应,告知客户端全部响应已经完成。当所有响应内容发送完毕后,该函数必须被调用1次。接受2个可选参数,意义与response.write()
相同。如果不调用该函数,客户端将永远处于等待状态。
HTTP客户端
http模块提供了2个函数 http.request()
和 http.get()
,功能是作为客户端向HTTP服务器发起请求。
http.request()
http.request(options,callback)
发起HTTP请求,接受2个参数, option
是一个类似关联数组的对象,表示请求的参数, callback
是请求的回调函数;其中 option
拥有如下常用参数:
host port method path headers
var http = require("http"); var querystring = require("querystring"); var contents = querystring.stringify({ name: "Hank", email: "uinika@163.com", address: "GuiZhou University" }); var options = { host: "www.uinika.com", path: "/application/nodejs/post.php", method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", "Content-Length": contents.length } }; var req = http.request(options, function(res) { res.setEncoding("utf8"); res.on("data", function(data) { console.log(data); }); }); req.write(contents); req.end(); // 不能忘记req.end()结束请求,否则服务器将不会收到信息
http.get()
http模块提供 http.get()
来更加简便的处理GET请求,这是 http.request()
的简化版本,区别在于 http.get()
自动将请求方法设置为 GET
,同时不需要手动调用 req.end()
。
var http = require("http"); http.get( { host: "https://uinika.github.io/" }, function(res) { res.setEncoding("utf8"); res.on("data", function(data) { console.log(data); }); } );
http.ClientRequest
http.ClientRequest
是 http.request()
或 http.get()
方法返回产生的对象,表示已经产生的HTTP请求。提供一个 response
事件(即 http.request()
或 http.get()
第2个参数指定的回调函数所绑定对象),也可以显式绑定该事件的监听函数。
var http = require("http"); var req = http.get({ host: "https://uinika.github.io/" }); req.on("response", function(res) { res.setEncoding("utf8"); res.on("data", function(data) { console.log(data); }); });
同 http.ServerResponse
一样, http.ClientRequest
也提供 write()
和 end()
函数,用于向服务器发送请求体,通常用于 POST
、 PUT
等操作,操作结束后必须手动调用end函数通知服务器,否则请求无效。 http.ClientRequest
还提供了以下函数:
-
request.abort()
:终止正在发送的请求。 -
request.setTimeout(timeout,[callback])
:设置请求超时时间,timeout
为毫秒数。请求超时后callback
会被调用。
还有request.setNoDelay()、request.setSocketKeepAlive()等函数,可查询NodeJS文档。
http.ClientResponse
http.ClientResponse
与 http.ServerRequest
类似,提供了 data
、 end
、 close
三个事件,分别在数据到达、传输结束、连接结束时触发,其中 data
事件传递一个参数 chunk
,表示接收到的数据。
http.ClientResponse
也提供了一些属性,用于表示请求的结果状态。
名 称 | 含 义 |
---|---|
headers |
HTTP请求头 |
trailers |
HTTP请求尾 |
statusCode |
HTTP状态码,如200、404、500 |
httpVersion |
HTTP协议版本,通常是1.0或1.1 |
http.ClientResponse
还提供以下几个特殊函数:
-
response.setEncoding([encoding])
:设置默认编码,当data事件被触发时,数据将以encoding
编码。默认值是null
,即不编码,以Buffer
的形式存储。常用编码为utf8
。 -
response.resume()
:从暂停状态中恢复。 -
response.pause()
:暂停接收数据和发送事件,方便实现下载功能。
Express服务器
NodeJS提供的http模块仅仅是1个HTTP服务器内核的简单封装,如果需要使用它直接开发网站,那么必须手动实现所有功能( POST请求、Cookie、会话管理 )。npm提供的轻量级Web开发框架 Express ( 截止到2018年2月其最新版本为4.16.0 ),为 http
模块提供了更高层的接口,还实现了许多功能(包括 用户会话
、 路由控制
、 模板解析支持
、 动态视图
、 CSRF保护
、 静态文件服务
、 错误控制器
、 缓存
、 插件支持
、 访问日志
等)。
Express4核心方法与对象
express()
express()
方法是express模块的最顶层函数,用于建立1个Express应用。
var express = require("express"); var app = express();
Application对象
Application
对象通常用来表示Express应用,通过调用express模块暴露出的 express()
方法可以获取该对象并赋值给变量 app
。 app
对象中的方法可以用来路由HTTP请求( app.METHOD
、 app.param
),配置中间件( app.route
),渲染HTML视图( app.render
),注册模板引擎( app.engine
)。
var express = require("express"); var app = express(); app.get("/", function(req, res) { res.send("hello world"); }); app.listen(3000);
Request / Response对象
Request/Response
对象分别用来表示HTTP 请求 与 响应 。
app.get("/user/:id", function(request, response) { response.send("user " + request.params.id); });
Router对象
Router
对象是执行路由功能和起中间件作用的独立实例,可通过顶层express对象的 Router()
函数建立新的 router
对象。
var router = express.Router([options]);
其中, options
参数用于指定该路由的具体行为:
-
caseSensitive
:启动大小写敏感,默认是关闭的,即"/HANK"
与"/hank"
等效。 -
mergeParams
:保存父级Router
的req.params
值,如果父级或子级存在参数名冲突,则子级Router
的属性值优先使用,该选项默认为false
。 -
strict
:是否允许严格路由,默认关闭,即"/uinika/"
与"/uinika"
等效。
可以象应用程序一样,增加中间件和Http方法到该路由。
// 被传入至该router的所有请求调用 router.use(function(req, res, next) { // ... next(); }); // 获取/uinika下的任意请求 router.get("/uinika", function(req, res, next) { // ... });
接下来所有发送至 /hank
相应路径的请求都会分发至 router
,从而分离应用路由至若干文件,或全部放置在一个文件中成为独立应用。
// 只有发送到/hank/*地址的请求,才会转发至指定的router app.use("/hank", router);
生产环境下的Express
NodeJS和Express在生产环境使用时,需要注意到这些方面的问题:不支持故障恢复、没有日志、 无法利用多核提高性能 、独占端口、需要手动启动。
Express支持开发和产品2种运行模式,生产环境下需要使用产品模式,设置NODE_ENV环境变量等于 production
即可。
接下来实现访问日志( 用户对服务器的请求信息 )、错误日志功能( 记录发生的错误信息 ),Express提供了日志访问中间件,只需指定其 stream
参数为一个输出流即可将访问日志写入文件。
首先在示例项目的 app.js
文件最上方加入如下代码:
var fs = require('fs'); var accessLogfile = fs.createWriteStream('access.log', {flags: 'a'}); var errorLogfile = fs.createWriteStream('error.log', {flags: 'a'});
然后在 app.js
的 app.configure()
函数第一行添加登录日志处理代码:
app.use(express.logger({ stream: accessLogfile }));
最后错误日志需要通过 app.error
注册错误响应函数,将错误信息写入错误日志流。
app.configure("production", function() { app.error(function(err, req, res, next) { var meta = "[" + new Date() + "] " + req.url + "\n"; errorLogfile.write(meta + err.stack + "\n"); next(); }); });
重新启动服务器,即可在 app.js
同目录下的 access.log
和 error.log
文件中查看到相应的错误信息。
模块加载机制
NodeJS模块分为是 核心模块 、 文件模块 2大类:
-
核心模块:NodeJS标准API提供的模块(例如fs、http、net、vm等),可以直接通过require 直接获取,例如require(‘fs’)。核心模块拥有最高的加载优先级,即如果有模块与其命名冲突,NodeJS总会优先加载核心模块。
-
文件模块:存储为单独文件或文件夹的模块( JavaScript代码、JSON、编译的C/C++代码 )。文件模块的加载方法复杂但是灵活,尤其是与npm结合使用时。在不显式指定文件模块扩展名时,NodeJS会试图加上
.js
、.json
、.node
扩展名。
模块类别 | 加载顺序 |
---|---|
文件模块 | 首先 .js |
JSON | 其次 .json |
C/C++扩展 | 最后 .node |
文件模块加载方式
按路径加载:如果require参数以/开头,就以绝对路径方式查找,例如require(‘/hank/uinika’)将会按优先级依次尝试加载/hank/uinika.js、uinika.json、uinika.node。
如果以./或../开头,则以相对路径方式查找,例如require(‘./uinika’)用来加载相同文件夹下的uinika.js。
查找 node_modules
加载 :如果 require()
函数参数不以 /、./、../
开头,该模块又不是核心模块,那么需要通过查找 node_modules
加载模块( npm获取的包就是以这种方式加载 )。
例如 node_modules
目录之外的 app.js
可以直接使用 require('express')
代替 require('./node_modules/express')
。
当 require()
遇到一个既非核心模块,又不以路径表示的模块时,会试图在当前目录下的 node_modules
当中进行查找。如果没有找到,则会进入上一层目录的 node_modules
继续查找,直至遇到根目录。
例如,在 /hank/app.js
中使用 require('test.js'))
,NodeJS会依次查找:
/home/hank/node_modules/test.js /home/node_modules/test.js /node_modules/test.js
加载缓存
NodeJS模块不会被重复加载,因为NodeJS通过文件名缓存所有加载过的文件模块,再次访问时将不会重复加载。
NodeJS根据实际文件名缓存模块,而非基于 require()
提供的参数进行缓存,即使分别通过 require('express')
和 require('./node_modules/express')
加载2次,尽管路径参数不同,但实际解析的文件依然是同一个。
异步模式下的流程控制
基于异步I/O的事件式编程需要将应用逻辑进行分拆,将会给应用程序的控制逻辑带来许多障碍,主要体现在如下两方面:
循环中回调函数的陷阱
下面代码通过 app.js
依次读取文件 a.txt
、 b.txt
、 c.txt
,然后分别输出文件名和内容。
➜ tree . ├── app.js ├── a.txt ├── b.txt └── c.txt
var fs = require("fs"); var files = ["a.txt", "b.txt", "c.txt"]; for (var i = 0; i < files.length; i++) { fs.readFile(files[i], "utf-8", function(err, contents) { console.log(files[i] + ": " + contents); }); }
➜ node app.js undefined: AAAAA undefined: CCCCC undefined: BBBBB
控制台输出结果当中,文件内容正确,但是文件名称却错误。接下来,将数据分别打印出来,在回调函数中分别输出 files
、 i
、 files[i]
的值。
var fs = require("fs"); var files = ["a.txt", "b.txt", "c.txt"]; for (var i = 0; i < files.length; i++) { fs.readFile(files[i], "utf-8", function(err, contents) { console.log("files : " + files); console.log("i : " + i); console.log("files[i] : " + files[i]); }); }
➜ node app.js files : a.txt,b.txt,c.txt i : 3 files[i] : undefined files : a.txt,b.txt,c.txt i : 3 files[i] : undefined files : a.txt,b.txt,c.txt i : 3 files[i] : undefined
可以发现 i
的输出一直是 3
,明显超出了 files
的长度,因此 files[i]
的值为 undefined
。这说明 readFile()
回调函数中访问到的 i
值都是循环退出后的结果。因为 files[i]
作为 fs.readFile
的第 1
个参数,并不是处于异步执行的回调函数中,所以能够正确定位文件。
这里可以通过手动建立闭包来解决这个问题,下面代码在 for
循环内建立了一个匿名函数,将循环迭代变量 i
作为函数参数传递进去。由于闭包的存在,匿名函数中定义的变量和参数在内部 fs.readFile()
回调函数执行完毕前都不会被释放,因此回调函数内访问的i分属不同的闭包实例,从而保留不同的值。
var fs = require("fs"); var files = ["a.txt", "b.txt", "c.txt"]; for (var i = 0; i < files.length; i++) { (function(i) { fs.readFile(files[i], "utf-8", function(err, contents) { console.log(files[i] + ": " + contents); }); })(i); }
➜ node app.js a.txt: AAAAA b.txt: BBBBB c.txt: CCCCC
因为上面这种方式降低了程序可读性,不推荐使用,推荐使用数组的 forEach()
方法解决该问题。
var fs = require("fs"); var files = ["a.txt", "b.txt", "c.txt"]; files.forEach(function(filename) { fs.readFile(filename, "utf-8", function(err, contents) { console.log(filename + ": " + contents); }); });
回调函数深层嵌套
除了循环的陷阱,NodeJS异步式编程还存在一个显著的问题:深层的回调函数嵌套。这种情况下,很难理清回调函数之间的关系,当程序规模扩大时必须降低耦合度,以增强代码可读性。
NodeJS提供了如下第三方模块来解决该问题:
-
async
是1个控制流解耦模块,提供了一系列函数来代替回调函数嵌套,但必须遵循其编程风格。 -
streamlinejs
、jscex
模块实现了一个JavaScript的编译器,其思想是 变同步为异步 ,用户可以使用同步方式编写代码,但是编译后执行时却是异步的。 -
eventproxy
模块对事件发射器进行了深度封装,采用完全基于事件松散耦合的方式来实现控制流的梳理。
第三方模块的实现手段具有侵入性,可能引入更加复杂的语法,需要酌情使用。
Cluster模块
NodeJS提供 cluster
核心模块,用于生成与当前进程相同的子进程,并且允许父进程和子进程之间共享端口。
NodeJS另一核心模块child_process也提供了类似功能,两者最大区别在于cluster允许跨进程端口复用。
如果在其它模块当中调用 app.js
,需要禁止服务器自动启动。可以修改 app.js
,并在 app.listen(3000);
附近添加如下判断语句:
if (!module.parent) { app.listen(3000); // 打印输出:Express正在以test模式监听端口3000! console.log( "Express正在以%s模式监听端口%d!",, app.settings.env, app.address().port ); }
上面代码判断当前模块是否由其它模块调用,如果是则不自动启动服务器,如果不是则直接启动调试服务器。经过上面修改,以后执行 node app.js
的时候,服务器会直接运行,但是在其它模块调用 "require('./app')"
则不会自动启动,而需要去显式调用 listen()
。
接下来通过 cluster
调用 app.js
,并创建 cluster.js
:
var cluster = require("cluster"); var os = require("os"); // 获取CPU 的数量 var numCPUs = os.cpus().length; var workers = {}; if (cluster.isMaster) { // 主进程分支 cluster.on("death", function(worker) { // 当一个工作进程结束时,重启工作进程 deleteworkers[worker.pid]; worker = cluster.fork(); workers[worker.pid] = worker; }); // 初始开启与CPU 数量相同的工作进程 for (var i = 0; i < numCPUs; i++) { var worker = cluster.fork(); workers[worker.pid] = worker; } } else { // 工作进程分支,启动服务器 var app = require("./app"); app.listen(3000); } // 当主进程被终止时,关闭所有工作进程 process.on("SIGTERM", function() { for (var pid in workers) { process.kill(pid); } process.exit(0); });
cluster.js
的功能是创建与CPU 核心个数相同的服务器进程,以确保充分利用多核CPU资源。主进程生成若干工作进程,并监听工作进程结束事件,当工作进程结束时,重新启动一个工作进程。分支进程产生时会自顶向下重新执行当前程序,并通过判断进入工作进程分支,最后在其中读取模块并启动服务器。
通过 cluster
启动的工作进程可以直接实现端口复用,所有工作进程只需监听相同端口。当主进程终止时,还要主动关闭其它工作进程。
在控制台执行 node cluster.js
,可以看到在8核CPU上启动了多个进程。如果终止工作进程,新的工作进程会立即启动,终止主进程,所有工作进程也会同时结束。这样,既能利用多核资源,又有实现故障恢复的服务器就诞生了。
NodeJS由于其单线程性的特性,必须通过多进程的方法才能充分利用多核资源。
NodeJS的瓶颈
计算密集型程序
NodeJS不善于处理计算密集型应用,当事件回调函数需要进行复杂运算,那么事件循环中所有请求都要等待计算完成之后才能响应。解决这个问题,需要将复杂运算拆解成若干逻辑,但这样又会提高代码的复杂度。
单用户多任务型应用
单用户多任务的情况下,需要进程之间相互协作,NodeJS当中处理类似场景不方便。NodeJS多进程往往是在执行同一任务,通过多进程来利用多核处理器资源,但当遇到多进程需要相互协作的时候,就显得捉襟见肘。
逻辑复杂的事务
NodeJS的控制流被一个个事件拆散,是非线性的,但是人类思维是线性的,这样容易造成开发复杂度的提高。NodeJS更善于处理逻辑简单但访问频繁的任务,而不适合完成逻辑十分复杂的工作。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。