nodejs中流(stream)的理解

antzone發表於2017-04-04

nodejs的fs模組並沒有提供一個copy的方法,但我們可以很容易的實現一個,比如:

[JavaScript] 純文字檢視 複製程式碼
var source = fs.readFileSync('/path/to/source', {encoding: 'utf8'});
fs.writeFileSync('/path/to/dest', source);

這種方式是把檔案內容全部讀入記憶體,然後再寫入檔案,對於小型的文字檔案,這沒有多大問題,比如grunt-file-copy就是這樣實現的。但是對於體積較大的二進位制檔案,比如音訊、視訊檔案,動輒幾個GB大小,如果使用這種方法,很容易使記憶體“爆倉”。理想的方法應該是讀一部分,寫一部分,不管檔案有多大,只要時間允許,總會處理完成,這裡就需要用到流的概念。

a:3:{s:3:\"pic\";s:43:\"portal/201704/04/224035fcjxqq9jzmc28j0v.png\";s:5:\"thumb\";s:0:\"\";s:6:\"remote\";N;}

如上面高大上的圖片所示,我們把檔案比作裝水的桶,而水就是檔案裡的內容,我們用一根管子(pipe)連線兩個桶使得水從一個桶流入另一個桶,這樣就慢慢的實現了大檔案的複製過程。

Stream在nodejs中是EventEmitter的實現,並且有多種實現形式,例如:

(1).http responses request

(2).fs read write streams

(3).zlib streams

(4).tcp sockets

(5).child process stdout and stderr

上面的檔案複製可以簡單實現一下:

[JavaScript] 純文字檢視 複製程式碼
var fs = require('fs');
var readStream = fs.createReadStream('/path/to/source');
var writeStream = fs.createWriteStream('/path/to/dest');
 
readStream.on('data', function(chunk) { // 當有資料流出時,寫入資料
    writeStream.write(chunk);
});
 
readStream.on('end', function() { // 當沒有資料時,關閉資料流
    writeStream.end();
});

上面的寫法有一些問題,如果寫入的速度跟不上讀取的速度,有可能導致資料丟失。正常的情況應該是,寫完一段,再讀取下一段,如果沒有寫完的話,就讓讀取流先暫停,等寫完再繼續,於是程式碼可以修改為:

[JavaScript] 純文字檢視 複製程式碼
var fs = require('fs');
var readStream = fs.createReadStream('/path/to/source');
var writeStream = fs.createWriteStream('/path/to/dest');
 
readStream.on('data', function(chunk) { // 當有資料流出時,寫入資料
    if (writeStream.write(chunk) === false) { // 如果沒有寫完,暫停讀取流
        readStream.pause();
    }
});
 
writeStream.on('drain', function() { // 寫完後,繼續讀取
    readStream.resume();
});
 
readStream.on('end', function() { // 當沒有資料時,關閉資料流
    writeStream.end();
});

或者使用更直接的pipe

[JavaScript] 純文字檢視 複製程式碼
// pipe自動呼叫了data,end等事件
fs.createReadStream('/path/to/source').pipe(fs.createWriteStream('/path/to/dest'));

下面是一個更加完整的複製檔案的過程

[JavaScript] 純文字檢視 複製程式碼
var fs = require('fs'),
    path = require('path'),
    out = process.stdout;
 
var filePath = '/Users/chen/Movies/Game.of.Thrones.S04E07.1080p.HDTV.x264-BATV.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觀察一次完成進度,並把已完成的大小、百分比和複製速度一併寫到控制檯上,當複製完成時,計算總的耗費時間,效果如圖:

a:3:{s:3:\"pic\";s:43:\"portal/201704/04/224336vbbuzlj33ut9lcjn.png\";s:5:\"thumb\";s:0:\"\";s:6:\"remote\";N;}

複製了一集1080p的權利的遊戲第四季第7集,大概3.78G大小,由於使用了SSD,可以看到速度還是非常不錯的

複製完成後,顯示總花費時間

a:3:{s:3:\"pic\";s:43:\"portal/201704/04/224407avl33484gg6vz3x6.png\";s:5:\"thumb\";s:0:\"\";s:6:\"remote\";N;}

結合nodejs的readline, process.argv等模組,我們可以新增覆蓋提示、強制覆蓋、動態指定檔案路徑等完整的複製方法,有興趣的可以實現一下,實現完成,可以

[JavaScript] 純文字檢視 複製程式碼
ln -s /path/to/copy.js /usr/local/bin/mycopy

這樣就可以使用自己寫的mycopy命令替代系統的cp命令

相關文章