官网上(http://www.nodejs.org )给Node下的定义是:”一个搭建在Chrome JavaScript 运行时上的平台,用于构建高速、可伸缩的网络程序。Node.js采用的事件驱动、非阻塞I/O模型,使它既轻量又高效,并成为构建运行在分布式设备上的数据密集型实时程序的完美选择“。
异步和事件触发:浏览器
Node为服务端JavaScript提供了一个事件驱动的、异步的平台,它是一个服务器端的JavaScript解释器。了解浏览器的工作原理对我们了解Node的工作原理会有很大帮助。它们都是事件驱动(用事件轮询)和非阻塞的I/O处理(用异步I/O)。下面举个例子说明这是什么意思。
我们来看一小段jQuery 用XMLHttpRequest(XHR)做Ajax 请求的代码:
$.post('/resource.json',function(data){//I/O不会阻塞执行 console.log(data); }) //脚本继续执行
这个程序会发送一个到resource.json的HTTP请求。当响应返回时会调用带着参数data的匿名函数(在这个上下文中的回调函数),data就是从那个请求中得到的数据。注意,代码没有写成下面这样:
var data=$.post('/resource.json');//在I/O完成之前程序会被阻塞 console.log(data);
在这个例子中,假定对resource.json的响应在准备好后会存储在变量data中,并且在此之前函数console.log 不会执行。I/O操作(Ajax 请求)会阻塞脚本继续执行,直到数据准备好。因为浏览器是单线程的,如果这个请求用了400ms 才返回,那么页面上的其他任何事件都要等到那之后才能执行。可以想象一下,如果一幅动画被停住了,或者用户试着跟页面交互时动不了,那种用户体验有多糟糕。理解JavaScript中的事件轮询
实际情况是,当浏览器中有I/O操作时,并且是用回调函数的方式写的。那么当这个I/O操作完成时,它会发出一个“事件”,然后这个事件会进入任务队列,等待CPU处理。
这个I/O是异步的,并且不会“阻塞”脚本执行,事件轮询仍然可以响应页面上执行的其他交互或请求。这样,浏览器可以对客户做出响应,并且可以处理页面上的很多交互动作。
异步和事件触发:服务器
可能大多数人都了解传统的服务器编程的I/O模型,下面是一个PHP的例子:
$result=mysql_query('select * from myTable'); print_r($result);
这段代码做了些I/O操作,并且在所有数据回来之前,这个进程会被阻塞。对于很多程序而言,这个模型没有什么问题,并且很容易理解。但有一点可能会被忽略:这个进程也有状态,或者说内存空间,并且在I/O完成之前基本上什么也不会做。根据I/O操作的延迟情况,那可能会有10ms到几分钟的时间。
对于服务器而言,当有更多的请求过来时,服务器通常会用多线程的方式,给每个连接分配一个线程。尽管这听起来是个很自然的委派服务器劳动力的方式,单程序内的线程管理会非常复杂。此外,当需要大量的线程处理很多并发的服务器连接时,线程会消耗额外的操作系统资源。线程需要CPU和额外的RAM来做上下文切换。
在Node中,I/O几乎总是在主事件轮询之外进行,使得服务器可以一直处于高效并且随时能够做出响应的状态。这样进程就更加不会受I/O限制,因为I/O延迟不会拖垮服务器,或者像在阻塞方式下那样占用很多资源。因此一些在服务器上曾经是重量级的操作,在Node服务器上仍然是可以轻量级的。
DIRT程序
实际上,Node所针对的应用程序有一个专门的简称:DIRT。它表示数据密集型实时(data-intensive real-time)程序。
var fs = require('fs'); fs.readFile('./resource.json',function(er,data){ console.log(data); });
这段程序要从硬盘里读取resource.json文件。读取文件是异步任务,我们可以继续处理其他任何操作,直到数据准备好。这个不是在浏览器中用jQurey发起的一个Ajax请求,而是在Node中访问文件系统抓取resource.json。
下面是一个简单的HTTP服务器实现,它会用“Hello Word”响应所有请求:
var http = require('http'); http.createServer(function(req,res){ res.writeHead(200,{'Content-Type':'text/plain'}); res.end('Hello World\n'); }).listen(3000); console.log('Server running at http://localhost:300/');
只要有请求过来,它就会把“Hello World”写入到响应中返回。这个事件模型跟浏览器中对onclick事件的监听类似。下面是服务器的另一种写法,这样看起来request事件更明显:
var http = require('http'); var server = http.createServer(); server.on('request',function(req,res){ res.writeHead(200,{'Content-Type':'text/plain'}); res.end('Hello World\n'); }) server.listen(3000); console.log('Server running at http://localhost:3000/');
Node在数据流和数据流动上也很强大。你可以把数据流看成特殊的数组,只不过数组中的数据分散在空间上,而数据流中的数据是分散在时间上的。下面我们用数据流的方式来处理resource.json
var stream = fs.createReadStream('./resource.json'); stream.on('data',function(chunk){ console.log(chunk); }); stream.on('end',function(){ console.log('finished'); });
只要有新的数据块准备好,就会激发data事件,当所有数据块都加载完之后,会激发一个end事件。下面看看如何把一张图片留到客户端:
var http = require('http'); var fs = require('fs'); http.createServer(function(req,res){ res.writeHead(200,{'Content-Type':'image/png'}); fs.createReadStream('./image.png').pipe(res); }).listen(3000); console.log('Server running at http://localhost:3000/');
在这行代码中,数据从文件中读出来(fs.createReadStream),然后数据随着进来就被送到(.pipe)客户端(res)。在数据流动时,事件轮询还能处理其他事件。