Contents

Giới thiệu về cách sử dụng luồng trong Node.js

Bài học chính

Các luồng đóng một vai trò thiết yếu trong Node.js vì chúng hỗ trợ xử lý và truyền dữ liệu hiệu quả, từ đó cung cấp hỗ trợ tối ưu cho các ứng dụng kích hoạt sự kiện và thời gian thực.

Bằng cách sử dụng các dịch vụ của mô-đun hệ thống tệp Node.js, người ta có thể sử dụng hàm createWriteStream() để thiết lập luồng có thể ghi hướng dữ liệu tới một khu vực cụ thể.

Môi trường Node.js bao gồm nhiều loại luồng khác nhau phục vụ cho các mục đích và yêu cầu khác nhau. Bốn loại chính này bao gồm các luồng có thể đọc, có thể ghi, song công và chuyển đổi. Mỗi loại phục vụ một chức năng riêng biệt và cung cấp các khả năng cụ thể, cho phép các nhà phát triển chọn tùy chọn phù hợp nhất cho nhu cầu ứng dụng cụ thể của họ.

Về bản chất, luồng đóng vai trò là cấu trúc lập trình quan trọng trong việc tạo điều kiện thuận lợi cho việc truyền thông tin liên tục từ vị trí này sang vị trí khác. Khái niệm về luồng về cơ bản xoay quanh việc truyền byte một cách có hệ thống dọc theo một lộ trình được xác định trước. Theo các tài nguyên có thẩm quyền của Node.js, luồng tạo thành một khung giả định cho phép người dùng tương tác và thao tác dữ liệu.

Việc truyền thông tin hiệu quả qua luồng là một ví dụ điển hình về ứng dụng của nó trong hệ thống máy tính và truyền thông mạng.

Luồng trong Node.js

Các luồng là một yếu tố quan trọng tạo nên sự thịnh vượng của Node.js do tính phù hợp của chúng đối với các ứng dụng xử lý dữ liệu theo thời gian thực và hướng sự kiện, cả hai đều là khía cạnh cơ bản của môi trường thời gian chạy Node.js.

Để thiết lập một luồng mới trong Node.js, người ta phải sử dụng API luồng, được dành riêng cho việc xử lý các đối tượng Chuỗi và đệm dữ liệu trong hệ thống. Đáng chú ý, có bốn loại luồng chính được Node.js công nhận, đó là các loại có thể ghi, có thể đọc, song công và biến đổi.

Cách tạo và sử dụng luồng có thể ghi

Mô-đun Hệ thống tệp (fs) cung cấp lớp WriteStream cho phép tạo luồng có thể ghi để truyền dữ liệu đến đích được chỉ định. Bằng cách sử dụng phương thức fs.createWriteStream(), người ta có thể thiết lập luồng mới và chỉ định đường dẫn đích mong muốn bằng tham số được cung cấp. Ngoài ra, một loạt các lựa chọn cấu hình tùy chọn có thể được đưa vào nếu cần thiết.

 const {createWriteStream} = require("fs");

(() => {
  const file = "myFile.txt";
   const myWriteStream = createWriteStream(file);
  let x = 0;
  const writeNumber = 10000;

  const writeData = () => {
    while (x < writeNumber) {
      const chunk = Buffer.from(`${x}, `, "utf-8");
      if (x === writeNumber-1) return myWriteStream.end(chunk);
       if (!myWriteStream.write(chunk)) break;
      x\+\+
    }
  };

  writeData();
})();

Mã đã cho nhập hàm createWriteStream(), được sử dụng trong hàm mũi tên ẩn danh để tạo luồng tệp nối thêm dữ liệu vào một tệp được chỉ định, trong trường hợp này là “myFile.txt”. Trong hàm ẩn danh, có một hàm nhúng có tên writeData() , chịu trách nhiệm ghi thông tin vào tệp được chỉ định.

Hàm createWriteStream() sử dụng bộ đệm để ghi một chuỗi chữ số (từ 0 đến 9,999) trong tệp đầu ra được chỉ định. Khi được thực thi, tập lệnh này sẽ tạo một tệp nằm trong thư mục hiện tại và điền vào đó thông tin như sau:

/vi/images/myfile-initial-data-1.jpg

Bộ số liệu hiện tại kết thúc ở mức 2.915; tuy nhiên, nó phải bao gồm các chữ số cao tới 9. Sự chênh lệch này là do mỗi WriteStream sử dụng bộ đệm lưu giữ một lượng thông tin được xác định trước tại bất kỳ thời điểm nào. Để phân biệt giá trị mặc định của cài đặt này, người ta phải tham khảo tùy chọn highWaterMark.

 console.log("The highWaterMark value is: " \+
  myWriteStream.writableHighWaterMark \+ " bytes."); 

Việc kết hợp hướng dẫn nói trên trong hàm chưa đặt tên sẽ tạo ra thông báo tiếp theo trên dấu nhắc lệnh, như sau:

/vi/images/writestream-highwatermark-1.jpg

Đầu ra được hiển thị từ thiết bị đầu cuối cho biết ngưỡng highWaterMark được định cấu hình trước được đặt thành 16.384 byte theo mặc định. Do đó, nó hạn chế khả năng của bộ đệm này để chứa không quá 16.384 byte thông tin cùng một lúc. Theo đó, 2.915 ký tự đầu tiên (bao gồm bất kỳ dấu phẩy hoặc dấu cách nào), bao gồm giới hạn dữ liệu có thể được lưu trữ trong bộ đệm cùng một lúc.

Để giải quyết vấn đề lỗi bộ đệm, bạn nên sử dụng sự kiện truyền phát. Các luồng trải qua một số sự kiện trong quá trình truyền dữ liệu xảy ra tại các thời điểm khác nhau. Trong số các sự kiện này, sự kiện Drain đặc biệt phù hợp để xử lý các tình huống phát sinh lỗi bộ đệm.

Khi triển khai hàm writeData() , lệnh gọi phương thức write() của đối tượng WriteStream trả về một boolean cho biết liệu đoạn dữ liệu hoặc bộ đệm bên trong được phân bổ có đạt đến ngưỡng định trước hay không, được gọi là “ mực nước cao.” Nếu điều kiện này được đáp ứng, điều đó có nghĩa là ứng dụng có khả năng truyền dữ liệu bổ sung đến luồng đầu ra liên quan. Ngược lại, khi phương thức write() trả về giá trị sai, luồng điều khiển sẽ tiến hành loại bỏ mọi dữ liệu còn lại trong bộ đệm vì không thể thực hiện ghi thêm cho đến khi bộ đệm được làm trống.

 myWriteStream.on('drain', () => {
  console.log("a drain has occurred...");
  writeData();
}); 

Việc kết hợp mã sự kiện thoát nước nói trên trong một hàm ẩn danh sẽ cho phép làm cạn kiệt bộ đệm của WriteStream khi nó đạt công suất tối đa. Do đó, điều này kích hoạt việc thu hồi phương thức writeData() để cho phép truyền dữ liệu thêm. Khi thực hiện chương trình sửa đổi, thu được kết quả sau:

/vi/images/the-writestream-drain-event-1.jpg

Điều quan trọng cần lưu ý là ứng dụng buộc phải xóa bộ đệm WriteStream trong ba lần riêng biệt trong suốt quá trình hoạt động của nó. Ngoài ra, có vẻ như tệp văn bản cũng đã trải qua một số thay đổi nhất định.

/vi/images/myfile-updated-data-1.jpg

Cách tạo và sử dụng luồng có thể đọc được

Để bắt đầu quá trình đọc dữ liệu, hãy bắt đầu bằng cách thiết lập một luồng dễ hiểu thông qua việc sử dụng hàm fs.createReadStream().

 const {createReadStream} = require("fs");

(() => {
  const file = "myFile.txt";
   const myReadStream = createReadStream(file);

  myReadStream.on("open", () => {
    console.log(`The read stream has successfully opened ${file}.`);
  });

  myReadStream.on("data", chunk => {
    console.log("The file contains the following data: " \+ chunk.toString());
  });

  myReadStream.on("close", () => {
    console.log("The file has been successfully closed.");
  });
})(); 

Tập lệnh sử dụng phương thức createReadStream() để có quyền truy cập vào tệp có tên “myFile.txt” được tạo trước đó bằng lần lặp mã trước đó. Phương thức cụ thể này nhận được một đường dẫn tệp, có thể được trình bày ở định dạng chuỗi, bộ đệm hoặc URL, cùng với các tham số tùy chọn khác nhau làm đối số để xử lý.

Trong bối cảnh hàm ẩn danh liên quan đến luồng, điều đáng chú ý là tồn tại nhiều sự cố quan trọng liên quan đến luồng. Tuy nhiên, người ta có thể quan sát thấy không có bất kỳ dấu hiệu nào liên quan đến sự xuất hiện của hiện tượng “cống”. Hiện tượng này có thể là do luồng có thể đọc được thường không đệm dữ liệu cho đến khi hàm “stream.push(chunk)” được gọi hoặc sự kiện “có thể đọc được” được sử dụng.

Việc kích hoạt sự kiện open xảy ra bất cứ khi nào một tệp được mở để người dùng đọc. Bằng cách gắn sự kiện dữ liệu vào một luồng vốn đã liên tục, luồng đó sẽ chuyển sang trạng thái mà dữ liệu có thể được truyền ngay lập tức khi có sẵn. Việc thực thi mã được cung cấp sẽ tạo ra kết quả đầu ra sau:

/vi/images/read-stream-terminal-output-1.jpg

Cách tạo và sử dụng luồng song công

Luồng song công bao gồm cả khả năng ghi và đọc được, do đó cho phép thực hiện các hoạt động đọc và ghi đồng thời thông qua cùng một phiên bản. Một trường hợp minh họa liên quan đến việc sử dụng mô-đun mạng kết hợp với việc thiết lập ổ cắm TCP.

Một phương pháp đơn giản để minh họa các đặc điểm của kênh liên lạc song công liên quan đến việc phát triển hệ thống máy chủ và máy khách Giao thức điều khiển truyền dẫn (TCP) có khả năng trao đổi thông tin.

Tệp server.js

 const net = require('net');
const port = 5000;
const host = '127.0.0.1';

const server = net.createServer();

server.on('connection', (socket)=> {
    console.log('Connection established from client.');

    socket.on('data', (data) => {
        console.log(data.toString());
    });

    socket.write("Hi client, I am server " \+ server.address().address);

    socket.on('close', ()=> {
        console.log('the socket is closed')
    });
});

server.listen(port, host, () => {
    console.log('TCP server is running on port: ' \+ port);
}); 

Tệp client.js

 const net = require('net');
const client = new net.Socket();
const port = 5000;
const host = '127.0.0.1';

client.connect(port, host, ()=> {
    console.log("connected to server!");
    client.write("Hi, I'm client " \+ client.address().address);
});

client.on('data', (data) => {
    console.log(data.toString());
    client.write("Goodbye");
    client.end();
});

client.on('end', () => {
    console.log('disconnected from server.');
});

Thật vậy, cả tập lệnh máy chủ và máy khách đều sử dụng luồng có thể đọc và ghi để tạo điều kiện giao tiếp, cho phép trao đổi dữ liệu giữa chúng. Thuận tiện, ứng dụng máy chủ được khởi chạy trước và bắt đầu tích cực chờ đợi các kết nối đến. Khi khởi động máy khách, nó sẽ thiết lập kết nối với máy chủ bằng cách chỉ định

/vi/images/tcp-server-and-client-terminal-output-1.jpg

Khi hình thành kết nối, máy khách sẽ tạo điều kiện truyền dữ liệu bằng cách sử dụng WriteStream của nó để gửi thông tin đến máy chủ. Đồng thời, máy chủ ghi lại dữ liệu đến trong thiết bị đầu cuối trước khi sử dụng WriteStream của chính nó để truyền dữ liệu trở lại máy khách. Sau đó, khách hàng ghi lại dữ liệu đã nhận và tiếp tục trao đổi thêm thông tin, sau đó chấm dứt kết nối. Tại thời điểm này, máy chủ duy trì trạng thái mở để đáp ứng mọi kết nối tiếp theo từ máy khách.

Cách tạo và sử dụng luồng chuyển đổi

zlib và các luồng mật mã. Các luồng Zlib hỗ trợ việc nén và giải nén các tệp văn bản sau đó trong quá trình truyền tệp, trong khi các luồng mật mã cho phép liên lạc an toàn thông qua các hoạt động mã hóa và giải mã.

Ứng dụng nénFile.js

 const zlib = require('zlib');
const { createReadStream, createWriteStream } = require('fs');

(() => {
    const source = createReadStream('myFile.txt');
    const destination = createWriteStream('myFile.txt.gz');

    source.pipe(zlib.createGzip()).pipe(destination);
})(); 

Tập lệnh đơn giản này hoạt động bằng cách lấy tài liệu văn bản ban đầu, nén và lưu trữ trong thư mục hiện tại, nhờ vào tính hiệu quả của chức năng pipe() của luồng có thể đọc được. Việc loại bỏ các vùng đệm thông qua công nghệ đường ống dẫn dòng tạo điều kiện thuận lợi cho quy trình này.

Trước khi dữ liệu được ghi vào luồng có thể ghi của tập lệnh, nó sẽ trải qua một quá trình chuyển hướng nhỏ thông qua quá trình nén được hỗ trợ bởi phương thức createGzip() của thư viện zlib. Phương thức nói trên tạo ra một phiên bản nén của tệp và trả về một phiên bản mới của đối tượng Gzip, sau đó trở thành đối tượng nhận luồng ghi.

Ứng dụng decompressFile.js

 const zlib = require('zlib');
 const { createReadStream, createWriteStream } = require('fs');
 
(() => {
    const source = createReadStream('myFile.txt.gz');
    const destination = createWriteStream('myFile2.txt');
       
    source.pipe(zlib.createUnzip()).pipe(destination);
})(); 

Ứng dụng ngày nay mở rộng dựa trên tài liệu nén và nếu ai đó kiểm tra tệp mới được tạo có tên “myFile2.txt”, họ sẽ thấy rằng nội dung của nó tương ứng chính xác với nội dung của tài liệu gốc.

/vi/images/myfile2-data-1.jpg

Tại sao luồng lại quan trọng?

Các luồng đóng một vai trò quan trọng trong việc tối ưu hóa việc truyền dữ liệu bằng cách tạo điều kiện liên lạc liền mạch giữa các hệ thống máy khách và máy chủ, đồng thời hỗ trợ nén và truyền hiệu quả các kích thước tệp đáng kể.

Các luồng tăng cường đáng kể chức năng của ngôn ngữ lập trình bằng cách đơn giản hóa quá trình truyền dữ liệu. Việc thiếu tính năng truyền phát sẽ làm tăng độ phức tạp trong hoạt động truyền dữ liệu, đòi hỏi mức độ can thiệp thủ công cao hơn từ các nhà phát triển, điều này có thể dẫn đến gia tăng lỗi và giảm hiệu suất.