Node.js Streams: Everything you need to know

Streams are Node's best and most misunderstood idea -Dominic Tarr


What exactly are streams?

Stream là collections của data, giống như là mảng hay string.Sự khác biệt là stream tất cả mọi thứ không cùng tồn tại ở một thời điểm, và nó không phù hợp để lưu tất cả trong bộ nhớ.Chính đặc điểm này khiến stream thực sự giàu sức mạnh khi làm việc với số lượng data lớn, hoặc dữ liệu đến từ nhiều nguồn bên ngoài tại một thời điểm. Tuy nhiên, stream không gói gọn trong phạm vi làm việc với big data.Nó cũng mang tới sức mạnh của tính composability(chương trình là sự kết hợp của nhiều component(thành phần), các thành phần này độc lập và có khả năng thay thế mà không ảnh hướng tới các thành phần khác) trong code của chúng ta.Cũng giống như việc chúng ta có thể biên soạn một linux command bằng cách piping smaller Linux commands, chúng ta có thể thực hiện chính xác điều này giống với NodeJS Stream.

grep -R exports * | wc -l
const grep = ...  // stream cho grep output
const wc = ...  // stream cho wc input

grep.pipe(wc)

Rất nhiều built-in modules trong Node cài đặt streaming interface:

List dưới đây có một số ví dụ cho native Node.js objects mà nó cũng có khả năng đọc và ghi streams.Một số objects đó có cả đọc và ghi streams, giống như TCP sockets, zlib và crypto streams. Lưu ý objects cũng có mối liên kết chặt chẽ.Trong khi HTTP response là một readable stream trong trường hợp HTTP, chúng ta dựa trên việc đọc từ một object (http.IncomingMessage) và ghi vào object khác (http.ServerResponse). Cũng lưu ý rằng các stdio streams(stdin, stdout, stderr) có kiểu stream trái ngược khi nó trở thành tiến trình con.Nó mang tới một cách thức thật sự dễ dàng để pipe từ những stream trong main process stdio.

A streams practical example.

Lý thuyết là tương đối tuyệt vời tuy nhiên không thực sự thuyết phục 100%.Hãy xem một ví dụ chứng minh các luồn khác nhau có thể thực hiện trong code khi nói đến vấn đề tiêu tốn bộ nhớ. Chúng ta sẽ create một file như sau:

const fs = require('fs');
const file = fs.createWriteStream('./big.file');

for(let i=0; i<= 1e6; i++) {
  file.write('Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n');
}

file.end();

Hãy nhìn vào cách chúng ta đã dùng để tạo file đó: writable stream. Module fs có thể được sử dụng để đọc và ghi vào file sử dụng stream interface.Trong ví dụ bên trên, chúng ta writing vào file: big.file qua một writable stream 1 triệu dòng với một vòng lặp. Running đoạn script bên trên để generate một file có dung lượng khoảng ~400MB. Đây là một Node web server đơn giản được design để sử lí duy nhất file big.file:

const fs = require('fs');
const server = require('http').createServer();

server.on('request', (req, res) => {
  fs.readFile('./big.file', (err, data) => {
    if (err) throw err;

    res.end(data);
  });
});

server.listen(8000);

Khi server get một request, nó sẽ xử lí một big file xử dụng method không đồng bộ, fs.readFile.Tuy nhiên nó không giống như việc chúng ta đang blocking event hay bất cứ thứ gì khác.Mọi thứ đều ổn phải không nhỉ? OK, hãy cùng xem điều gì sẽ xảy ra khi run server, connect tới nó, và hãy xem memory khi nó đang thực hiện. Khi ta ran server, nó bắt đầu với một lượng memory khá nhỏ: 8,7 MB.

Sau khi đã connected tới server, lưu ý tới điều gì ảnh hưởng tới việc tiêu thụ memory:

Thật bất ngờ memory tiêu thụ lên tới 434.8 MB. Về cơ bản, chúng ta đã đặt toàn bộ nội dung tệp tin lớn trong bộ nhớ trước khi chúng ta viết ra response object. Điều này rất không hiệu quả. HTTP response object(res trong code bên trên) cũng là một writable stream.Nó có nghĩa là nếu chúng ta có một readable stream đại diện cho nội dung file big.file, chúng ta chỉ có thể gối hai cái này lên nhau và đạt được hầu hết kết quả tương tự mà không tốn đến 400 MB bộ nhớ.

Node fs module có thể cho chúng ta một readable stream cho bất kỳ file nào bằng cách sử dụng phương thức createReadStream.Chúng ta có thể gửi đến đối tượng trả lời:

const fs = require('fs');
const server = require('http').createServer();

server.on('request', (req, res) => {
  const src = fs.createReadStream('./big.file');
  src.pipe(res);
});

server.listen(8000);

Bây giờ khi bạn connect tới server, điều kì diệu sẽ xảy ra, hãy nhìn vào việc sử dụng memory dưới đây:

Điều gì đã xảy ra thế? Khi client request big file đó, chúng ta sẽ gửi nó một lần, điều đó có nghĩa là chúng ta không lưu trữ nó trong bộ nhớ. Việc sử dụng bộ nhớ đã tăng khoảng 25 MB và đó là lí do. Bạn có thể phát triển ví dụ này đến giới hạn của nó. Tạo lại big.file với năm triệu dòng thay vì chỉ một triệu, điều đó sẽ đưa tệp lên hơn 2 GB và thực sự lớn hơn giới hạn buffer mặc định trong Node. Nếu bạn cố gắng serve tệp đó bằng fs.readFile, điều đó không hẳn là đơn giản, theo mặc định (bạn có thể thay đổi các giới hạn).Nhưng với fs.createReadStream, không có vấn đề ở tất cả streaming 2 GB dữ liệu để yêu cầu, tất cả vẫn rất ổn, việc xử lí sử dụng memory sẽ gần như giống nhau. Bạn đã sẵn sàng để đọc stream rồi chứ?

This article is a write-up of part of my Pluralsight course about Node.js. I cover similar content in video format there.

Streams 101

Có bốn loại stream cơ bản trong Node.js: Các luồng có thể đọc, ghi, song công và chuyển đổi(khái niệm song công chúng ta cũng từng đọc ở đâu đó: Mạng máy tính phải không nào?)

  • Một stream có thể đọc được là một sự trừu tượng cho một source mà từ đó dữ liệu có thể được sử dụng.Một ví dụ về điều đó là phương thức fs.createReadStream.
  • Một stream ghi được là một sự trừu tượng cho một đích đến dữ liệu có thể được ghi.Một ví dụ của đó là phương thức fs.createWriteStream.
  • Một luồng duplex có thể đọc và ghi được.Một ví dụ của đó là một socket TCP.
  • Một luồng biến đổi cơ bản là một luồng duplex có thể được sử dụng để sửa đổi hoặc chuyển đổi dữ liệu khi nó được viết và đọc.Một ví dụ là stream zlib.createGzip để nén dữ liệu bằng gzip.Bạn có thể nghĩ đến một stream chuyển đổi như là một chức năng mà đầu vào là một stream có thể ghi và đầu ra có thể đọc một phần stream.Bạn cũng có thể nghe thấy các dòng chuyển đổi được gọi là "through streams".

Tất cả các luồng là các trường hợp của EventEmitter.Chúng phát ra các sự kiện có thể được sử dụng để đọc và ghi dữ liệu.Tuy nhiên, chúng ta có thể tiêu thụ dữ liệu theo một cách đơn giản bằng cách sử dụng phương pháp pipe.

Vậy là chúng ta đã nắm được khái niệm về stream, hy vọng chúng ta có thể thấy được nhiều điều thú vị hơn trong phần tới! Nguồn tham khảo: https://medium.freecodecamp.org/node-js-streams-everything-you-need-to-know-c9141306be93


All Rights Reserved