Prozesssubstitution für Streaming ohne Zwischenarchiv
Nutzen Sie die Bash-Prozesssubstitution, wenn ein Befehl Dateinamen für Streams benötigt, und eine Pipeline, wenn ein Befehl die Standardausgabe eines anderen lesen kann. Diese Anleitung zeigt, wie Sie Fehler in beiden Fällen erkennen, ein komprimiertes tar-Archiv an einen lokalen HTTP-Empfänger hochladen und anschließend die empfangenen Dateien vergleichen.
Verwenden Sie Linux mit Bash 5.2.21 oder neuer, GNU sort, diff, cmp, GNU tar 1.35, gzip 1.12 oder neuer,
cURL 8.5.0 oder neuer und Node.js 24.15.0 oder neuer. Die Beispiele wurden auch mit Bash 5.3.15,
gzip 1.14, cURL 8.22.0 und Node.js 26.8.1 getestet. Führen Sie gespeicherte Shell-Skripte mit
bash aus, nicht mit sh;
der Empfänger nutzt die native TypeScript-Unterstützung von Node.js
und die explizite Erweiterung .mts. Für den Empfänger müssen keine Pakete
installiert werden.
Fügen Sie zunächst Folgendes in Bash ein, um einen kleinen Datensatz zu erstellen. Die Klammern
lassen Ihr aktuelles Verzeichnis unverändert, und && stoppt die Einrichtung,
wenn stream-demo bereits existiert oder der Verzeichniswechsel fehlschlägt.
(
mkdir stream-demo &&
cd stream-demo &&
mkdir input &&
printf 'pear\napple\n' >input/left.txt &&
printf 'apple\npear\n' >input/right.txt &&
printf '\000\377\200binary\n' >input/bytes.bin &&
: >input/empty
)
Öffnen Sie zwei Terminals in stream-demo. Speichern Sie dort die folgenden Skripte
und führen Sie alle weiteren Befehle aus diesem Verzeichnis aus.
Prozesssubstitution verstehen
Mit <(command) startet Bash einen Erzeuger asynchron und übergibt einen Dateinamen,
der auf dessen Ausgabe verweist. Unter Linux sieht dieser häufig wie /dev/fd/63
aus. Es handelt sich um einen Stream: Verbraucher können daher nicht davon ausgehen, darin wie in
einer regulären Datei frei positionieren zu können. Die umgekehrte Form,
>(command), liefert einen Dateinamen, in den Sie als Eingabe für diesen Befehl
schreiben können. Siehe das
Bash-Handbuch zur Prozesssubstitution.
Der verlockende Einzeiler diff <(sort file1.txt) <(sort file2.txt) hat eine Tücke: Wenn beide Dateien fehlen,
können beide Erzeuger fehlschlagen, während diff zwei leere Streams
vergleicht und null zurückgibt. pipefail erfasst die Statuswerte dieser
Hintergrundprozesse nicht.
Speichern Sie Folgendes als compare.sh. Das Skript hält die Streams offen,
erfasst sofort die PID jedes Erzeugers und wartet auf beide Erzeuger, bevor es den Status des
Vergleichs meldet:
#!/usr/bin/env bash
if (( $# != 2 )); then
printf 'Usage: bash compare.sh FILE1 FILE2\n' >&2
exit 2
fi
exec {left}< <(LC_ALL=C sort -- "$1")
left_pid=$!
exec {right}< <(LC_ALL=C sort -- "$2")
right_pid=$!
diff "/dev/fd/$left" "/dev/fd/$right"
comparison=$?
exec {left}<&- {right}<&-
wait "$left_pid"; left_status=$?
wait "$right_pid"; right_status=$?
if (( left_status != 0 || right_status != 0 )); then
printf 'Cannot compare: a sort failed.\n' >&2
exit 2
fi
exit "$comparison"
Führen Sie bash compare.sh input/left.txt input/right.txt aus: Keine Ausgabe und Status null bedeuten, dass die
sortierten Inhalte übereinstimmen. Unterschiedliche Inhalte ergeben eins; ein fehlgeschlagener
Erzeuger ergibt zwei. Das Skript erfasst die Statuswerte bewusst ohne set -e,
damit ein fehlgeschlagener Befehl das verbleibende wait nicht umgehen kann.
Bash dokumentiert das Warten auf Prozesssubstitutionen in seiner
Referenz zu wait.
Vorteile von Streaming ohne Zwischenarchiv
Der folgende Upload vermeidet ein Zwischenarchiv auf Senderseite. Quelldateien werden weiterhin
gelesen, und der Empfänger schreibt das endgültige Archiv. „Ohne Festplattenzugriff“ beschreibt
also die Zwischenübertragung, nicht den gesamten Ablauf. Selbst
sort
kann große Eingaben in temporäre Dateien auslagern.
Ohne zwischengespeichertes Archiv entfallen dessen Speicherbedarf und Schreib-/Lesezyklus. Das macht den Ablauf nicht garantiert schneller: Komprimierung, Speicher und Netzwerk können jeweils den Durchsatz begrenzen. Außerdem verzichten Sie auf eine Kopie mit freier Positionierung, die cURL nach einem Fehler erneut lesen könnte.
Daten zwischen Befehlen streamen
Für tar und cURL reicht eine gewöhnliche Pipe:
tar schreibt ein Archiv nach stdout, cURL liest stdin. Beginnen Sie mit
einem Empfänger, der die Anfrage tatsächlich akzeptiert. Speichern Sie Folgendes als
receiver.mts:
import { createWriteStream } from 'node:fs'
import { createServer } from 'node:http'
import { pipeline } from 'node:stream'
const server = createServer((request, response) => {
if (request.method !== 'PUT' || request.url !== '/archive') {
request.resume()
response.writeHead(404).end()
return
}
pipeline(request, createWriteStream('received.tar.gz', { flags: 'wx', mode: 0o600 }), (error) => {
if (error) {
console.error('Upload failed; inspect received.tar.gz before retrying.')
response.destroy()
return
}
response.writeHead(201).end()
})
})
Hängen Sie den folgenden Startcode an dieselbe Datei an. Port null lässt das Betriebssystem einen freien Port wählen; ein optionales numerisches Argument legt einen bestimmten Port fest.
const port = Number(process.argv[2] ?? 0)
if (!Number.isInteger(port) || port < 0 || port > 65535) {
throw new Error('Port must be an integer from 0 to 65535')
}
server.on('error', (error) => {
console.error('Receiver could not listen:', error.message)
process.exitCode = 1
})
server.listen(port, '127.0.0.1', () => {
const address = server.address()
if (!address || typeof address === 'string') throw new Error('Missing listening address')
console.log(`http://127.0.0.1:${address.port}/archive`)
})
Führen Sie im ersten Terminal node receiver.mts aus und lassen Sie den Prozess laufen.
Kopieren Sie die ausgegebene URL. Der Empfänger bindet sich nur an die Loopback-Schnittstelle,
akzeptiert HTTP/1.1-Uploads mit Chunked-Übertragung und streamt Bytes nach
received.tar.gz mithilfe von
pipeline aus Node.
Das Flag wx verweigert das Schreiben, wenn die Datei bereits existiert.
Ein fehlgeschlagener Upload kann eine unvollständige Datei hinterlassen; ein Speicherfehler schließt
die Verbindung. Dieser kleine lokale Empfänger hat weder Authentifizierung noch eine Richtlinie
zur Upload-Größe oder eine Archivvalidierung. Halten Sie ihn privat und führen Sie jeweils nur
einen Upload aus.
Speichern Sie als Nächstes diese Argumentprüfung als Anfang von upload.sh:
#!/usr/bin/env bash
if (( $# != 2 )); then
printf 'Usage: bash upload.sh DIRECTORY URL\n' >&2
exit 2
fi
if [[ $2 != https://* && ! $2 =~ ^http://127[.]0[.]0[.]1:[0-9]+/ ]]; then
printf 'Use HTTPS, or loopback HTTP for this demo.\n' >&2
exit 2
fi
Hängen Sie die Pipeline an upload.sh an:
set -o pipefail
if status=$(tar -czf - -C "$1" . |
curl --disable -fsS --http1.1 --noproxy 127.0.0.1 --globoff \
--connect-timeout 5 --max-time 60 --proto '=http,https' \
--header 'Content-Type: application/gzip' --upload-file - \
--output /dev/null --write-out '%{http_code}' --url "$2"
) && [[ $status == 201 ]]; then
printf 'Archive sent; verify the received files.\n'
else
printf 'Upload failed; inspect the receiver before retrying.\n' >&2
exit 1
fi
Führen Sie im zweiten Terminal bash upload.sh input 'COPIED_URL' aus und ersetzen Sie
COPIED_URL durch die vollständige URL des Empfängers. Lassen Sie das
Quellverzeichnis während der Übertragung unverändert. Das Archiv des Empfängers liegt außerhalb
von input, sodass tar nicht versehentlich seine
eigene Ausgabe einschließen kann.
Hier erzeugt tar -czf - ein gzip-komprimiertes Archiv. Der Inhaltstyp ist daher
application/gzip.
Die cURL-Option --upload-file - liest stdin und sendet
eine HTTP-PUT-Anfrage. --disable verhindert, dass eine persönliche
.curlrc das Verhalten des Beispiels verändert. Es gibt weder Weiterleitungen
noch automatische Wiederholungsversuche. Das Skript verlangt von diesem Empfänger HTTP 201 und eine
erfolgreiche Pipeline: pipefail
macht einen Fehler von tar sichtbar, selbst wenn cURL die empfangenen Bytes
erfolgreich sendet.
Fügen Sie nach der Erfolgsmeldung Folgendes in das zweite Terminal ein, um das Archiv in ein neues Verzeichnis zu entpacken und den Datensatz Byte für Byte zu vergleichen:
(
mkdir unpacked &&
tar -xzf received.tar.gz -C unpacked &&
cmp input/left.txt unpacked/left.txt &&
cmp input/right.txt unpacked/right.txt &&
cmp input/bytes.bin unpacked/bytes.bin &&
cmp input/empty unpacked/empty &&
printf 'All four files match.\n'
)
Dabei werden gewöhnlicher Text, Binärbytes ohne Textinhalt und eine leere Datei geprüft. Wenn das
Verzeichnis unpacked bereits existiert, wird das Entpacken gestoppt, statt
frühere Ergebnisse zu überschreiben. Entpacken Sie nur Archive, denen Sie vertrauen.
Stoppen Sie den Empfänger zum Abschluss mit Strg+C.
Fehlerbehebung und bewährte Verfahren
Wenn tar seine Eingabe nicht lesen kann, gibt das Skript einen Status
ungleich null zurück, selbst wenn der Empfänger 201 zurückgibt. Diese HTTP-Antwort bedeutet, dass
er einen vollständigen Anfragekörper gespeichert hat. Sie sagt nicht aus, ob der Erzeuger
fehlgeschlagen ist oder das Archiv die beabsichtigten Dateien enthält. Betrachten Sie das Ergebnis
am Ziel bis zur Überprüfung als möglicherweise fehlerhaft.
Auch eine HTTP-Ablehnung, ein Verbindungsabbruch oder eine Zeitüberschreitung von cURL lässt den Upload fehlschlagen. Bricht die Verbindung nach dem Speichern ab, bevor die Antwort cURL erreicht, bleibt das Ergebnis ungewiss: Die Datei kann bereits vollständig sein. Wenn der Empfänger nicht startet, prüfen Sie vor dem Upload seine Fehlermeldung. Ein belegter Port beweist nicht, dass Ihr Empfänger läuft.
Fügen Sie einem cURL-Prozess, der aus einer Pipe liest, nicht --retry in der
Erwartung hinzu, dass er das Archiv neu erzeugt. Bereits verbrauchte Bytes lassen sich nicht
zurückspulen, und cURL startet tar nicht neu. Die
Dokumentation zu Wiederholungsversuchen warnt auch vor umgeleiteter
Eingabe. Um dieses Beispiel erneut auszuführen, stoppen Sie den Empfänger, prüfen Sie dessen
received.tar.gz und verschieben oder entfernen Sie diese Datei. Starten Sie dann den
Empfänger erneut und führen Sie bash upload.sh mit der neu ausgegebenen URL nochmals
aus. Jeder Aufruf startet einen neuen Erzeuger und sendet das gesamte Archiv. Nutzen Sie zur
Überprüfung ein neues Verzeichnis zum Entpacken; dieser Ablauf unterstützt kein Fortsetzen.
Erweiterte Komprimierungsoptionen
Bleiben Sie für diese Anleitung bei gzip, damit Erzeuger, Dateiname, Inhaltstyp und Entpackbefehl zusammenpassen. Ein Wechsel des Komprimierungsprogramms ändert alle vier. Die Optimierung der Komprimierung ist eine andere Entscheidung als die für Streaming: Messen Sie mit Ihren eigenen Daten, bevor Sie ein anderes Format wählen.
Einsatz in der Praxis
Ein tatsächliches HTTP-Ziel muss PUT, das Streaming-Framing der Anfrage und den von Ihnen erwarteten Erfolgsstatus unterstützen. Dies ist weder ein Multipart-Formular-Upload noch ein SSH-Entpackbefehl. Nutzen Sie für Remote-Übertragungen HTTPS und die dokumentierte Authentifizierung des Ziels; schreiben Sie keine Passwörter in die Befehlszeile. Das Skript erlaubt unverschlüsseltes HTTP nur für die Loopback-Demonstration und folgt keinen Weiterleitungen.
Wenn Sie erneut sendbare Bytes, eine bekannte Inhaltslänge oder eine Überprüfung vor der Veröffentlichung benötigen, kann ein zwischengespeichertes Archiv die bessere Abwägung sein. Ein Empfänger im Produktivbetrieb benötigt außerdem eigene Validierungs- und Veröffentlichungsrichtlinien, bevor er ein hochgeladenes Objekt anderen Lesern zugänglich macht.
Entscheiden Sie, wann Streaming sinnvoll ist
Nutzen Sie Prozesssubstitution für Werkzeuge, die Dateinamen für Streams verlangen, und eine Pipeline für einen einzelnen Erzeuger und Verbraucher. Berücksichtigen Sie in beiden Fällen den Exit-Status jedes Erzeugers, bevor Sie den Vorgang als erfolgreich bewerten. Wenn Sie verwaltete Komprimierung nutzen möchten, statt diesen Übertragungsweg selbst zu pflegen, sehen Sie sich unseren Dienst zur Dateikomprimierung an.
