nodejs Stream使用中的陷阱

文章转自:http://my.oschina.net/sundq/blog/189505


最近公司有个专供下载文件的http服务器出现了内存泄露的问题,该服务器是用node写的,后来测试发现只有在下载很大文件的时候才会出现内存泄露的情况。最后干脆抓了一个profile看看,发现有很多等待发送的buff占用着内存,我的profile如下(怎么抓取profile,大家可以google一下):   

于是查看了一下发送数据的代码,如下:     

1
2
3
4
5
6
7
     var  fReadStream = fs.createReadStream(filename);
     fReadStream.on( 'data' function  (chunk) {
         res.write(chunk);
     });
     fReadStream.on( 'end' function  () {
         res.end();
     });

开始觉得没有什么问题,于是在google上查了一下node http处理大文件的方法,结果发现有人使用pipe方法,于是将代码修改如下:  

1
2
     var  fReadStream = fs.createReadStream(filename);
     fReadStream.pipe(res)

测试了一下,发现OK,但是还是不明白为什么会这样,于是研究一个一下pipe方法的代码,发现pipe有如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
function  pipeOnDrain(src) { //可写流可以执行写操作
   return  function () {
     var  dest =  this ;
     var  state = src._readableState;
     state.awaitDrain--;
     if  (state.awaitDrain === 0)
       flow(src); //写数据
   };
}
 
function  flow(src) { //写操作函数
   var  state = src._readableState;
   var  chunk;
   state.awaitDrain = 0;
 
   function  write(dest, i, list) {
     var  written = dest.write(chunk);
     if  ( false  === written) { //判断写数据是否成功
       state.awaitDrain++; //计数器
     }
   }
 
   while  (state.pipesCount &&  null  !== (chunk = src.read())) {
 
     if  (state.pipesCount === 1)
       write(state.pipes, 0,  null );
     else
       state.pipes.forEach(write);
 
     src.emit( 'data' , chunk);
 
     // if anyone needs a drain, then we have to wait for that.
     if  (state.awaitDrain > 0)
       return ;
   }
 
   // if every destination was unpiped, either before entering this
   // function, or in the while loop, then stop flowing.
   //
   // NB: This is a pretty rare edge case.
   if  (state.pipesCount === 0) {
     state.flowing =  false ;
 
     // if there were data event listeners added, then switch to old mode.
     if  (EE.listenerCount(src,  'data' ) > 0)
       emitDataEvents(src);
     return ;
   }
 
   // at this point, no one needed a drain, so we just ran out of data
   // on the next readable event, start it over again.
   state.ranOut =  true ;
}

原来pipe方法每次写数据的时候,都会判断是否写成功,如果写失败,会等待可写流触发"drain"事件,表示可写流可以继续写数据了,然后pipe才会继续写数据。

     这下明白了,我们第一次使用的代码没有判断res.write(chunk)是否执行成功,就继续写,这样如果文件比较大,而可写流的写速度比较慢的话,会导致大量的buff缓存在内存中,就会导致内存撑爆的情况。

总结:

      在使用流的过程中,一定要注意可读流和可写流读和写之间的平衡,负责会导致内存泄露,而pipe就实现了这样的功能。稍微研究了一下文档,发现stream类有pause()和resume()两个方法,这样的话我们也可以自己控制读写的平衡。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var  http = require( "http" );
var  fs = require( "fs" );
var  filename =  "file.iso" ;
 
var  serv = http.createServer( function  (req, res) {
     var  stat = fs.statSync(filename);
     res.writeHeader(200, { "Content-Length" : stat.size});
     var  fReadStream = fs.createReadStream(filename);
     fReadStream.on( 'data' function  (chunk) {
         if (!res.write(chunk)){ //判断写缓冲区是否写满(node的官方文档有对write方法返回值的说明)
             fReadStream.pause(); //如果写缓冲区不可用,暂停读取数据
         }
     });
     fReadStream.on( 'end' function  () {
         res.end();
     });
     res.on( "drain" function  () { //写缓冲区可用,会触发"drain"事件
         fReadStream.resume(); //重新启动读取数据
     });
});
 
serv.listen(8888);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值