Netzwerkprogrammierung in Rust: TCP-Server und HTTP-Clients
Fast jedes Programm, das du benutzt, redet mit einem anderen Computer: eine Chat-App, ein Spiel, ein Wetter-Widget. Unter der Haube öffnen sie eine Verbindung, schicken ein paar Bytes und lesen die Antwort. Darin ist Rust richtig gut. Es ist schnell, braucht wenig Speicher, und der Compiler findet viele der Fehler, die Netzwerkcode sonst um drei Uhr nachts abstürzen lassen.
Auf dieser Seite schreibst du einen kleinen TCP-Server und einen Client. Du bedienst mehrere Clients gleichzeitig (zuerst mit Threads, dann mit async tokio) und rufst danach echte HTTP-APIs mit reqwest auf. Die TCP-Beispiele laufen direkt hier im Rust Playground. Die reqwest-Beispiele brauchen Internet, die startest du also auf deinem eigenen Computer mit cargo run. Jedes Beispiel wurde gebaut und ausgeführt, und die gezeigte Ausgabe ist echt.
Was du bauen kannst
Abschnitt betitelt „Was du bauen kannst“- Netzwerkdienste und Proxys. Cloudflare hat Pingora (auf Englisch), sein Open-Source-Framework für Proxys, in Rust gebaut. Auch das Service Mesh Linkerd (auf Englisch) verwendet einen Proxy in Rust.
- Web-APIs und Backends, mit axum auf Basis von tokio. Sieh dir dazu Projekt: Web-API an.
- API-Clients und Tools: ein Kommandozeilen-Tool, das GitHub aufruft, ein Bot, ein Skript, das prüft, ob deine Websites erreichbar sind.
- Eigene Protokolle: ein Spieleserver, ein Chat-Server, ein Gerät, das Sensorwerte per UDP verschickt.
Wo es schwieriger ist: Async-Rust hat eine echte Lernkurve, und der erste Build mit Netzwerk-Crates dauert ein, zwei Minuten. Für ein 10-Zeilen-Skript, das eine einzige API aufruft, ist Python vielleicht schneller geschrieben. Rust lohnt sich, wenn das Programm schnell sein, monatelang laufen oder viele Verbindungen bewältigen muss.
Welches Crate?
Abschnitt betitelt „Welches Crate?“| Crate | Wofür | Stil | Nimm es, wenn |
|---|---|---|---|
std::net |
TCP- und UDP-Sockets, in Rust eingebaut | blockierend | Du lernst, kleine Tools schreibst, wenige Verbindungen hast. Keine Abhängigkeiten. |
| tokio | Async-Runtime mit asynchronem TCP, UDP und Timern | async | Viele Verbindungen gleichzeitig. Die meisten Netzwerk-Crates bauen darauf auf. |
| reqwest | Freundlicher HTTP-Client (JSON, Timeouts, TLS) | async (blockierend optional) | Du rufst Web-APIs auf. Die übliche erste Wahl. |
| ureq | Kleiner, einfacher HTTP-Client | blockierend | Du willst überhaupt kein async und kein tokio. |
| hyper | HTTP-Engine auf niedriger Ebene, unter reqwest und axum | async | Du baust ein Framework oder brauchst volle Kontrolle. |
| axum | Web-Framework für HTTP-Server | async | Du schreibst eine Web-API. Siehe das axum-Projekt. |
Alle sind ausgereift und weit verbreitet. Ein guter Weg: Lerne die Ideen mit std::net und steig dann auf tokio, reqwest und axum um.
Ein TCP-Server und -Client in einem Programm
Abschnitt betitelt „Ein TCP-Server und -Client in einem Programm“TCP liefert dir einen Stream (Datenstrom) von Bytes zwischen zwei Programmen, wie ein Telefongespräch. Der Server lauscht auf einer Adresse und einem Port. Der Client verbindet sich damit. Danach können beide Seiten lesen und schreiben.
Normalerweise sind Server und Client zwei getrennte Programme. Hier stecken wir beide in ein Programm, damit es im Playground läuft: Der Server läuft in einem Thread, und main ist der Client.
use std::io::{BufRead, BufReader, Write};use std::net::{TcpListener, TcpStream};use std::thread;
fn main() -> std::io::Result<()> { // Port 0 heißt „irgendein freier Port“. Danach fragen wir, welchen wir bekommen haben. let listener = TcpListener::bind("127.0.0.1:0")?; let addr = listener.local_addr()?;
// Der Server: einen Client annehmen, eine Zeile lesen, mit einer Zeile antworten. let server = thread::spawn(move || -> std::io::Result<()> { let (stream, _) = listener.accept()?; let mut name = String::new(); BufReader::new(&stream).read_line(&mut name)?; writeln!(&stream, "Hallo, {}!", name.trim())?; Ok(()) });
// Der Client: verbinden, eine Zeile senden, die Antwort lesen. let stream = TcpStream::connect(addr)?; writeln!(&stream, "Ferris")?; let mut reply = String::new(); BufReader::new(&stream).read_line(&mut reply)?; print!("Client hat erhalten: {reply}");
server.join().unwrap()?; println!("Server ist fertig"); Ok(())}Ausgabe:
Client hat erhalten: Hallo, Ferris!Server ist fertigEin paar Dinge, die dir auffallen sollten:
127.0.0.1heißt „nur dieser Computer“. Nimm0.0.0.0, wenn andere Rechner deinen Server erreichen sollen.- TCP weiß nicht, wo eine Nachricht endet. Wir verwenden eine einfache Regel: eine Nachricht = eine Zeile.
writeln!funktioniert auf einem Stream so, wieprintln!auf dem Bildschirm: Es hängt das\nan.read_lineliest bis dorthin. - Jeder Netzwerkaufruf kann fehlschlagen. Deshalb gibt jeder ein
Resultzurück, und wir verwenden?. &streamfunktioniert zum Lesen und zum Schreiben, weil&TcpStreamsowohlReadals auchWriteimplementiert.
Viele Clients mit Threads
Abschnitt betitelt „Viele Clients mit Threads“Ein echter Server darf Client Nummer 2 nicht warten lassen, bis Client Nummer 1 fertig ist. Die klassische Antwort: ein Thread pro Verbindung. Die Schleife nimmt einen Client an, übergibt ihn an einen neuen Thread und kehrt sofort zu accept zurück.
Dieser Server zählt die Wörter in jeder Zeile, die er bekommt. Drei Clients verbinden sich gleichzeitig:
use std::io::{BufRead, BufReader, Write};use std::net::{SocketAddr, TcpListener, TcpStream};use std::thread;
// Serverseite: auf jede Zeile mit ihrer Wortanzahl antworten.fn handle_client(stream: TcpStream) -> std::io::Result<()> { for line in BufReader::new(&stream).lines() { let words = line?.split_whitespace().count(); writeln!(&stream, "{words} Wörter")?; } Ok(()) // der Client hat die Verbindung geschlossen}
// Clientseite: eine Zeile senden, die Antwort zurückgeben.fn ask(addr: SocketAddr, msg: &str) -> std::io::Result<String> { let stream = TcpStream::connect(addr)?; writeln!(&stream, "{msg}")?; let mut reply = String::new(); BufReader::new(&stream).read_line(&mut reply)?; Ok(format!("{msg:?} -> {}", reply.trim()))}
fn main() -> std::io::Result<()> { let listener = TcpListener::bind("127.0.0.1:0")?; let addr = listener.local_addr()?;
thread::spawn(move || { for stream in listener.incoming() { let stream = stream.unwrap(); thread::spawn(move || handle_client(stream)); // ein Thread pro Client } });
// Drei Clients gleichzeitig, jeder in seinem eigenen Thread. let clients = ["hallo zusammen", "rust zu lernen macht spaß", "wir sehen uns"] .map(|msg| thread::spawn(move || ask(addr, msg))); // Der Reihe nach auf sie warten, damit die Ausgabe immer gleich aussieht. for c in clients { println!("{}", c.join().unwrap()?); } Ok(())}Ausgabe:
"hallo zusammen" -> 2 Wörter"rust zu lernen macht spaß" -> 5 Wörter"wir sehen uns" -> 3 WörterWenn ein Client seinen TcpStream freigibt (droppt), schließt sich die Verbindung, lines() endet, und der Thread dieses Clients ist fertig. Die Accept-Schleife endet nie, und das ist bei einem Server normal. Hier hört das Programm einfach auf, wenn main zurückkehrt. Dieses Muster ist simpel und funktioniert gut für Dutzende oder Hunderte von Clients. Bei Tausenden kosten all diese Threads aber viel Speicher. Genau da kommt async ins Spiel.
Derselbe Server mit tokio
Abschnitt betitelt „Derselbe Server mit tokio“Mit tokio ist jeder Client ein Task statt eines Threads. Ein Task ist winzig, deshalb kann ein einziger Server viele Tausend Verbindungen halten. Vergleich es mit der Thread-Version oben: Aus std::net wird tokio::net, aus thread::spawn wird tokio::spawn, und überall, wo das Programm auf das Netzwerk wartet, kommt ein .await dazu.
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};use tokio::net::{TcpListener, TcpStream};
// Derselbe Wortzähl-Server, aber jeder Client ist ein billiger Task statt eines Threads.async fn handle_client(stream: TcpStream) -> std::io::Result<()> { let (read_half, mut write_half) = stream.into_split(); let mut lines = BufReader::new(read_half).lines(); while let Some(line) = lines.next_line().await? { let words = line.split_whitespace().count(); write_half.write_all(format!("{words} Wörter\n").as_bytes()).await?; } Ok(())}
async fn ask(addr: std::net::SocketAddr, msg: &str) -> std::io::Result<String> { let mut stream = TcpStream::connect(addr).await?; stream.write_all(format!("{msg}\n").as_bytes()).await?; let mut reply = String::new(); BufReader::new(stream).read_line(&mut reply).await?; Ok(format!("{msg:?} -> {}", reply.trim()))}
#[tokio::main]async fn main() -> std::io::Result<()> { let listener = TcpListener::bind("127.0.0.1:0").await?; let addr = listener.local_addr()?;
tokio::spawn(async move { loop { let (stream, _) = listener.accept().await.unwrap(); tokio::spawn(handle_client(stream)); // ein Task pro Client } });
// Drei Clients auf einmal starten, dann der Reihe nach auf jeden warten. let clients = ["hallo zusammen", "rust zu lernen macht spaß", "wir sehen uns"] .map(|msg| tokio::spawn(ask(addr, msg))); for c in clients { println!("{}", c.await.unwrap()?); } Ok(())}Ausgabe:
"hallo zusammen" -> 2 Wörter"rust zu lernen macht spaß" -> 5 Wörter"wir sehen uns" -> 3 Wörterinto_split() gibt dir eine Lesehälfte und eine Schreibhälfte, damit Lesen und Schreiben in verschiedenen Variablen leben können. Wie vorher läuft die Accept-Schleife endlos. Wenn main zurückkehrt, beendet tokio alle Tasks.
UDP in einer Minute
Abschnitt betitelt „UDP in einer Minute“UDP hat keine Verbindung. Du schickst einzelne Nachrichten (Datagramme) an eine Adresse, und jede kommt ganz an oder gar nicht. Reihenfolge und Zustellung sind nicht garantiert. UDP wird für Spiele, Videoanrufe, DNS und Sensoren verwendet, wo Tempo wichtiger ist als ein verlorenes Paket. In std::net ist es ein einziger Typ, UdpSocket:
let socket = UdpSocket::bind("127.0.0.1:0")?;socket.send_to(b"temp=21.5", "127.0.0.1:9000")?; // eine Nachricht, an eine Adresse geschickt
let mut buf = [0u8; 1500];let (n, from) = socket.recv_from(&mut buf)?; // wartet auf eine Nachricht von irgendwemtokio::net::UdpSocket funktioniert genauso, nur mit .await.
Timeouts und Netzwerkfehler
Abschnitt betitelt „Timeouts und Netzwerkfehler“Im Netzwerk geht ständig etwas schief: Niemand lauscht, das WLAN bricht ab, ein Server hängt. Zwei Regeln halten dein Programm gesund: Behandle jeden Fehler, und warte nie ewig. Standardmäßig wartet ein Lesevorgang ewig. set_read_timeout behebt das:
use std::io::{ErrorKind, Read};use std::net::{TcpListener, TcpStream};use std::time::Duration;
fn main() -> std::io::Result<()> { // 1. Niemand lauscht: Das Verbinden schlägt sofort fehl. let free_port = TcpListener::bind("127.0.0.1:0")?.local_addr()?; // gedroppt = geschlossen match TcpStream::connect(free_port) { Ok(_) => println!("verbunden?!"), Err(e) => println!("Verbinden fehlgeschlagen: {:?}", e.kind()), }
// 2. Ein Server, der Verbindungen annimmt, aber nie antwortet. let silent = TcpListener::bind("127.0.0.1:0")?; let mut stream = TcpStream::connect(silent.local_addr()?)?; stream.set_read_timeout(Some(Duration::from_millis(200)))?;
let mut buf = [0u8; 64]; match stream.read(&mut buf) { Ok(n) => println!("{n} Bytes gelesen"), // Linux meldet WouldBlock, Windows meldet TimedOut. Err(e) if matches!(e.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) => { println!("Lesen nach 200 ms abgebrochen (Timeout)") } Err(e) => println!("anderer Fehler: {e}"), } Ok(())}Ausgabe:
Verbinden fehlgeschlagen: ConnectionRefusedLesen nach 200 ms abgebrochen (Timeout)e.kind() liefert dir eine ErrorKind (auf Englisch), auf die du mit match reagieren kannst. Für langsame Verbindungsaufbauten gibt es außerdem TcpStream::connect_timeout. In tokio packst du jedes Future in tokio::time::timeout:
let result = tokio::time::timeout(Duration::from_secs(2), stream.read(&mut buf)).await;// Err(_) = hat zu lange gedauert; Ok(inner) = das Lesen ist fertig (inner ist sein eigenes Result)HTTP und JSON mit reqwest
Abschnitt betitelt „HTTP und JSON mit reqwest“HTTP ist die Sprache des Webs, und es läuft auf TCP. Du könntest HTTP von Hand über einen TcpStream schreiben, aber ein Client-Crate kümmert sich um die Details: HTTPS, Weiterleitungen, Header, Kompression. Diese Beispiele brauchen Internetzugang und das Crate reqwest, deshalb laufen sie nicht im Playground. Starte sie auf deinem Computer:
cargo new api-clientcd api-clientcargo add reqwest --features jsoncargo add tokio --features fullcargo add serde --features derive[dependencies]reqwest = { version = "0.13", features = ["json"] }serde = { version = "1", features = ["derive"] }tokio = { version = "1", features = ["full"] }Wir verwenden JSONPlaceholder (auf Englisch), eine kostenlose Fake-API zum Üben. Der erste Build lädt und kompiliert ziemlich viele Crates, gib ihm also eine Minute.
JSON per GET und POST
Abschnitt betitelt „JSON per GET und POST“reqwest::get(url).await?.text().await? gibt dir den Body als String. Für JSON gibt es aber etwas Besseres: Mit dem Feature json verwendet reqwest serde, um die Antwort direkt in dein Struct zu verwandeln und dein Struct in einen Request-Body:
use serde::{Deserialize, Serialize};
// Nur die Felder, die du aufführst, werden gelesen. Zusätzliche JSON-Felder (wie "userId") werden ignoriert.#[derive(Deserialize)]struct Todo { id: u32, title: String, completed: bool,}
#[derive(Serialize)]struct NewTodo { title: String, completed: bool,}
#[tokio::main]async fn main() -> Result<(), reqwest::Error> { let client = reqwest::Client::new();
// GET: JSON -> dein Struct let todo: Todo = client .get("https://jsonplaceholder.typicode.com/todos/1") .send() .await? .json() .await?; println!("#{}: {} (done: {})", todo.id, todo.title, todo.completed);
// POST: dein Struct -> JSON (diese Test-API antwortet, speichert aber nicht wirklich) let new = NewTodo { title: "Learn reqwest".to_string(), completed: false }; let response = client .post("https://jsonplaceholder.typicode.com/todos") .json(&new) .send() .await?; println!("status: {}", response.status()); println!("{}", response.text().await?); Ok(())}#1: delectus aut autem (done: false)status: 201 Created{ "title": "Learn reqwest", "completed": false, "id": 201}Erstelle einen Client und verwende ihn immer wieder. Er hält Verbindungen offen und nutzt sie erneut, dadurch werden spätere Anfragen schneller. .json(&new) setzt außerdem den Header Content-Type: application/json für dich. (Der Titel delectus aut autem ist Platzhaltertext, den die Test-API liefert.)
Du willst kein async? ureq ist ein kleiner, blockierender Client: ureq::get(url).call()?.body_mut().read_to_string()? gibt dir den Body als String, ganz ohne tokio.
Timeouts und Fehler mit reqwest
Abschnitt betitelt „Timeouts und Fehler mit reqwest“reqwest hat standardmäßig keinen Timeout. Ein hängender Server kann dein Programm also ewig warten lassen. Setz deshalb einen Timeout am Client. Und beachte: Ein 404 Not Found ist für reqwest kein Fehler. Die Anfrage hat funktioniert, die Antwort war nur „nein“. Ruf error_for_status() auf, um 4xx- und 5xx-Antworten in Fehler zu verwandeln:
use std::time::Duration;
// Macht auch aus 4xx/5xx-Antworten Fehler, nicht nur aus Netzwerkproblemen.async fn fetch(client: &reqwest::Client, url: &str) -> Result<String, reqwest::Error> { let response = client.get(url).send().await?.error_for_status()?; response.text().await}
#[tokio::main]async fn main() -> Result<(), reqwest::Error> { // Ohne Timeout kann eine Anfrage an einen hängenden Server ewig warten. let client = reqwest::Client::builder() .timeout(Duration::from_secs(2)) .build()?;
// Ein lokaler Server, der Verbindungen annimmt, aber nie antwortet. let silent = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let stuck = format!("http://{}/", silent.local_addr().unwrap());
let urls = [ "https://jsonplaceholder.typicode.com/todos/1", "https://jsonplaceholder.typicode.com/todos/999999", "http://127.0.0.1:1/", &stuck, ]; for url in urls { match fetch(&client, url).await { Ok(body) => println!("ok, {} bytes", body.len()), Err(e) if e.is_status() => println!("server said {}", e.status().unwrap()), Err(e) if e.is_connect() => println!("could not connect: {e}"), Err(e) if e.is_timeout() => println!("gave up after 2 seconds"), Err(e) => println!("other error: {e}"), } } Ok(())}ok, 83 bytesserver said 404 Not Foundcould not connect: error sending request for url (http://127.0.0.1:1/)gave up after 2 secondsis_status(), is_connect() und is_timeout() sagen dir, welche Art von Fehler passiert ist. So kannst du entscheiden, was zu tun ist: später erneut versuchen, eine Meldung anzeigen oder aufgeben. In einem größeren Programm kannst du diesen Fehlern mit anyhow ganz leicht Kontext hinzufügen.
Häufige Fehler
Abschnitt betitelt „Häufige Fehler“- Beide Seiten warten aufeinander. Der Client ruft
read_lineauf, aber der Server liest auch, weil der Client das\nvergessen hat. Niemand sendet je etwas, und das Programm hängt. Legt fest, wo eine Nachricht endet (ein Zeilenumbruch ist die einfachste Regel). - Zu früh „bis zum Ende“ lesen.
read_to_stringwartet, bis die andere Seite die Verbindung schließt. Bei einer Verbindung, die offen bleibt, kehrt es nie zurück. Lies stattdessen Zeile für Zeile. - Blockieren in async-Code. Wenn du
std::netoderstd::thread::sleepin einem tokio-Task benutzt, blockierst du einen ganzen Worker-Thread. Nimm in async-Codetokio::netundtokio::time::sleep. - Timeouts vergessen. Deine Tests laufen im schnellen lokalen Netz durch. Dann friert ein einziger langsamer Server alles ein. Setz Timeouts vom ersten Tag an.
Übungen
Abschnitt betitelt „Übungen“1. Ein Taschenrechner-Server
Abschnitt betitelt „1. Ein Taschenrechner-Server“Schreib einen Server, der Zeilen wie add 2 3 oder mul 4 5 liest und mit dem Ergebnis antwortet. Auf eine ungültige Zeile antwortet er mit einer kurzen Meldung Fehler: .... Teste ihn aus main heraus mit einer Verbindung und vier Fragen. Tipp: Zerleg die Zeile mit split_whitespace(), wandle die Zahlen mit parse::<i64>() um und antworte mit writeln!.
Lösung anzeigen
use std::io::{BufRead, BufReader, Write};use std::net::{TcpListener, TcpStream};use std::thread;
fn calculate(line: &str) -> String { let parts: Vec<&str> = line.split_whitespace().collect(); let [op, a, b] = parts[..] else { return "Fehler: Format ist <op> <a> <b>".to_string(); }; let (Ok(a), Ok(b)) = (a.parse::<i64>(), b.parse::<i64>()) else { return "Fehler: keine Zahl".to_string(); }; match op { "add" => (a + b).to_string(), "mul" => (a * b).to_string(), _ => format!("Fehler: unbekannte Operation {op}"), }}
fn main() -> std::io::Result<()> { let listener = TcpListener::bind("127.0.0.1:0")?; let addr = listener.local_addr()?;
let server = thread::spawn(move || -> std::io::Result<()> { let (stream, _) = listener.accept()?; for line in BufReader::new(&stream).lines() { writeln!(&stream, "{}", calculate(&line?))?; } Ok(()) });
let stream = TcpStream::connect(addr)?; let mut replies = BufReader::new(&stream); for question in ["add 2 3", "mul 4 5", "add 2 x", "div 8 2"] { writeln!(&stream, "{question}")?; let mut answer = String::new(); replies.read_line(&mut answer)?; println!("{question} = {}", answer.trim()); } drop(stream); // die Verbindung zu schließen beendet die Schleife des Servers server.join().unwrap()}Ausgabe:
add 2 3 = 5mul 4 5 = 20add 2 x = Fehler: keine Zahldiv 8 2 = Fehler: unbekannte Operation divWeil die Logik in einer ganz normalen Funktion calculate steckt, kannst du sie ganz ohne Netzwerk testen. Die Zeilen mit let ... else kehren früh zurück, wenn die Eingabe nicht die richtige Form hat.
2. Erledigte To-dos zählen (auf deinem Rechner)
Abschnitt betitelt „2. Erledigte To-dos zählen (auf deinem Rechner)“Hol mit reqwest https://jsonplaceholder.typicode.com/todos?userId=1 ab. Die Antwort ist ein JSON-Array von To-dos. Gib aus, wie viele es sind, wie viele erledigt sind und den Titel des ersten, das noch offen ist. Tipp: Deserialisiere in ein Vec<Todo> und nutze dann Iterator-Methoden wie filter und find.
Lösung anzeigen
use serde::Deserialize;
#[derive(Deserialize)]struct Todo { title: String, completed: bool,}
#[tokio::main]async fn main() -> Result<(), reqwest::Error> { let todos: Vec<Todo> = reqwest::get("https://jsonplaceholder.typicode.com/todos?userId=1") .await? .error_for_status()? .json() .await?;
let done = todos.iter().filter(|t| t.completed).count(); println!("user 1 has {} to-dos, {done} completed", todos.len());
let first_open = todos.iter().find(|t| !t.completed).unwrap(); println!("first open one: {}", first_open.title); Ok(())}user 1 has 20 to-dos, 11 completedfirst open one: delectus aut autemEin JSON-Array wird zu einem Vec, und serde füllt jedes Element für dich aus. Dein Struct listet nur die Felder auf, die du brauchst.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“std::netbietet dir blockierendes TCP (TcpListener,TcpStream) und UDP (UdpSocket), ganz ohne zusätzliche Crates.- Binde an Port
0, um irgendeinen freien Port zu bekommen, und lies ihn mitlocal_addr()aus. - TCP ist ein Strom von Bytes: Legt fest, wo eine Nachricht endet, zum Beispiel eine Nachricht pro Zeile.
- Bediene viele Clients mit einem Thread pro Verbindung oder mit tokio-Tasks, wenn du Tausende brauchst.
- reqwest (async) oder ureq (blockierend) rufen HTTP-APIs auf, und serde verwandelt JSON in deine Structs.
- Setz immer Timeouts und nutze
error_for_status(), damit ein 404 nicht wie ein Erfolg aussieht.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Was passiert, wenn du einen TcpListener an 127.0.0.1:0 bindest?
2. Warum schicken die Beispiele auf dieser Seite am Ende jeder Nachricht ein \n?
3. reqwest bekommt die Antwort 404 Not Found. Was gibt send().await zurück?
4. Dein tokio-Server bedient jeden Client mit std::net und std::thread::sleep. Wo ist das Problem?
Punkte: 0 / 4
Weiterlesen
Abschnitt betitelt „Weiterlesen“- std::net – Rust-Standardbibliothek (auf Englisch):
TcpListener,TcpStream,UdpSocketund Adressen, mit Beispielen. - Einen Single-Threaded-Webserver erstellen – Das Rust-Buch: Das Abschlussprojekt des Buchs baut einen HTTP-Server auf
std::netund ergänzt dann einen Thread-Pool. - Tokio-Tutorial (auf Englisch): asynchrones TCP Schritt für Schritt, vom Starten von Tasks bis zum Aufteilen von Nachrichten (Framing).
- reqwest – docs.rs (auf Englisch): der Client, Anfragen, JSON, Timeouts und das Modul
blocking. - Web-Clients – Rust Cookbook (auf Englisch): kurze Rezepte für HTTP-Anfragen, APIs und Downloads.