3.1 开始使用Node.js编程
3.1.1 Hello World
将以下源代码保存到helloworld.js文件中
console.log(‘Hello World!‘); console.log(‘%s:%d‘, ‘hello‘, 25);
找到文件位置,执行node helloworld.js。结果如下:
3.1.2 Node.js命令行工具
输入:node --help可以看到详细的帮助信息。
除了直接运行脚本外,node --help显示的用法中说明了另一种输出hello world方式。
使用node的REPL模式
REPL(Read-eval-print loop),即输入-求值-输出循环。
进入REPL模式后,会出现一个">"提示符提示你输入,输入后按回车,Node.js将会解析并执行命令。如果你执行了一个函数,那么REPL还会在下面显示这个函数的返回值,上面例子中的undefined就是console.log的返回值,如果你输入了一个错误指令,REPL会立即显示错误并输出调用栈。在任何时候连续按两次CTRL + C即可退出Node.js的REPL模式。
node提出REPL在应用开发时会给人带来很大的便利,例如我们可以测试一个包能否正常使用,单独调用应用的某一个模块,执行简单的计算等。
3.1.3 建立HTTP服务器
node.js的服务器架构与PHP架构
建立一个名为app.js的文件,代码如下:
var http = require(‘http‘); http.createServer(function(req, res){ res.writeHead(200, {‘Content-type‘:‘text/html‘}); res.write(‘<h1>Node.js</h1>‘); res.end(‘<p>Hello world</p>‘); }).listen(3000); console.log(‘HTTP server is listening at port 3000.‘)
运行node app.js,打开浏览器http://127.0.0.1:3000,就可以看到图3-2所示的内容。
图3-2 用Node.js实现的Http服务器
用Node.js实现的最简单的HTTP服务器就这样诞生了。这个程序调用了Node.js提供的http模块,对所有HTTP请求答复同样的内容监听3000端口。在终端中运行这个脚本时,我们会发现它并不像Hello World一样结束后立即退出,而是一直等待,直到按下CTRL + C才会结束。这是因为listen函数中创建了事件监听器,使得Node.js进程不会退出事件循环。
小技巧——使用supervisor。
安装:$ npm install -g supervisor
如果你使用的是linux或Mac,直接键入上面的命令很可能有权限错误。原因是npm需要把supervisor安装到系统目录,需要管理员权限,可以使用sudo npm install -g supervisor命令来安装。
接下来使用supervisor命令启动app.js
3.2 异步式I/O与事件式编程
Node.js最大的特定就是异步式I/O(或者非阻塞I/O)与事件紧密结合的编程模式。这种模式与传统的同步式I/O线性的编程思想有很大的不同,因为控制流很大程度上要靠事件和回调函数来组织,一个逻辑要拆分为若干个单元。
3.2.1 阻塞与线程
什么是阻塞(block)呢?线程在执行中如果遇到磁盘读写或者网络通信(统称为I/O操作),通常需要耗费较长的时间,这时操作系统会剥夺这个线程的CPU控制权,使其暂停执行,同时将资源让给其他的工作线程,这个线程调度方式称为阻塞。当I/O操作完毕时,操作系统将这个线程的阻塞状态解除,恢复其对CPU的控制权,令其继续执行。这种I/O模式就是通常的同步式I/O(Synchronous I/O)或阻塞式(Blocking I/O)。
相应地,异步式I/O(Asynchronous I/O)或非阻塞式I/O(Non-blocking I/O)则针对所有I/O操作采用不阻塞的策略。当线程遇到I/O操作时,不会以阻塞的方式等待I/O操作的完成或数据的返回,而只是将I/O请求发送给操作系统,继续执行下一条语句。当操作系统完成I/O操作时,以事件的形式通知执行I/O操作的线程,线程会在特定时候处理这个事件。为了处理异步I/O,线程必须有事件循环,不断地检查有没有未处理的事件,一次予以处理。
阻塞模式下,一个线程只能处理一项任务,要想提高吞吐量必须多线程。而非阻塞模式下,一个线程永远在执行计算操作,这个线程所使用的CPU核心利用率永远是100%,I/O以事件的方式通知。在阻塞模式下,多线程往往能提高系统吞吐量,因为一个线程阻塞时还有其他线程在工作,多线程可以让CPU资源不被阻塞中的线程浪费。而在非阻塞模式下,线程不会被I/O阻塞,永远在利用CPU。多线程带来的好处仅仅是在多核CPU的情况下利用更多的核,而Node.js的单线程也能带来同样的好处。这就是为什么Node.js使用了单线程、非阻塞的事件编程模式。
图3-3和图3-4分别是多线程同步式与单线程异步式I/O的示例。假设我们有一项工作,可以分为两个计算部分和一个I/O部分,I/O部分占的时间比计算多得多(通常都是这样的)。如果我们使用阻塞I/O,那么要想获得高并发就必须开启多个线程。而使用异步式I/O时,单线程即可胜任。
单线程事件驱动的异步式I/O比传统多线程阻塞式I/O究竟好在哪里呢?简而言之,异步式I/O就是少了多线程的开销。对操作系统来说,创建一个线程的代价是十分昂贵的,需要给它分配内存,列入调度,同时在线程键切换的时候,还要执行内存换页,CPU的缓存被清空,切换回来的时候,还要重新从内存中读取信息,破坏了数据的局部性。
当然,异步式编程的缺点在于不符合人们一般的程序设计思维,容易让控制流变得晦涩难懂,给编码和调试都带来不小的困难。
3.2.2 回调函数
让我们看看在Node.js中如何使用异步的方式读取一个文件,下面是一个例子:
// readfile.js var fs = require(‘fs‘); fs.readFile(‘file.txt‘, ‘utf-8‘, function(err, data) { if (err) { console.error(err); } else { console.log(data); } }); console.log(‘end‘)
在新建一个文件file.txt,输入hello world!
运行结果如下:
D:\001code\0011nodejs>node readfile.js
end
hello world!
Node.js 也提供了同步读取文件的API:
// readfilesync.js var fs = require(‘fs‘); var data = fs.readFileSync(‘file.txt‘, ‘utf-8‘); console.log(data); console.log(‘end‘);
运行结果如下:
D:\001code\0011nodejs>node readfilesync.js
hello world!
end
比较运行结果:同步的先输出读取文件的内容,在输出end,而异步是先输出end,再输出文件的内容。fs.readFile调用时所做的工作只是将异步式I/O请求发送给了操作系统,然后立即返回并执行后面的语句,执行完以后进入事件循环监听事件。当fs收到I/O请求完成的事件时,事件循环会主动调用回调函数以完成后续工作。因此我们会先看到end,再看到file.txt文件的内容。
3.2.3 事件
Node.js所有的异步I/O操作在完成时都会发送一个事件到事件队列。在开发者看来,事件由EventEmitter对象提供。前面提到的fs.readFile和http.createServer的回调函数都是通过EventEmitter来实现的。下面我们用一个简单的例子说明EventEmitter的用法:
// event.js var EventEmitter = require(‘events‘).EventEmitter; var event = new EventEmitter(); event.on(‘some_event‘, function(){ console.log(‘some_event occured.‘); }); setTimeout(function(){ event.emit(‘some_event‘); }, 1000);
运行这段代码, 一秒后控制台输出了some_event occured.其原理是event对象注册了事件some_event的一个监听器,然后我们通过setTimeout在1000毫秒以后向event对象发送事件some_event,此时会调用some_event监听器。
Node.js的时间循环机制
Node.js在什么时候会进入事件循环呢?答案是Node.js程序由事件循环开始,到事件循环结束,所有的逻辑都是事件的回调函数,所以Node.js始终在事件循环中,程序入口就是事件循环第一个事件的回调函数。事件的回调函数在执行的过程中,可能会发出I/O请求或直接发射(emit)事件,执行完毕后再事件循环,事件循环会检查事件队列中有没有未处理的事件,直到程序结束。
与其他语言不同的是,Node.js没有显示的事件循环,类似Ruby的EventMachine::run()的函数在Node.js中是不存在的。Node.js的事件循环对开发者不可见,由libev实现。libev支持多种类型的事件,如ev_io, ev_timer, ev_signal,
ev_idle等,在Node.js中均被EventEmitter封装。libev事件循环的每一次迭代,在Node.js中就是一次Tick,libev不断检查是否有活动的、可供检查的时间监听器,直到检测不到时才退出事件循环,进行结束。事件,执行完毕后再事件循环,事件循环会检查事件队列中有没有未处理的事件,直到程序结束。
与其他语言不同的是,Node.js没有显示的事件循环,类似Ruby的EventMachine::run()的函数在Node.js中是不存在的。Node
.js的事件循环对开发者不可见,由libev实现。libev支持多种类型的事件,如ev_io, ev_timer, ev_signal, ev_idle等,在Node.js中均被EventEmitter封装。libev事件循环的每一次迭代,在Node.js中就是一次Tick,libev不断检查是否有活动的、可供检查的时间监听器,直到检测不到时才退出事件循环,进行结束。
3.3 模块和包
3.3.1 什么是模块
模块时Node.js应用程序的基本组成部分,文件和模块时一一对应的。换言之,一个Node.js文件就是一个模块,这个文件可以是javascript代码、JSON或者编译过的C/C++扩展。
在前面的例子中,我们曾经用到var http = require(‘http‘),其中http是Node.js的一个核心模块,其内部用C++实现的,外部用javascript封装,我们通过require函数获取了这个模块,然后才能使用其中的对象。
3.3.2 创建及加载模块
(1) 创建模块
在Node.js中,创建一个模块非常简单,因为一个文件就是一个模块,我们要关注的问题仅仅在于如何在其他文件中获取这个模块。Node.js提供了exports和require两个对象,其中exports是模块公开的接口,require用于从外部获取一个模块的接口,即所获取模块的exports对象。
让我们以一个例子来了解模块。创建一个module.js的文件,内容是:
// module.js var name; exports.setName = function(thyName){ name = thyName; }; exports.sayHello = function(){ console.log(‘Hello ‘ + name); }
在同一目录下,创建getmodule.js,内容是:
// getmodule.js var myModule = require(‘./module‘); myModule.setName(‘BYVoid‘); myModule.sayHello();
运行 node getmodule.js,结果如下:
hello BYVoid
(2)单次加载
/*=============================================== # Last modified:2014-08-27 13:55 # Filename:loadmodule.js # Description: 单次加载示例 =================================================*/ var hello1 = require(‘./module‘); hello1.setName(‘BYVoid1‘); var hello2 = require(‘./module‘); hello2.setName(‘BYVoide2‘); hello1.sayHello();
运行 node loadmodule.js 结果如下:
Hello BYVoide2
这是因为变量hello1和hello2指向的是同一个实例,因此hello1.setName的结果被hello2.setName覆盖,最终结果是由后者决定。
(3)覆盖exports
a 第一种方法 exports.Hello
模块
/*=============================================== # Author: RollerCoaster # Last modified:2014-08-27 14:02 # Filename: singleobject.js # Description: 覆盖exports示例1 =================================================*/ function Hello(){ var name; this.setName = function(thyName){ name = thyName; }; this.sayHello = function(){ console.log(‘Hello ‘ + name + ‘!‘); }; }; exports.Hello = Hello;
调用代码:
/*=============================================== # Author: RollerCoaster # Last modified:2014-08-27 14:05 # Filename: runSingleObject.js # Description: 调用singleobject模块 =================================================*/ var Hello = require(‘./singleobject‘).Hello; hello = new Hello(); hello.setName(‘roller coaster‘); hello.sayHello();
运行结果:
Hello roller coaster!
b 第二种方法 module.exports = Hello;
模块代码:
/*=============================================== # Author: RollerCoaster # Last modified:2014-08-27 14:17 # Filename: hello.js # Description: hello 模块 =================================================*/ function Hello(){ var name; this.setName = function(thyName){ name=thyName; }; this.sayHello = function(){ console.log(‘Hello ‘ + name + ‘!‘); } }; module.exports = Hello;
调用代码
/*=============================================== # Author: RollerCoaster # Last modified:2014-08-27 14:17 # Filename: gethello.js # Description: 调用hello模块 =================================================*/ var Hello = require(‘./Hello‘); hello = new Hello(); hello.setName(‘roller coaster‘); hello.sayHello();
注意,模块接口的唯一变化是使用module.exports = Hello 代替了exports.Hello = Hello。在外部调用该模块时,其接口对象就是要输出Hello对象本身,而不是原先的exports。
事实上,exports本身仅仅是一个普通的空对象,即{},它专门用来声明接口,本质上是通过它为模块闭包内部建立了一个有限的访问接口。因为它没有任何特殊的地方。所以可以用其他东西来代替,譬如我们上面例子上的Hello对象。
不可以通过对 exports直接复制代替对module.exports赋值。exports实际上只是一个和module.exports指向同一个对象的变量,它本身会在模块执行结束后释放,但module不会,因此只能通过指定module.exports来改变访问接口。