Themen
Binaere Prozesskanaele und ByteString
Dieses Kapitel ist fuer den Moment gedacht, an dem Text nicht mehr reicht.
Sobald du mit gzip, Bilddaten, Checksummen, eigenen kleinen Protokollen oder
Payloads mit 0x00 arbeiten willst, bist du mit normalem Text-I/O schnell an
einer Grenze. Dann kommen ByteString und die Byte-APIs von Process und
System ins Spiel.
Die Kurzversion#
Es gibt jetzt drei sinnvolle Ebenen fuer Prozesskommunikation:
- Text:
Process.write(...),Process.read_all(...),System.send_text(...),System.receive_text(...) - Normale Antra-Werte:
System.send(...),System.receive(...) - Rohe Bytes:
Process.write_bytes(...),Process.read_bytes(...),Process.read_all_bytes(...),System.send_bytes(...),System.receive_bytes(...)
Die Regel ist einfach:
- Wenn die Gegenstelle Text spricht, nimm Text.
- Wenn beide Seiten Antra-Werte sprechen, nimm
System.send/receive. - Wenn die Gegenstelle echte Bytes erwartet oder liefert, nimm
ByteString.
Was ein ByteString ist#
Ein ByteString ist keine normale Text-String-Instanz, sondern ein opaker
Binärwert.
Das ist wichtig, weil manche Bytes kein gueltiger UTF-8-Text sind oder als Text eine andere Bedeutung bekommen wuerden.
Ein paar typische Helfer:
import Antra::Bytes as Bytes
my a = Bytes.from_hex("00ff4142")
println(a)
println(Bytes.len(a))
println(Bytes.to_hex(a))len(a) ist dasselbe wie Bytes.len(a) (die Anzahl Bytes), und ==
vergleicht zwei ByteStrings nach ihrem Inhalt.
Wann Process.write_bytes(...) richtig ist#
Process.write(...) ist fuer Text.
Process.write_bytes(...) ist fuer alles, was unveraendert als rohe Bytes in
den Kindprozess gehen soll.
Ein typischer Fall ist gzip:
import Antra::Bytes as Bytes
import Antra::Process as Process
import Antra::Result as Result
my source = Bytes.from_hex("414243000a")
my gzip = Process.spawn("gzip", ["-c"]).!
Result.unwrap!(Process.write_bytes(gzip, source))
Result.unwrap!(Process.close_input(gzip))
my compressed = Process.read_all_bytes(gzip).!
println(compressed)
println(Process.wait(gzip).!)Das vollstaendige Repo-Beispiel dazu ist:
Was read_bytes(...) gegenueber read_all_bytes(...) bringt#
read_all_bytes(...) ist die einfache Variante:
- lies alles bis EOF
read_bytes(process, n) ist die wichtige Variante fuer kleine Binärprotokolle:
- lies genau den naechsten Abschnitt
- interpretiere danach selbst, was die Bytes bedeuten
Das brauchst du zum Beispiel fuer:
- Längenpräfixe
- Header
- Versionsbytes
- Checksummen
- kleine Binär-Frames
Kleines eigenes Protokoll: Laenge + Checksumme#
Ein minimales selbstgebautes Protokoll kann so aussehen:
- 1 Byte: Payload-Laenge
- 1 Byte: Checksumme
- danach die eigentlichen Nutzdaten
Die Gegenstelle kann dann zuerst 2 Bytes Header lesen und danach die Nutzdaten.
Das Repo-Beispiel dazu ist:
Die Idee ist nicht, dass dieses Protokoll "grossartig" ist. Es ist nur klein genug, um das Prinzip zu sehen:
- erst Header lesen
- dann Nutzdaten lesen
- Header in Antra auswerten
- Hex oder Laenge pruefen
Binaeres stderr#
Nicht jede Gegenstelle schreibt Fehler als normalen Text. Deshalb gibt es fuer
stderr jetzt auch Byte-Helfer:
Process.read_stderr_bytes(...)Process.read_stderr_all_bytes(...)
Das ist selten der erste Einstieg, aber wichtig, wenn ein Tool Diagnosedaten,
Binärheader oder Rohframes auf stderr schreibt.
Binaere Nachrichten mit System.fork(...)#
Bei System.fork(...) gibt es zwei interessante Modi:
System.send/receive(...)fuer normale Antra-Werte via ANDSystem.send_bytes/receive_bytes(...)fuer roheByteString-Nachrichten
Der Unterschied ist wichtig:
System.send(...)serialisiert Antra-WerteSystem.send_bytes(...)schickt einen bereits fertigen Byte-Block
System.send_bytes(...) benutzt intern einen laengenpraefixierten Frame. Das
heisst:
- eine Nachricht kann
0x00enthalten - eine Nachricht kann Newlines enthalten
- mehrere Byte-Nachrichten bleiben trotzdem sauber getrennt
Ein minimales Muster sieht so aus:
import Antra::Bytes as Bytes
import Antra::Process as Process
import Antra::Result as Result
import Antra::System as System
my child = System.fork(|stdin, stdout| -> {
my packet = System.receive_bytes(stdin).!
Result.unwrap!(System.send_bytes(stdout, packet))
}).!
Result.unwrap!(System.send_bytes(child, Bytes.from_hex("00ff4142")))
Result.unwrap!(Process.close_input(child))
my reply = System.receive_bytes(child).!
println(Bytes.to_hex(reply))
println(Process.wait(child).!)Die wichtigste Denkregel#
Wenn du Byte-I/O machst, denke nicht in "Zeilen" oder "Strings".
Denke lieber so:
- Welche Bytes schicke ich genau?
- Wie weiss die Gegenstelle, wo ein Paket endet?
- Ist das ein kompletter Block oder ein Header plus Nutzdaten?
Sobald diese drei Fragen klar sind, wird Binary-I/O erstaunlich mechanisch.
Was du zuerst ausprobieren solltest#
Wenn du das Thema ueben willst, nimm diese Reihenfolge:
- Starte ../examples/process_gzip_bytes.ant und sieh dir den Roundtrip an.
- Starte ../examples/process_checksum_protocol.ant und achte auf Header und Nutzdaten.
- Schreibe danach selbst ein Mini-Protokoll mit Versionsbyte oder Typbyte.
