Strumienie Node.js: strumienie plików, potoki i zdarzenia

⚡ Inteligentne podsumowanie

Strumienie Node.js odczytują i zapisują dane w małych porcjach zamiast ładować całe pliki do pamięci. Na tej stronie opisano tworzenie strumieni plików, łączenie potoków, obsługę przeciwciśnienia i model zdarzeń, który zapewnia wydajność przesyłania danych na dużą skalę.

  • ???? Typy strumieni: Node.js organizuje każdy strumień jako możliwy do odczytu, zapisu, dupleksu lub transformacji, a każde API Node.js zwraca jeden z tych czterech typów.
  • 📂 Strumień pliku: Funkcje fs.createReadStream() i fs.createWriteStream() odczytują i zapisują pliki fragmentami, bez konieczności ładowania całego pliku do pamięci.
  • 🔗 Piping: Metoda pipe() łączy strumień czytelny bezpośrednio ze strumieniem zapisywalnym i automatycznie przesyła dane między nimi.
  • ⚖️. Ciśnienie zwrotne: write() zwraca false, gdy bufor jest pełny, a zdarzenie drain sygnalizuje, kiedy można bezpiecznie wznowić zapisywanie.
  • 📡 Transmisja wydarzeń: data, end, error i finish oznaczają kluczowe momenty w cyklu życia strumienia i sterują większością kodu obsługującego strumień.
  • Niestandardowe wydarzenia: Moduł zdarzeń i EventEmitter umożliwiają dowolnej aplikacji Node.js definiowanie, emitowanie i nasłuchiwanie własnych nazwanych zdarzeń.
  • 🛠️. Kontrola słuchacza: once(), listenerCount() i zdarzenie newListener zarządzają liczbą uruchomień modułu obsługi i sposobem działania modułów nasłuchujących tracked.

Typy strumieni w Node.js

Każdy strumień udostępniany przez Node.js należy do jednego z czterech typów bazowych, a wiedza o tym, jaki typ zwraca dane API, wyjaśnia, jak ten strumień się zachowuje, zanim zostanie napisany jakikolwiek kod. Moduł strumienia dostarczany z Node.js definiuje te kategorie, a interfejsy API plików, sieci i kompresji używane w tym artykule są zbudowane na ich bazie.

Typ strumienia Kierunek danych Przykładowe API Typowy przypadek użycia
Readable Tylko źródło (wypływ danych) fs.createReadStream() Odczyt pliku, odbieranie treści żądania HTTP
Zapisu Tylko miejsce docelowe (przepływ danych) fs.createWriteStream() Zapisywanie pliku, wysyłanie odpowiedzi HTTP
dupleks W obu kierunkach, niezależnie net.Socket (gniazdo TCP) Dwukierunkowa komunikacja sieciowa
Przekształcać W obu kierunkach dane są modyfikowane w trakcie przesyłu zlib.createGzip() Kompresja lub kodowanie danych w trakcie procesu

Strumień czytelny i strumień zapisywalny to dwa podstawowe bloki używane w tym artykule; oba zasilają również Serwer WWW HTTP w Node.js. Strumień dupleksowy łączy obie role w jednym obiekcie, podczas gdy strumień transformacji to strumień dupleksowy, którego dane wyjściowe modyfikują dane wejściowe, np. kompresja gzip stosowana w trakcie strumienia.

Strumień plików w Node.js

Po zdefiniowaniu czterech kategorii strumieni, dalsza część artykułu będzie poświęcona tworzeniu transferu plików przy użyciu interfejsów API strumieni do odczytu i zapisu.

Węzeł szeroko wykorzystuje strumienie jako mechanizm przesyłania danych.

Na przykład, gdy wysyłasz cokolwiek do konsoli za pomocą funkcji console.log, w rzeczywistości używasz strumienia do wysyłania danych do konsoli.

Node.js ma także możliwość strumieniowego przesyłania danych z plików, dzięki czemu można je odpowiednio odczytać i zapisać. Przyjrzymy się teraz przykładowi wykorzystania strumieni do odczytu i zapisu plików. W tym przykładzie musimy wykonać poniższe kroki

Krok 1) Utwórz plik o nazwie data.txt zawierający poniższe dane. Załóżmy, że plik ten jest przechowywany na dysku D naszej lokalnej maszyny.

Samouczek dotyczący Node.js

Wprowadzenie

Wydarzenia

Generators

Łączność danych

Używanie jaśminu

Krok 2) Napisz odpowiedni kod, który będzie korzystał ze strumieni do odczytania danych z pliku.

Strumień plików w Node.js

var fs = require("fs");
var stream;
stream = fs.createReadStream("D://data.txt");

stream.on("data", function(data) {
    var chunk = data.toString();
    console.log(chunk);
}); 

Code Wyjaśnienie:-

  1. Najpierw musimy dołączyć moduły „fs”, które zawierają całą funkcjonalność wymaganą do tworzenia strumieni.
  2. Następnie tworzymy czytelny strumień, używając metody – createReadStream. Jako dane wejściowe podajemy lokalizację naszego pliku data.txt.
  3. Funkcja steam.on jest obsługą zdarzeń i w niej określamy pierwszy parametr jako „data”. Oznacza to, że za każdym razem, gdy dane napływają do strumienia z pliku, należy wykonać funkcję wywołania zwrotnego. W naszym przypadku definiujemy funkcję wywołania zwrotnego, która wykona 2 podstawowe kroki. Pierwsza polega na konwersji danych odczytanych z pliku na ciąg znaków. Drugim byłoby wysłanie przekonwertowanego ciągu jako danych wyjściowych do konsoli.
  4. Pobieramy każdy fragment danych odczytany ze strumienia danych i konwertujemy go na ciąg znaków.
  5. Na koniec wysyłamy dane wyjściowe każdego fragmentu skonwertowanego ciągu znaków do konsoli.

Wyjście:

Strumień plików w Node.js

  • Jeśli kod zostanie wykonany poprawnie, w konsoli zobaczysz powyższe dane wyjściowe. Dane wyjściowe będą takie same, jak w pliku data.txt.

Zapis do pliku

W ten sam sposób, w jaki tworzymy strumień odczytu, możemy również utworzyć strumień zapisu do zapisu danych do pliku. Najpierw utwórzmy pusty plik bez zawartości o nazwie data.txt. Załóżmy, że plik ten znajduje się na dysku D naszego komputera.

Poniższy kod pokazuje w jaki sposób możemy zapisać dane do pliku.

Strumień plików w Node.js

var fs = require("fs");
var stream;
stream = fs.createWriteStream("D://data.txt");

stream.write("Tutorial on Node.js")
stream.write("Introduction")
stream.write("Events")
stream.write("Generators")
stream.write("Data Connectivity")
stream.write("Using Jasmine") 

Code Wyjaśnienie:-

  1. Tworzymy zapisywalny strumień metodą createWriteStream. Jako dane wejściowe podajemy lokalizację naszego pliku data.txt.
  2. Następnie użyliśmy metody stream.write do zapisania różnych linii tekstu do naszego pliku tekstowego. Strumień zajmie się zapisaniem tych danych do pliku data.txt.

Jeśli otworzysz plik data.txt, zobaczysz teraz w pliku następujące dane

Samouczek dotyczący Node.js

Wprowadzenie

Wydarzenia

Generators

Łączność danych

Korzystanie z Jaśmin

Potoki w Node.js

Możliwość niezależnego odczytywania i zapisywania strumieni jest przydatna, ale Node.js pozwala również na bezpośrednie połączenie strumienia czytelnego ze strumieniem zapisywalnym, co pozwala na przesyłanie danych bez konieczności ręcznego buforowania kodu.

W aplikacjach Node strumienie można łączyć za pomocą metody pipe(), która przyjmuje dwa argumenty:

  • Wymagany zapisywalny strumień, który działa jako miejsce docelowe danych i
  • Opcjonalny obiekt używany do przekazywania opcji.

Typowy przykład użycia potoków, jeśli chcesz przenieść dane z jednego pliku do drugiego.

Zobaczmy więc przykład, jak możemy przenieść dane z jednego pliku do drugiego za pomocą potoków.

Krok 1) Utwórz plik o nazwie datainput.txt zawierający poniższe dane. Załóżmy, że plik ten jest przechowywany na dysku D naszej lokalnej maszyny.

Samouczek dotyczący Node.js

Wprowadzenie

Wydarzenia

Generators

Łączność danych

Używanie jaśminu

Krok 2) Utwórz pusty plik o nazwie dataOutput.txt i umieść go na dysku D komputera lokalnego.

Krok 3) Napisz poniższy kod realizujący transfer danych z pliku datainput.txt do pliku dataOutput.txt.

Potoki w Node.js

var fs = require("fs");
var readStream = fs.createReadStream("D://datainput.txt");
var writeStream = fs.createWriteStream("D://dataOutput.txt");
readStream.pipe(writeStream);

Code Wyjaśnienie:-

  1. Najpierw tworzymy „readstream” do naszego pliku datainput.txt, który zawiera wszystkie nasze dane, które należy przenieść do nowego pliku.
  2. Następnie musimy utworzyć „writestream” do naszego pliku dataOutput.txt, który jest naszym pustym plikiem i jest miejscem docelowym przesyłania danych z pliku datainput.txt.
  3. Następnie używamy polecenia potoku, aby przesłać dane ze strumienia odczytu do strumienia zapisu. Polecenie potok pobierze wszystkie dane, które przyjdą do strumienia odczytu i wypchnie je do strumienia zapisu.

Jeśli teraz otworzysz plik dataOutput.txt, zobaczysz wszystkie dane, które znajdowały się w pliku datainput.txt.

Zdarzenia strumieniowe i presja zwrotna w Node.js

Wywołanie pipe(), jak pokazano powyżej, ukrywa dwie mechaniki, które mają znaczenie na dużą skalę: zdarzenia emitowane przez każdy strumień oraz system przeciwciśnienia, który zapobiega przytłoczeniu wolnego celu przez szybkie źródło.

  • Dane: emitowane przez czytelny strumień za każdym razem, gdy dostępny jest nowy fragment.
  • koniec: emitowane, gdy czytelny strumień dostarczy swój ostatni fragment.
  • koniec: emitowane przez zapisywalny strumień po opróżnieniu wszystkich danych w kolejce.
  • błąd: emitowane przez dowolny typ strumienia w przypadku awarii; zawsze obsługuj tę sytuację, w przeciwnym razie proces ulegnie awarii.

Poniższy przykład nasłuchuje wszystkich czterech zdarzeń, jednocześnie ręcznie zarządzając ciśnieniem wstecznym, czyli tak samo, jak pipe() zapewnia to automatycznie.

var fs = require("fs");
var readStream = fs.createReadStream("D://data.txt");
var writeStream = fs.createWriteStream("D://dataOutput.txt");

readStream.on("data", function(chunk) {
    var ok = writeStream.write(chunk);
    if (!ok) {
        readStream.pause();
    }
});

writeStream.on("drain", function() {
    readStream.resume();
});

readStream.on("end", function() {
    writeStream.end();
});

writeStream.on("finish", function() {
    console.log("Write completed.");
});

readStream.on("error", function(err) {
    console.log(err);
});

Zjawisko backpressure występuje, gdy strumień czytelny generuje dane szybciej, niż strumień zapisywalny może je skonsumować. Metoda write() zwraca false po zapełnieniu bufora, więc powyższy kod wstrzymuje źródło do momentu, aż źródło wyemituje zdarzenie drenażu, sygnalizując, że można je bezpiecznie wznowić. Metoda pipe() wykonuje ten cykl automatycznie.

⚠ Wskazówka: W środowisku produkcyjnym należy preferować funkcję pipeline() z modułu strumieniowego zamiast ręcznego łańcucha pipe(), ponieważ przekazuje ona błędy z każdego strumienia i automatycznie zamyka uchwyty plików.

Zdarzenia w Node.js

Strumienie są jednym z przykładów szerszego podejścia Node.js do projektowania opartego na zdarzeniach. W dalszej części artykułu przyjrzymy się, jak działają zdarzenia, wykorzystując tę ​​samą klasę EventEmitter, z której wewnętrznie korzystają strumienie.

Zdarzenia stanowią jedną z kluczowych koncepcji Node.js i czasami Node.js jest określany jako framework oparty na zdarzeniach.

Zasadniczo wydarzenie to coś, co się dzieje. Na przykład, jeśli zostanie nawiązane połączenie z bazą danych, wyzwalane jest zdarzenie połączenia z bazą danych. Programowanie sterowane zdarzeniami polega na tworzeniu funkcji, które będą uruchamiane w przypadku wyzwolenia określonych zdarzeń.

Spójrzmy na podstawowy przykład definiowania zdarzenia w Node.js.

Stworzymy wydarzenie o nazwie „data_received”. Po wywołaniu tego zdarzenia do konsoli zostanie wysłany tekst „dane otrzymane”.

Zdarzenia w Node.js

var events = require('events');
var eventEmitter = new events.EventEmitter();
eventEmitter.on('data_received', function() {
    console.log('data received succesfully.');
});

eventEmitter.emit('data_received'); 

Code Wyjaśnienie:-

  1. Użyj funkcji require, aby uwzględnić moduł „zdarzenia”. Dzięki temu modułowi będziesz mógł tworzyć zdarzenia w Node.js.
  2. Utwórz nowy emiter zdarzeń. Służy do powiązania zdarzenia, którym w naszym przypadku jest „data_received” z funkcją wywołania zwrotnego zdefiniowaną w kroku 3.
  3. Definiujemy funkcję sterowaną zdarzeniami, która mówi, że jeśli w przypadku wywołania zdarzenia „data_received” powinniśmy wypisać na konsolę tekst „data_received”.
  4. Wreszcie mamy ręczne wyzwalanie naszego zdarzenia za pomocą funkcji eventEmiter.emit. Spowoduje to wyzwolenie zdarzenia data_received.

Po uruchomieniu programu do konsoli zostanie wysłany tekst „dane otrzymane”, jak pokazano poniżej.

Zdarzenia w Node.js

Emisja zdarzeń

Oprócz emitowania i nasłuchiwania zdarzeń Node.js udostępnia dodatkowe metody kontrolujące sposób rejestrowania i sprawdzania obiektów nasłuchujących.

Podczas definiowania zdarzeń istnieją różne metody wywoływania zdarzeń. W tym temacie skupimy się na szczegółowym omówieniu każdego z nich.

  1. Jednorazowe procedury obsługi zdarzeń

Czasami możesz chcieć zareagować na wydarzenie dopiero za pierwszym razem, gdy ono nastąpi. W takich sytuacjach można użyć metody Once().

Zobaczmy, jak możemy wykorzystać metodę Once do obsługi zdarzeń.

Emisja zdarzeń

Code Wyjaśnienie:-

  1. Tutaj używamy metody „once”, aby powiedzieć, że w przypadku zdarzenia „data_received” funkcja wywołania zwrotnego powinna zostać wykonana tylko raz.
  2. Tutaj ręcznie wyzwalamy zdarzenie „data_received”.
  3. Gdy zdarzenie „data_received” zostanie ponownie wywołane, tym razem nic się nie stanie. Dzieje się tak ze względu na pierwszy krok, w którym powiedzieliśmy, że zdarzenie może zostać wywołane tylko raz.

Jeśli kod zostanie wykonany poprawnie, w dzienniku pojawi się komunikat „data_received pomyślnie”. Komunikat ten pojawi się w konsoli tylko raz.

  1. Sprawdzanie detektorów zdarzeń

W dowolnym momencie swojego istnienia do emitera zdarzeń może być podłączonych zero lub więcej słuchaczy. Elementy nasłuchujące dla każdego typu zdarzenia można sprawdzić na kilka sposobów.

Jeśli interesuje Cię tylko określenie liczby podłączonych słuchaczy, nie szukaj dalej niż metoda EventEmitter.listenerCount().

(Uwaga: Słuchacze są ważni, ponieważ główny program powinien wiedzieć, czy słuchacze są dodawani na bieżąco do wydarzenia, w przeciwnym razie program będzie działać nieprawidłowo, ponieważ zostaną wywołani dodatkowi słuchacze.)

Emisja zdarzeń

Code Wyjaśnienie:-

  1. Definiujemy typ eventEmitter, który jest wymagany do korzystania z metod związanych ze zdarzeniami.
  2. Następnie definiujemy obiekt zwany emiterem, który będzie używany do definiowania naszych procedur obsługi zdarzeń.
  3. Tworzymy 2 procedury obsługi zdarzeń, które w zasadzie nic nie robią. W naszym przykładzie jest to proste, aby pokazać, jak działa metoda ListenerCount.
  4. Teraz, gdy wywołasz metodę ListenerCount dla naszego zdarzenia data_received, wyśle ​​ona liczbę detektorów zdarzeń dołączonych do tego zdarzenia w dzienniku konsoli.

Jeżeli kod zostanie wykonany poprawnie, w logu konsoli pojawi się wartość 2.

  1. Wydarzenie nowego słuchacza

Za każdym razem, gdy rejestrowana jest nowa procedura obsługi zdarzenia, emiter zdarzeń emituje zdarzenie newListener. To zdarzenie służy do wykrywania nowych programów obsługi zdarzeń. Zwykle używasz zdarzenia newListener, gdy musisz przydzielić zasoby lub wykonać jakąś akcję dla każdego nowego modułu obsługi zdarzenia.

Emisja zdarzeń

var events = require('events');
var eventEmitter = events.EventEmitter;
var emitter = new eventEmitter();
emitter.on("newListener", function(eventName, listener) {
    console.log("Added listener for " + eventName + " events");
});
emitter.on('data_received', function() {});
emitter.on('data_received', function() {}); 

Code Wyjaśnienie:-

  1. Tworzymy nowy moduł obsługi zdarzeń dla zdarzenia 'newListener'. Tak więc za każdym razem, gdy rejestrowany jest nowy moduł obsługi zdarzeń, w konsoli wyświetlany jest tekst „Added listener for” + nazwa zdarzenia.
  2. Tutaj zapisujemy do konsoli tekst „Dodano słuchacza dla” + nazwę zdarzenia dla każdego zarejestrowanego zdarzenia.
  3. Definiujemy 2 procedury obsługi zdarzeń dla naszego zdarzenia „data_received”.

Jeżeli powyższy kod zostanie wykonany poprawnie, w konsoli wyświetli się poniższy tekst. Pokazuje tylko, że procedura obsługi zdarzenia „newListener” została uruchomiona dwukrotnie.

Dodano detektor zdarzeń data_received

Dodano detektor zdarzeń data_received

FAQ

Funkcja fs.readFile() ładuje cały plik do pamięci przed jego zwróceniem, natomiast funkcja fs.createReadStream() dostarcza plik w małych fragmentach w miarę ich pojawiania się. Strumienie zużywają znacznie mniej pamięci w przypadku dużych plików i pozwalają na rozpoczęcie przetwarzania przed zakończeniem odczytu.

Tak. Wywołaj .pipe() raz dla każdego celu, np. readStream.pipe(destinationA), a następnie readStream.pipe(destinationB). Node.js dostarcza te same fragmenty do każdego celu, a każdy zapisywalny strumień nadal niezależnie zarządza swoim własnym ciśnieniem wstecznym.

Tak. Przepuść strumień czytelny przez strumień transformacji, taki jak zlib.createGzip(), a następnie do strumienia zapisywalnego: readStream.pipe(zlib.createGzip()).pipe(writeStream). Plik kompresuje się podczas strumieniowania, bez ładowania całego pliku do pamięci.

Potrafią szybko tworzyć działające szablony readStream i pipe, ale często pomijają obsługę zdarzeń błędów i obsługę backpressure. Zawsze sprawdzaj wygenerowany przez AI kod strumienia pod kątem obu luk przed użyciem go w środowisku produkcyjnym.

Mogą one sugerować prawdopodobne przyczyny, takie jak brakujący odbiornik strumienia lub nieobsłużone zdarzenie błędu, ale potwierdzenie rzeczywistej przyczyny i tak wymaga ręcznego sprawdzenia stanu strumienia i dzienników aplikacji.

Podsumuj ten post następująco: