Nodejs学习笔记之Stream模块


Posted in NodeJs onJanuary 13, 2015

一,开篇分析

流是一个抽象接口,被 Node 中的很多对象所实现。比如对一个 HTTP 服务器的请求是一个流,stdout 也是一个流。流是可读,可写或兼具两者的。

最早接触Stream是从早期的unix开始的, 数十年的实践证明Stream 思想可以很简单的开发出一些庞大的系统。

在unix里,Stream是通过 "|" 实现的。在node中,作为内置的stream模块,很多核心模块和三方模块都使用到。

和unix一样,node stream主要的操作也是.pipe(),使用者可以使用反压力机制来控制读和写的平衡。

Stream 可以为开发者提供可以重复使用统一的接口,通过抽象的Stream接口来控制Stream之间的读写平衡。

一个TCP连接既是可读流,又是可写流,而Http连接则不同,一个http request对象是可读流,而http response对象则是可写流。

流的传输过程默认是以buffer的形式传输的,除非你给他设置其他编码形式,以下是一个例子:

 var http = require('http') ;

  var server = http.createServer(function(req,res){

     res.writeHeader(200, {'Content-Type': 'text/plain'}) ;

     res.end("Hello,大熊!") ;

  }) ;

  server.listen(8888) ;

  console.log("http server running on port 8888 ...") ;

运行后会有乱码出现,原因就是没有设置指定的字符集,比如:“utf-8” 。

修改一下就好:

 var http = require('http') ;

 var server = http.createServer(function(req,res){

    res.writeHeader(200,{

        'Content-Type' : 'text/plain;charset=utf-8'  // 添加charset=utf-8

    }) ;

    res.end("Hello,大熊!") ;

 }) ;

 server.listen(8888) ;

 console.log("http server running on port 8888 ...") ;

运行结果:

Nodejs学习笔记之Stream模块

为什么使用Stream
node中的I/O是异步的,因此对磁盘和网络的读写需要通过回调函数来读取数据,下面是一个文件下载例子
上代码:

 var http = require('http') ;

 var fs = require('fs') ;

 var server = http.createServer(function (req, res) {

     fs.readFile(__dirname + '/data.txt', function (err, data) {

         res.end(data);

     }) ;

 }) ;

 server.listen(8888) ;

代码可以实现需要的功能,但是服务在发送文件数据之前需要缓存整个文件数据到内存,如果"data.txt"文件很
大并且并发量很大的话,会浪费很多内存。因为用户需要等到整个文件缓存到内存才能接受的文件数据,这样导致
用户体验相当不好。不过还好(req,res)两个参数都是Stream,这样我们可以用fs.createReadStream()代替fs.readFile()。如下:

 var http = require('http') ;

 var fs = require('fs') ;

 var server = http.createServer(function (req, res) {

     var stream = fs.createReadStream(__dirname + '/data.txt') ;

     stream.pipe(res) ;

 }) ;

 server.listen(8888) ;

.pipe()方法监听fs.createReadStream()的'data' 和'end'事件,这样"data.txt"文件就不需要缓存整
个文件,当客户端连接完成之后马上可以发送一个数据块到客户端。使用.pipe()另一个好处是可以解决当客户
端延迟非常大时导致的读写不平衡问题。

有五种基本的Stream:readable,writable,transform,duplex,and "classic” 。(具体使用请自己查阅api)

二,实例引入

当内存中无法一次装下需要处理的数据时,或者一边读取一边处理更加高效时,我们就需要用到数据流。NodeJS中通过各种Stream来提供对数据流的操作。

以大文件拷贝程序为例,我们可以为数据源创建一个只读数据流,示例如下:

 var rs = fs.createReadStream(pathname);

 rs.on('data', function (chunk) {

     doSomething(chunk) ; // 具体细节自己任意发挥

 });

 rs.on('end', function () {

     cleanUp() ;

 }) ;

代码中data事件会源源不断地被触发,不管doSomething函数是否处理得过来。代码可以继续做如下改造,以解决这个问题。

 var rs = fs.createReadStream(src) ;

 rs.on('data', function (chunk) {

     rs.pause() ;

     doSomething(chunk, function () {

         rs.resume() ;

     }) ;

 }) ;

 rs.on('end', function () {

     cleanUp();

 })  ;

给doSomething函数加上了回调,因此我们可以在处理数据前暂停数据读取,并在处理数据后继续读取数据。

此外,我们也可以为数据目标创建一个只写数据流,如下:

 var rs = fs.createReadStream(src) ;

 var ws = fs.createWriteStream(dst) ;

 rs.on('data', function (chunk) {

     ws.write(chunk);

 }) ;

 rs.on('end', function () {

     ws.end();

 }) ;

doSomething换成了往只写数据流里写入数据后,以上代码看起来就像是一个文件拷贝程序了。但是以上代码存在上边提到的问题,如果写入速度跟不上读取速度的话,只写数据流内部的缓存会爆仓。我们可以根据.write方法的返回值来判断传入的数据是写入目标了,还是临时放在了缓存了,并根据drain事件来判断什么时候只写数据流已经将缓存中的数据写入目标,可以传入下一个待写数据了。因此代码如下:

 var rs = fs.createReadStream(src) ;

 var ws = fs.createWriteStream(dst) ;

 rs.on('data', function (chunk) {

     if (ws.write(chunk) === false) {

         rs.pause() ;

     }

 }) ;

 rs.on('end', function () {

     ws.end();

 });

 ws.on('drain', function () {

     rs.resume();

 }) ;

最终实现了数据从只读数据流到只写数据流的搬运,并包括了防爆仓控制。因为这种使用场景很多,例如上边的大文件拷贝程序,NodeJS直接提供了.pipe方法来做这件事情,其内部实现方式与上边的代码类似。

下面是一个更加完整的复制文件的过程:

var fs = require('fs'),

  path = require('path'),

  out = process.stdout;

var filePath = '/bb/bigbear.mkv';

var readStream = fs.createReadStream(filePath);

var writeStream = fs.createWriteStream('file.mkv');

var stat = fs.statSync(filePath);

var totalSize = stat.size;

var passedLength = 0;

var lastSize = 0;

var startTime = Date.now();

readStream.on('data', function(chunk) {

  passedLength += chunk.length;

  if (writeStream.write(chunk) === false) {

    readStream.pause();

  }

});

readStream.on('end', function() {

  writeStream.end();

});

writeStream.on('drain', function() {

  readStream.resume();

});

setTimeout(function show() {

  var percent = Math.ceil((passedLength / totalSize) * 100);

  var size = Math.ceil(passedLength / 1000000);

  var diff = size - lastSize;

  lastSize = size;

  out.clearLine();

  out.cursorTo(0);

  out.write('已完成' + size + 'MB, ' + percent + '%, 速度:' + diff * 2 + 'MB/s');

  if (passedLength < totalSize) {

    setTimeout(show, 500);

  } else {

    var endTime = Date.now();

    console.log();

    console.log('共用时:' + (endTime - startTime) / 1000 + '秒。');

  }

}, 500);

可以把上面的代码保存为 "copy.js" 试验一下我们添加了一个递归的 setTimeout (或者直接使用setInterval)来做一个旁观者,

每500ms观察一次完成进度,并把已完成的大小、百分比和复制速度一并写到控制台上,当复制完成时,计算总的耗费时间。

三,总结一下

(1),理解Stream概念。

(2),熟练使用相关Stream的api

(3),注意细节的把控,比如:大文件的拷贝,采用的使用 “chunk data” 的形式进行分片处理。

(4),pipe的使用

(5),再次强调一个概念:一个TCP连接既是可读流,又是可写流,而Http连接则不同,一个http request对象是可读流,而http response对象则是可写流。

NodeJs 相关文章推荐
利用NodeJS的子进程(child_process)调用系统命令的方法分享
Jun 05 NodeJs
nodejs教程之环境安装及运行
Nov 21 NodeJs
轻松创建nodejs服务器(2):nodejs服务器的构成分析
Dec 18 NodeJs
NodeJS学习笔记之Http模块
Jan 13 NodeJs
nodejs修复ipa处理过的png图片
Feb 17 NodeJs
NodeJs的优势和适合开发的程序
Aug 14 NodeJs
nodejs获取微信小程序带参数二维码实现代码
Apr 12 NodeJs
nodejs后台集成ueditor富文本编辑器的实例
Jul 11 NodeJs
浅谈NodeJs之数据库异常处理
Oct 25 NodeJs
Mac 安装 nodejs方法(图文详细步骤)
Oct 30 NodeJs
nodejs操作mongodb的增删改查功能实例
Nov 09 NodeJs
Nodejs封装类似express框架的路由实例详解
Jan 05 NodeJs
Nodejs学习笔记之NET模块
Jan 13 #NodeJs
Nodejs学习笔记之Global Objects全局对象
Jan 13 #NodeJs
Nodejs为什么选择javascript为载体语言
Jan 13 #NodeJs
NodeJS中Buffer模块详解
Jan 07 #NodeJs
Nodejs中读取中文文件编码问题、发送邮件和定时任务实例
Jan 01 #NodeJs
Nodejs中调用系统命令、Shell脚本和Python脚本的方法和实例
Jan 01 #NodeJs
nodejs中实现路由功能
Dec 29 #NodeJs
You might like
php绝对路径与相对路径之间关系的的分析
2010/03/03 PHP
PHP执行批量mysql语句的解决方法
2013/05/02 PHP
Yii框架批量插入数据扩展类的简单实现方法
2017/05/23 PHP
Javascript miscellanea -display data real time, using window.status
2007/01/09 Javascript
jquery 文本上下无缝滚动,鼠标放上去就停止 小例子
2013/06/05 Javascript
NODE.JS加密模块CRYPTO常用方法介绍
2014/06/05 Javascript
js小数运算出现多位小数如何解决
2015/10/08 Javascript
jquery移动端TAB触屏切换实现效果
2020/12/22 Javascript
Jquery插件easyUi实现表单验证示例
2015/12/15 Javascript
JQuery中attr属性和jQuery.data()学习笔记【必看】
2016/05/18 Javascript
js中遍历对象的属性和值的方法
2016/07/27 Javascript
WEB 前端开发中防治重复提交的实现方法
2016/10/26 Javascript
微信小程序开发之animation循环动画实现的让云朵飘效果
2017/07/14 Javascript
详解Vue源码学习之callHook钩子函数
2018/07/25 Javascript
iconfont的三种使用方式详解
2018/08/05 Javascript
原生JS实现的跳一跳小游戏完整实例
2019/01/27 Javascript
vue单页面在微信下只能分享落地页的解决方案
2019/04/15 Javascript
[42:32]Secret vs Optic 2018国际邀请赛小组赛BO2 第二场 8.18
2018/08/19 DOTA
[49:58]完美世界DOTA2联赛PWL S3 Magma vs DLG 第一场 12.18
2020/12/19 DOTA
python版简单工厂模式
2017/10/16 Python
Python使用爬虫抓取美女图片并保存到本地的方法【测试可用】
2018/08/30 Python
selenium+python设置爬虫代理IP的方法
2018/11/29 Python
python3人脸识别的两种方法
2019/04/25 Python
Django model 中设置联合约束和联合索引的方法
2019/08/06 Python
Python根据指定文件生成XML的方法
2020/06/29 Python
Python项目实战之使用Django框架实现支付宝付款功能
2021/02/23 Python
利用CSS3把图片变成灰色模式的实例代码
2016/09/06 HTML / CSS
医学生个人求职信范文
2013/09/24 职场文书
实习护理工作自我评价
2013/09/25 职场文书
高中毕业的自我鉴定
2013/12/09 职场文书
中学教师请假制度
2014/02/03 职场文书
运动会800米加油稿
2014/02/22 职场文书
经济信息系毕业生自荐信范文
2014/03/15 职场文书
美容院店长岗位职责
2014/04/08 职场文书
中国梦演讲稿教师篇
2014/04/23 职场文书
庆七一活动总结
2014/08/27 职场文书