Node.js是单线程、异步非阻塞IO,但凡对Node.js有点了解的人都会说出这是Node的最大特点之一。但是怎么理解这个特点,或者说怎么能搞说服大家抛弃传统的Web应用架构而使用Node的架构呢?我想理解这所谓的单线程、异步非阻塞IO就显得异常重要。
今天我们就看一个Node.js中一个非常重要的概念Stream来简单的理解一下上述的几个特点。Stream,翻译成中文只有简单的一个“流”字,如果没有信息系统或者计算机技术背景的人根本不会有什么概念,但是你只要你对任何一种现代编程语言有所了解,都会对输入输出的“流”这个概念会有自己的理解,比如Java中的java.io.InputStream和java.io.OutputStream。我个人理解这个Node.js中的Stream与这些IO流没有什么本质的不同,只不过在Node.js这个架构中更加体现出流的特点,那么流的特点是什么呢?
流最大的特点就是我可以在它没有完全出现的时候能够使用它已经出现的部分。比如想我这个年纪,都有在中小学时代值日为班级打白开水的经历,我们要为全班打一大桶水,开水桶很大本来就需要等待很长时间,而你又很苦逼,打水的水龙头不是很灵水“流”很小,而更郁闷的是你刚刚下了体育课本来想假公济私一把自己可以马上喝到水。这个时候有两个选择,第一个是等把整个开水壶打满后和全班同学同乐,另外一个选择就是先用自己的茶缸不等开会桶满了的情况下自己先乐。我想百分之八十的像我这样非二三道杠的人都会选择第二个方案。这种情况与我们现在信息系统中的数据IO的情况很像,大部分情况或者说大部分系统架构只能采取第一种方式等待所有的数据(水)都准备好了之后才能处理这些数据,而Node.js就不同可以在数据没有完全准备好或者是说没有完全出现的情况下就开始异步处理数据。而这种需要异步处理数据的情况在大数据、实时响应和大量网络IO情况下尤为显的重要。
下面就让我们看一个具体点的例子:
var http = require('http'), fs = require('fs'); var server = http.createServer(function(req, res) { fs.readFile(__dirname + '/data.txt', function(err, data) { res.end(data); }); }); server.listen(8000);
这个Node.js应用很简单,估计所有学习过Node的人都做过这样的练习,可以说是Node的Hello World了。这段代码没有任何问题,你使用node可以正常的运行起来,使用浏览器或者其他的http客户端都可以正常的访问运行程序主机的8000端口读取主机上的data.txt文件。但是这种方式隐含了一个潜在的问题,node会把整个data.txt文件都缓存到内存中以便响应客户端的请求(request),随着客户端请求的增加内存的消耗将是非常惊人的,而且客户端需要等待很长传输时间才能得到结果。让我们再看一看另外一种方式:
var http = require('http'), fs = require('fs'); var server = http.createServer(function(req, res) { var stream = fs.createReadStream(__dirname + '/data.txt'); stream.pipe(res); }); server.listen(8000);
这里面有一个非常大的变化就是使用createReadStream这个fs的方法创建了stream这个变量,并由这个变量的pip方法来响应客户端的请求。使用stream这个变量就可以让node读取data.txt一定量的时候就开始向客户端发送响应的内容,而无需服务缓存以及客户端的等待。
这样就使我们对Node.js的stream有了点感觉了。流(Stream)可以定义为一个连续的数据流,并且可以在数据流入(出)的时候对这些数据进行异步操作。在Node.js中流既可以是可读的,也可以是可写的。一个可读的stream还是一个EventEmitter对象,当每一次接收到一定量的数据都会触发data事件。我们上边的例子中使用pipe方法将一个文件的内容发送给一个HTTP客户端。当stream到达文件的末尾时会触发一个end的事件,表明不会再有任何的data事件发生了。同时一个可读stream可以暂停并且继续。可写stream是接收数据流,这种类型的stream也继承了EventEmitter对象,并且实现了两个方法:write()和end()。第一个方法在写入缓存数据的时候触发,而且如果数据写入准确则返回值true,如果缓存满了(这种情况下数据会再次发送)。方法end()就会在stream结束的时候触发。
根据Stream的特点我们来做一个Steam类型的应用,这是一个简单的上传文件的应用。首先我们将创建一个客户端使用可读的stream读取一个文件并且将其pipe到一个特定的目标,另外在pipe结束的时候使用一个可写的stream实现一个服务将上传的数据进行保存。
首先我们先看一下客户端的代码,注释中有比较详细的解释。
// 引入http和文件系统模块 var http = require('http'), fs = require('fs'); // 定义http请求 var options = { host : 'localhost', port : 8000, path : '/', method : 'POST' }; var req = http.request(options, function(res) { console.log(res.statusCode); }); // 创建一个可读的stream读取文件并把读取的内容通过pipe发送给请求 var readStream = fs.ReadStream(__dirname + "/in.txt"); readStream.pipe(req); // 当stream停止读取的时候通过对request调用end()方法断开链接 readStream.on('close', function() { req.end(); console.log("I finished."); });
我们再看一下服务端的代码。
// 与客户端一样引入http和文件系统两个模块,并且使用一个可写的stream var http = require('http'), fs = require('fs'); var writeStream = fs.createWriteStream(__dirname + "/out.txt"); // 为了响应客户端上传文件的请求,我们创建了一个server,数据请求通过request对象进入 // 服务将使用stream并将缓存写入输出文件 var server = http.createServer(function(req, res) { req.on('data', function(data) { writeStream.write(data); }); req.on('end', function() { writeStream.end(); res.statusCode = 200; res.end("OK"); }); }); server.listen(8000);
通过上面的列子,大家应该对Node.js的流(stream)会更有感觉了。后面的时间里如果有机会我将继续深入的探讨一下stream的使用,特别是在请求的数据量巨大时,对数据分页或者分批处理的方法。
本文参考了以下内容: