Duplex Stream: Cánh Cửa Hai Chiều Đầy Quyền Năng Của Node.js
Nodejs

Duplex Stream: Cánh Cửa Hai Chiều Đầy Quyền Năng Của Node.js

Author

Admin System

@root

Ngày xuất bản

21 Mar, 2026

Lượt xem

2 Lượt

"stream.Duplex"

Duplex Stream: Cánh Cửa Hai Chiều Đầy Quyền Năng Của Node.js

Chào các "dev-er" tương lai, các bạn Gen Z năng động! Hôm nay, thầy Creyt sẽ dẫn các bạn đi khám phá một khái niệm cực kỳ hay ho trong Node.js: stream.Duplex. Nghe tên thôi đã thấy "hai chiều" rồi đúng không? Chính xác! Hãy hình dung nó như một con đường cao tốc mà xe có thể chạy cả hai chiều, hoặc một chiếc điện thoại mà bạn vừa có thể nói, vừa có thể nghe cùng lúc vậy.

1. stream.Duplex là gì và để làm gì?

Trong vũ trụ Node.js, Stream là những đường ống dẫn dữ liệu. Chúng ta có:

  • Readable Stream: Như một vòi nước, chỉ chảy ra. Bạn chỉ có thể đọc dữ liệu từ nó.
  • Writable Stream: Như một cái xô, chỉ đổ vào. Bạn chỉ có thể ghi dữ liệu vào nó.
  • Duplex Stream: Đây chính là "nhân vật chính" của chúng ta. Nó là sự kết hợp "đỉnh cao" của cả ReadableWritable trong cùng một thực thể. Nghĩa là, bạn vừa có thể ghi dữ liệu vào (như một Writable stream), và đọc dữ liệu ra (như một Readable stream) từ chính nó, cùng một lúc!

Để làm gì ư? Đơn giản thôi. Trong nhiều trường hợp, chúng ta cần một "bộ xử lý" trung gian có khả năng nhận dữ liệu vào, làm gì đó với nó, rồi đẩy dữ liệu đã xử lý ra. Hoặc, khi bạn cần một kênh giao tiếp mà cả hai phía đều có thể gửi và nhận thông tin liên tục, không ai phải chờ ai. Duplex stream sinh ra để giải quyết những bài toán "song kiếm hợp bích" như vậy. Nó giống như một "trạm biến hình" vậy, nhận đầu vào, biến đổi, rồi cho ra đầu ra.

2. Code Ví Dụ Minh Hoạ: Trạm Biến Hình Chữ Hoa

Để các bạn dễ hình dung, chúng ta hãy cùng xây dựng một Duplex stream đơn giản. Stream này sẽ nhận bất kỳ dữ liệu chuỗi nào bạn gửi vào, biến nó thành chữ IN HOA, rồi đẩy ra ngoài.

const { Duplex } = require('stream');

// Tạo một Duplex stream tùy chỉnh
class UppercaseDuplexStream extends Duplex {
  constructor(options) {
    super(options);
  }

  // Phương thức _write: xử lý dữ liệu khi được ghi vào stream
  // Trong ví dụ này, chúng ta sẽ biến đổi và đẩy dữ liệu ra ngay lập tức.
  _write(chunk, encoding, callback) {
    const transformedData = chunk.toString().toUpperCase();
    console.log(`[_write] Nhận được: ${chunk.toString()}, Đẩy ra: ${transformedData}`);
    this.push(transformedData); // Đẩy dữ liệu đã biến đổi ra phần Readable của stream
    callback(); // Báo hiệu đã xử lý xong chunk này
  }

  // Phương thức _read: xử lý khi stream được yêu cầu đọc dữ liệu
  // Với cách triển khai mà _write đã push dữ liệu, _read thường không cần làm gì nhiều.
  // Nó chỉ là 'placeholder' để báo hiệu rằng stream này có khả năng đọc.
  // Khi không còn dữ liệu để đọc (nguồn đóng), Node.js sẽ tự động gọi push(null)
  // để báo hiệu kết thúc phần Readable.
  _read(size) {
    // Không cần làm gì ở đây nếu chúng ta push ngay trong _write.
    // Để stream không kết thúc ngay lập tức, ta không push(null) ở đây.
    // Stream sẽ tự động kết thúc phần Readable khi phần Writable kết thúc và không còn dữ liệu để đọc.
  }
}

const uppercaseStream = new UppercaseDuplexStream();

// Ghi dữ liệu vào phần Writable của stream
uppercaseStream.write('hello');
uppercaseStream.write('world');
uppercaseStream.end('nodejs'); // Ghi nốt và báo hiệu kết thúc phần Writable

// Đọc dữ liệu từ phần Readable của stream
uppercaseStream.on('data', (chunk) => {
  console.log(`[onData] Nhận được từ stream: ${chunk.toString()}`);
});

uppercaseStream.on('end', () => {
  console.log('[onEnd] Stream đã kết thúc việc đọc.');
});

Giải thích code:

  • Chúng ta tạo một class UppercaseDuplexStream kế thừa từ Duplex.
  • Phương thức _write(chunk, encoding, callback): Đây là nơi dữ liệu được ghi vào stream. Khi uppercaseStream.write('hello') được gọi, chunk sẽ là 'hello'. Ta biến nó thành chữ hoa và dùng this.push(transformedData) để đẩy nó ra khỏi phần Readable của stream. callback() báo hiệu đã xử lý xong chunk này.
  • Phương thức _read(size): Với cách triển khai này, _read gần như không cần làm gì cụ thể. Nó chỉ là một "lời hứa" rằng stream này có thể đọc được. Dữ liệu đã được push ra từ _write sẽ được on('data') của stream nhận. Khi uppercaseStream.end() được gọi, nó không chỉ báo hiệu phần Writable kết thúc mà còn ngụ ý rằng không còn dữ liệu mới để push ra, từ đó kích hoạt sự kiện end của phần Readable.

3. Mẹo (Best Practices) để Ghi Nhớ và Dùng Thực Tế

  • Nhớ "hai chiều": Luôn hình dung Duplex là một đường ống có thể gửi và nhận dữ liệu qua lại. Nó là một Readable và một Writable "dính" vào nhau.
  • Khi nào dùng Duplex, khi nào dùng Transform?: Thực ra, Transform stream chính là một dạng đặc biệt của Duplex stream, nơi đầu ra Readable được "biến đổi" từ đầu vào Writable. Nếu bạn chỉ cần "biến đổi" dữ liệu (nhận vào A, trả ra B), hãy dùng Transform stream vì nó đơn giản hơn và được thiết kế cho mục đích đó. Nếu bạn cần logic phức tạp hơn, nơi _read_write hoạt động độc lập hơn (ví dụ: một proxy server), thì Duplex là lựa chọn. Trong ví dụ trên, Transform stream sẽ là lựa chọn tự nhiên hơn, nhưng Duplex vẫn có thể làm được.
  • Quản lý Backpressure: Đừng quên cơ chế backpressure của Node.js streams. Nếu bạn ghi dữ liệu vào Duplex quá nhanh mà nó không kịp xử lý và đẩy ra, hệ thống sẽ bị quá tải. Luôn lắng nghe sự kiện drain và kiểm tra giá trị trả về của write() để quản lý luồng dữ liệu.
  • Xử lý Lỗi: Luôn lắng nghe sự kiện error trên stream của bạn. Một lỗi nhỏ trong quá trình xử lý có thể làm sập cả ứng dụng nếu không được bắt.

4. Văn Phong Học Thuật Sâu Của Anh Creyt: "Cái Lõi Của Sự Biến Hóa"

Các bạn thấy đấy, Duplex stream không chỉ là một cái ống dẫn đơn thuần. Nó là "cái lõi của sự biến hóa", nơi dữ liệu có thể được nhào nặn, thay đổi hình dạng, rồi tiếp tục hành trình của mình. Nó cho phép chúng ta tạo ra các "bộ lọc" hay "bộ chuyển đổi" mạnh mẽ ngay giữa dòng chảy dữ liệu.

Hãy nghĩ về nó như một "cổng dịch chuyển" trong game. Bạn bước vào một chiều, dữ liệu của bạn được xử lý, và bạn xuất hiện ở chiều kia với một hình hài mới. Sức mạnh của Duplex nằm ở chỗ nó duy trì một kết nối logic duy nhất cho cả hai hoạt động này, thay vì phải tạo hai đường ống riêng biệt. Điều này cực kỳ hiệu quả khi bạn làm việc với các giao thức mạng phức tạp, nơi yêu cầu và phản hồi thường đi qua cùng một kết nối.

5. Ví Dụ Thực Tế Các Ứng Dụng/Website Đã Ứng Dụng

  • WebSockets: Đây là ví dụ kinh điển nhất! Một kết nối WebSocket là một kênh giao tiếp hai chiều (duplex) giữa client và server. Cả hai phía đều có thể gửi và nhận tin nhắn độc lập. Trong Node.js, các thư viện WebSocket thường sử dụng Duplex stream hoặc các abstraction tương tự để quản lý luồng dữ liệu này.
  • TCP/IP Sockets: Bản thân net.Socket trong Node.js là một Duplex stream. Khi bạn tạo một kết nối TCP, bạn có thể gửi dữ liệu (write) và nhận dữ liệu (read) qua cùng một socket đó. Đây chính là nền tảng của hầu hết các giao tiếp mạng.
  • Proxy Servers: Các máy chủ proxy thường hoạt động như một Duplex stream. Nó nhận yêu cầu từ client (ghi vào), xử lý/chuyển tiếp nó đến server đích, rồi nhận phản hồi từ server đích (đọc từ server) và chuyển tiếp lại cho client (ghi ra client).
  • zlibcrypto modules: Các module này cung cấp các Transform streams (một dạng của Duplex) để nén/giải nén hoặc mã hóa/giải mã dữ liệu. Bạn có thể pipe dữ liệu vào một zlib.createGzip() stream, nó sẽ nén và đẩy dữ liệu đã nén ra.

6. Thử Nghiệm Đã Từng và Hướng Dẫn Nên Dùng Cho Case Nào

Anh Creyt đã từng "vật lộn" với Duplex khi xây dựng một hệ thống proxy tùy chỉnh cho một dự án legacy. Ban đầu, anh nghĩ đến việc dùng hai stream riêng biệt (một Readable từ client, một Writable đến server), nhưng sau đó nhận ra Duplex là giải pháp thanh lịch hơn nhiều. Nó giúp quản lý trạng thái và luồng dữ liệu một cách chặt chẽ, vì cả hai chiều đều thuộc cùng một "thực thể" logic.

Nên dùng Duplex khi:

  • Bạn cần tạo một "bộ lọc" hoặc "bộ chuyển đổi" giữa dòng chảy dữ liệu, nơi dữ liệu đi vào một phía, được xử lý, và đi ra phía kia trên cùng một kênh logic.
  • Bạn đang xây dựng một giao thức mạng tùy chỉnh yêu cầu giao tiếp hai chiều qua một kết nối duy nhất (như WebSockets hoặc các giao thức RPC qua TCP).
  • Bạn muốn tạo một "bridge" (cầu nối) giữa hai nguồn/đích dữ liệu khác nhau, nơi bridge này vừa nhận, vừa gửi.
  • Bạn cần mô phỏng một thiết bị I/O hai chiều (như một terminal ảo).

Nên cân nhắc khi không dùng Duplex (và dùng Readable hoặc Writable thay):

  • Nếu bạn chỉ cần đọc dữ liệu từ một nguồn (ví dụ: đọc file, đọc API response).
  • Nếu bạn chỉ cần ghi dữ liệu vào một đích (ví dụ: ghi file log, gửi dữ liệu lên API).
  • Nếu bài toán của bạn đơn giản chỉ là chuyển đổi dữ liệu một chiều (nhận vào A, trả ra B), Transform stream (là một dạng Duplex nhưng chuyên biệt hơn) sẽ là lựa chọn tốt hơn vì nó được thiết kế chính xác cho mục đích đó và dễ sử dụng hơn.

Nhớ nhé các bạn, Duplex stream là một công cụ mạnh mẽ, nhưng hãy dùng nó đúng lúc, đúng chỗ. Đừng "lạm dụng" nó cho những bài toán đơn giản, kẻo lại "biến voi thành kiến" đấy! Hãy thực hành nhiều, thử nghiệm nhiều, và các bạn sẽ thấy sức mạnh của nó. Chúc các bạn "code như rồng bay phượng múa"!

Thuộc Series: Nodejs

Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

#tech #cyberpunk #laravel
Chỉnh sửa bài viết

Bình luận (0)

Vui lòng Đăng Nhập để Bình luận

Hỗ trợ Markdown cơ bản
Nguyễn Văn A
1 ngày trước

Tính năng này đỉnh quá ad ơi, chờ mãi mới thấy một blog Tiếng Việt có UI/UX xịn như vầy!