Warum Rust der neue König der Netzwerkprogrammierung ist?
\n\nHaben Sie jemals einen Netzwerkserver in C oder C++ geschrieben und einen Segmentation Fault aufgrund eines vergessenen Mutex erwischt? Oder haben Sie mit Datenrennen in Go gekämpft und versucht zu verstehen, warum eine Goroutine „hängen geblieben“ ist? Rust löst diese Probleme ein für alle Mal. Sein Besitzsystem (ownership) und das Modell der „furchtlosen Nebenläufigkeit“ (fearless concurrency) machen die Netzwerkprogrammierung in Rust nicht nur sicher, sondern auch unglaublich leistungsfähig.
\n\nIn diesem Artikel werden wir uns ansehen, wie man TCP- und UDP-Server in Rust erstellt, sowohl mit der Standardbibliothek (std::net) als auch mit dem modernen Async-Stack auf Basis von Tokio. Sie werden sehen, dass Rust nicht nur eine Sprache für die Systemprogrammierung ist, sondern auch ein hervorragendes Werkzeug zum Schreiben hochbelastbarer Netzwerkdienste.
Grundlagen: std::net – TCP-Echo-Server in 5 Minuten
\n\nBeginnen wir mit dem Klassiker – einem Echo-Server. Er nimmt eine TCP-Verbindung entgegen, liest Daten und sendet sie an den Client zurück. In Rust geht das erstaunlich prägnant.
\n\nuse std::io::{Read, Write};\nuse std::net::{TcpListener, TcpStream};\nuse std::thread;\n\nfn handle_client(mut stream: TcpStream) {\n let mut buffer = [0; 1024];\n loop {\n match stream.read(&mut buffer) {\n Ok(0) => {\n println!("Client hat die Verbindung getrennt");\n break;\n }\n Ok(n) => {\n if let Err(e) = stream.write_all(&buffer[..n]) {\n eprintln!("Schreibfehler: {}", e);\n break;\n }\n }\n Err(e) => {\n eprintln!("Lesefehler: {}", e);\n break;\n }\n }\n }\n}\n\nfn main() -> std::io::Result<()> {\n let listener = TcpListener::bind("127.0.0.1:7878")?;\n println!("Server gestartet auf Port 7878");\n\n for stream in listener.incoming() {\n match stream {\n Ok(stream) => {\n thread::spawn(|| handle_client(stream));\n }\n Err(e) => {\n eprintln!("Verbindungsfehler: {}", e);\n }\n }\n }\n Ok(())\n}\n\n\nWas passiert hier? TcpListener::bind erstellt einen lauschenden Socket. Die Methode incoming() gibt einen Iterator über eingehende Verbindungen zurück. Für jeden neuen Client starten wir einen separaten Thread – thread::spawn. Innerhalb von handle_client lesen wir Daten in einen Puffer und senden sie über write_all zurück.
Wichtig: Rust garantiert, dass TcpStream nicht versehentlich von zwei Threads gleichzeitig verwendet wird – der Compiler lässt das einfach nicht zu. Das ist fearless concurrency.
Probleme dieses Ansatzes
\n- \n
- Skalierung: Jeder Client ist ein eigener Thread. Bei 10.000 Clients würde das Betriebssystem einfach überlastet. \n
- Ressourcen: Der Standard-Thread-Stack beträgt 2 MB. 10.000 Clients = 20 GB Speicher. \n
Hier kommt die asynchrone Programmierung zur Hilfe.
\n\nAsync-Revolution: Tokio und async/await
\n\nRust hat keine eingebaute asynchrone Laufzeitumgebung (wie Go oder Node.js). Stattdessen bietet es die Traits Future und die Schlüsselwörter async/await, die Wahl der Laufzeitumgebung liegt bei Ihnen. Die beliebteste ist Tokio. Es ist nicht nur eine Bibliothek, sondern ein ganzes Ökosystem: TCP/UDP, Datei-E/A, Timer, Kanäle und sogar ein eigener Scheduler.
Lassen Sie uns unseren Echo-Server mit Tokio neu schreiben.
\n\nTokio-Echo-Server
\nFügen Sie in Cargo.toml hinzu:
[dependencies]\ntokio = { version = "1", features = ["full"] }\n\nJetzt der Code:
\n\nuse tokio::io::{AsyncReadExt, AsyncWriteExt};\nuse tokio::net::TcpListener;\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn std::error::Error>> {\n