Skip to content
Lektion 81 von 1150/115 abgeschlossen
Modul 18 — Input & Output

Readers & Writers: Rasts einheitliches I/O-System

Das Problem: Viele Quellen, viele Ziele

Wo kommen Daten her? Aus Dateien, dem Netzwerk, der Tastatur oder einfach aus einem Byte-Array im Speicher. Und wo gehen sie hin? In Dateien, ins Netz, auf die Konsole oder in einen Vec<u8>.

Rust löst das mit zwei Traits, die jede dieser Quellen und Ziele einheitlich machen:

  • Reader liefern Bytes. Sie implementieren std::io::Read.
  • Writer nehmen Bytes entgegen. Sie implementieren std::io::Write.
use std::io::Read;

fn main() {
    // Ein Reader aus einem Byte-Array im Speicher
    let mut reader = std::io::Cursor::new(b"hello, world".to_vec());

    let mut buf = String::new();
    reader.read_to_string(&mut buf).unwrap();
    println!("{buf}"); // hello, world
}

Bekannte Reader

  • File::open(filename) — bestehende Datei lesen
  • TcpStream — Daten über das Netzwerk empfangen
  • stdin() — Standard-Eingabe des Prozesses
  • Cursor<&[u8]>, Cursor<Vec<u8>> — aus einem Byte-Array "lesen"

Bekannte Writer

  • File::create(filename) — neue Datei schreiben
  • TcpStream — Daten über das Netzwerk senden
  • stdout(), stderr() — auf den Bildschirm schreiben
  • Vec<u8> — der write-Methode hängt ans Ende an
  • Cursor<Vec<u8>> — lesen und schreiben mit freiem Positionieren

Ein Trait, viele Quellen

Der Zauber: Eine Funktion, die eine generische Read- oder Write-Implementierung annimmt, funktioniert identisch für Dateien, Netzwerk-Sockets und Buffer.

use std::io::{self, Read};

// Arbeitet mit jeder Quelle, die bytes liefern kann:
fn drucke_inhalt(mut quelle: impl Read) -> io::Result<()> {
    let mut text = String::new();
    quelle.read_to_string(&mut text)?;
    println!("{text}");
    Ok(())
}

Läuft für File, TcpStream, Cursor, Stdin — ohne eine Zeile zu ändern.

📝 Schnellprüfung

Welcher Typ IST ein Reader?

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 82 von 1150/115 abgeschlossen
Modul 18 — Input & Output

Buffered Readers: Zeilenweise lesen mit BufRead

Warum Buffering?

System-Calls sind langsam. Hunderte einzelne read-Aufrufe über die Betriebssystem-Grenze sind teuer. Die Lösung: ein Puffer im Speicher.

Ein BufReader holt Daten in großen Blöcken (mehrere KB) vom Betriebssystem und verdient daraus viele kleine Lese-Operationen — fast ohne System-Calls.

use std::io::BufRead;

fn main() {
    // Eine Datei zeilenweise lesen
    let file = std::fs::File::open("test.txt").unwrap();
    let buffered = std::io::BufReader::new(file);

    for line in buffered.lines() {
        // `lines()` liefert die Zeilen OHNE das '\n' am Ende
        let line = line.unwrap();
        println!("Zeile: {line}");
    }
}

Die BufRead-Methoden

  • reader.read_line(&mut line) — liest eine Zeile in eine String
  • reader.lines() — Iterator über die Zeilen (Newline wird entfernt)
  • reader.read_until(byte, &mut vec) — liest bis zu einem Trenner-Byte
  • reader.split(byte) — Iterator, teilt nach einem Byte

lines() ist fast immer richtig

Für Text-Eingabe ist lines() meist genau das, was du willst: Es entfernt die \n (bzw. \r\n unter Windows) automatisch. So liest ein grep-Klon in Rust:

use std::io::{self, BufRead};

fn grep(target: &str) -> io::Result<()> {
    for line_result in io::stdin().lock().lines() {
        let line = line_result?;
        if line.contains(target) {
            println!("{line}");
        }
    }
    Ok(())
}

stdin().lock() gibt einen BufRead auf der Standard-Eingabe.

📝 Schnellprüfung

Was entfernt reader.lines() aus den gelieferten Strings?

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 83 von 1150/115 abgeschlossen
Modul 18 — Input & Output

Writers & Dateien schreiben

Schreiben mit write! und writeln!

Bisher hast du println!() benutzt. Für beliebige Writer gibt es write!() und writeln!(). Zwei Unterschiede zu println!:

  1. Sie nehmen als erstes Argument den Writer.
  2. Sie geben ein Result zurück — Fehler müssen behandelt werden.
use std::fs::File;
use std::io::{self, Write};

fn main() -> io::Result<()> {
    let mut file = File::create("output.txt")?;
    writeln!(file, "Zeile 1")?;
    writeln!(file, "Zeile 2")?;

    // An den Standard-Fehlerstrom schreiben:
    writeln!(io::stderr(), "Fehler: Welt nicht begrüßbar")?;
    Ok(())
}

File::open und File::create

  • File::open(filename) — öffnet eine bestehende Datei zum Lesen.
  • File::create(filename) — erstellt eine Datei zum Schreiben (leert sie, wenn sie existiert).

Feinsteuerung mit OpenOptions

Wenn keiner der beiden Fälle passt, gibt es OpenOptions:

use std::fs::OpenOptions;

fn log_eintrag(msg: &str) -> std::io::Result<()> {
    // Anhängemodus: bestehende Datei NICHT leeren
    let mut log = OpenOptions::new()
        .append(true)
        .open("server.log")?;
    writeln!(log, "{msg}")
}

Wem gehört der File?

File::create gibt eine eigene Datei zurück. Das writeln!(file, ...) borgt sie nur. Schließen musst du sie nicht explizit: Wenn die File aus dem Scope fällt, wird sie automatisch geschlossen und alle restlichen Daten geschrieben (flush). Bei langlaufenden Programmen lohnt ein explizites file.flush(), um Daten nicht im Puffer vergessen zu lassen.

📝 Schnellprüfung

Was bewirkt File::create, wenn eine Datei mit dem Namen bereits existiert?

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 84 von 1150/115 abgeschlossen
Modul 18 — Input & Output

Pfade: Path, PathBuf & OsStr

Das Problem mit Dateinamen

Dein Betriebssystem zwingt Dateinamen nicht zu gültigem Unicode. Unter Linux ist fast jede Bytefolge ein gültiger Name — sogar \xf4.txt. Rust-Strings sind aber immer gültiges Unicode.

Deshalb gibt es std::ffi::OsStr und OsString: String-Typen, die alles repräsentieren können, was das Betriebssystem an Namen akzeptiert. Und std::path::Path (bzw. PathBuf für den besitzenden Typ) ist eine bequeme Sicht auf OsStr mit vielen Pfad-Methoden.

Die Familien

geliehenbesitzendZweck
strStringUnicode-Text
OsStrOsStringalles, was das OS zulässt
PathPathBufDateinamen & Pfade, mit Methoden

Pfade zusammensetzen mit join()

use std::path::Path;

fn main() {
    let basis = Path::new("/home/user");
    let voll = basis.join("projekt").join("Cargo.toml");
    println!("{}", voll.display());
    // /home/user/projekt/Cargo.toml

    println!("Dateiname: {:?}", voll.file_name());
    println!("Endung:    {:?}", voll.extension());
    println!("Eltern:    {:?}", voll.parent());
    println!("Existiert B.d.P. die Datei? {}", voll.exists());
}

.display() macht den Pfad für println! nutzbar — denn Path selbst implementiert kein Display, nur Debug.

Wann Path, wann OsStr?

Nutze Path/PathBuf für ganze (relative oder absolute) Pfade. Für eine einzelne Komponente eines Pfads (etwa einen Ordnernamen) nimm OsStr. Der sanitize-Teil des praktischen Rust-Idioms: setze Pfade nie per String- Konkatenation zusammen, sondern mit join() — das beherrscht Trennzeichen auf allen Plattformen korrekt.

📝 Schnellprüfung

Warum existiert OsStr zusätzlich zu String?

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 85 von 1150/115 abgeschlossen
Modul 18 — Input & Output

Verzeichnisse lesen & das Dateisystem

Verzeichnisse mit read_dir durchlaufen

std::fs::read_dir liefert einen Iterator über die Einträge eines Ordners. Jeder Eintrag ist ein DirEntry mit Methoden wie path(), metadata() und file_type().

use std::fs;

fn main() -> std::io::Result<()> {
    for eintrag in fs::read_dir(".")? {
        let eintrag = eintrag?;
        let typ = eintrag.file_type()?;
        let art = if typ.is_dir() { "Verzeichnis" } else { "Datei" };
        println!("{art:12}  {}", eintrag.path().display());
    }
    Ok(())
}

Wichtige fs-Funktionen

  • fs::read_to_string(pfad) — ganze Datei in eine String
  • fs::read(pfad) — ganze Datei in einen Vec<u8>
  • fs::write(pfad, inhalt) — ganze Datei schreiben
  • fs::create_dir(pfad) / fs::create_dir_all(pfad) — Ordner anlegen
  • fs::remove_file(pfad) / fs::remove_dir(pfad) — löschen
  • fs::copy(quelle, ziel) — kopieren
  • fs::rename(alt, neu) — verschieben/umbenennen
  • fs::metadata(pfad) — Größe, Zeitstempel, Dateityp

read_to_string ist praktisch für kleine Dateien

Willst du eine kleine Konfigurationsdatei komplett lesen, ist read_to_string ideal — kein manueller Reader, kein Buffering nötig:

fn lade_punkte() -> std::io::Result<i32> {
    let inhalt = std::fs::read_to_string("punkte.txt")?;
    Ok(inhalt.trim().parse().unwrap_or(0))
}

Für große Dateien (Filme, Logs, Downloads) nimm dagegen einen Reader oder Stream — alles auf einmal in den Speicher schlucken bitte nicht.

📝 Schnellprüfung

Was liefert fs::read_to_string(pfad)?

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 86 von 1150/115 abgeschlossen
Modul 18 — Input & Output

Netzwerk: TcpListener & TcpStream

std::net: TCP und UDP

Für Low-Level-Netzwerk bietet std::net blockierende TCP- und UDP-I/O. Zwei zentrale Typen:

  • TcpListener — lauscht auf eingehende Verbindungen (Server)
  • TcpStream — eine offene Verbindung (Client UND Server-Seite)

Ein Echo-Server

Ein Echo-Server schickt einfach alles zurück, was er empfängt. Mit einem Thread pro Verbindung brauchst du nur wenige Zeilen:

use std::net::{TcpListener, TcpStream};
use std::{io, thread};

fn handle_conn(mut stream: TcpStream) -> io::Result<()> {
    // Echo: copy everything from stream back to itself.
    let mut write_stream = stream.try_clone()?;
    io::copy(&mut stream, &mut write_stream)?;
    Ok(())
}

fn echo_main(addr: &str) -> io::Result<()> {
    let listener = TcpListener::bind(addr)?;
    println!("listening on {addr}");

    for stream_result in listener.incoming() {
        let stream = stream_result?;
        let peer = stream.peer_addr()?;
        println!("connection received from {peer}");

        thread::spawn(move || {
            if let Err(e) = handle_conn(stream) {
                eprintln!("error in client thread: {e}");
            }
        });
    }
    Ok(())
}

fn main() {
    echo_main("127.0.0.1:17007").expect("error: ");
}
  • listener.incoming() — wartet auf Verbindungen, liefert Result<TcpStream>
  • stream.try_clone() — zweites Handle fürs Schreiben (Rust braucht &mut für Kopien)
  • io::copy(reader, writer) — kopiert alles von Reader zu Writer

Netzwerk: I/O ist überall gleich

Ein TcpStream ist gleichzeitig ein Read und ein Write. Deshalb funktioniert die Reader/Writer-Logik aus den früheren Lektionen auch im Netzwerk — und io::copy braucht nur einen impl Read und einen impl Write, egal ob Datei oder Socket.

Für hochperformante Server mit Tausenden Verbindungen brauchst du aber asynchrone I/O (siehe Modul 17) statt eines Threads pro Verbindung.

📝 Schnellprüfung

Was macht der TcpListener in einem Server?

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 87 von 1150/115 abgeschlossen
Modul 18 — Input & Output

Übung: Ein Log-Datei-Analyse-Tool

Projekt: Fehler aus Logs herausfiltern

Du kombinierst jetzt alles aus diesem Modul zu einem echten kleinen Tool:

  1. Lies eine Log-Datei zeilenweise (BufReader + lines()).
  2. Filtere Zeilen heraus, die einen Fehler-Schlüssel wie ERROR oder PANIC enthalten.
  3. Schreibe die gefundenen Zeilen in eine Ausgabedatei (File::create + writeln!).
  4. Gib zusätzlich eine Zusammenfassung auf der Konsole aus (stdout).

Diese Struktur prüft: Reader/Writer-Traits, Buffering, Datei-Pfade und Fehlerbehandlung mit ? — genau das angewandte Repertoire aus den Lektionen 1–6.

Rezepte aus diesem Modul

  • Datei zeilenweise: BufReader::new(File::open(&pfad)?).lines()
  • In Datei schreiben: writeln!(File::create(&ziel)?, "{line}")?
  • Auf Konsole: println!("{line}")
  • Fehler weiterreichen: mit ? nach oben geben
  • Pfad nutzbar machen: pfad.display() für println!

Übung: Log-Analyse-Tool

🧩 Übung m18-l7-e1

🔮 Vorhersage: Was passiert beim Ausführen?

Editor wird geladen…
KI-Tutor (sokratisch — keine Komplettlösungen)

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Modul-Checkpoint

Bestehe das Modul mit 80 %. Der Versuch zählt — beantwortete Fragen werden nicht erneut angezeigt; erst nach dem Absenden gibt es Auflösung und Erklärung.

Checkpoint: Modul 18 — Input & Output

Bestehensgrenze: 80 %. Der Versuch wird bewertet und zählt für das Modul-Gate.

This site uses essential cookies for Stripe payments. No tracking cookies.