Einen minimalistischen Rust-HTTP-Server ohne Frameworks erstellen
Ethan Miller
Product Engineer · Leapcell

Einleitung
Im lebendigen Rust-Ökosystem bieten zahlreiche Web-Frameworks wie Axum, Actix-web und Warp einen optimierten Weg zum Erstellen robuster HTTP-Dienste. Diese Frameworks bieten praktische Abstraktionen, bewährte Middleware und umfangreiche Funktionen, die die Entwicklung erheblich beschleunigen. Für diejenigen, die die grundlegenden Mechanismen eines HTTP-Servers verstehen möchten oder für Projekte, die extreme Kontrolle und minimale Abhängigkeiten erfordern, ist es von unschätzbarem Wert, diese Frameworks zu umgehen und direkt auf Kernbibliotheken aufzubauen. Diese Erkundung befasst sich mit dem Prozess des Erstellens eines minimalistischen Rust-HTTP-Servers, der hyper für das HTTP-Protokoll und tokio für die asynchrone Laufzeit und E/A nutzt. Ein solches Unterfangen vertieft nicht nur das Verständnis der asynchronen Programmierung in Rust, sondern zeigt auch die Leistungsfähigkeit und Flexibilität seiner Kernbibliotheken und führt zu effizienten und hochgradig anpassbaren Lösungen.
Kernkomponenten eines rohen HTTP-Servers
Bevor wir uns mit dem Code befassen, definieren wir kurz die zentralen Komponenten, die wir verwenden werden:
tokio: Dies ist eine leistungsstarke asynchrone Laufzeit für Rust. Sie bietet die notwendigen Werkzeuge für nicht-blockierende E/A, Task-Scheduling und das Management von Futures. In unserem Server übernimmttokiodas Lauschen auf eingehende Verbindungen und die effiziente Verwaltung gleichzeitiger Anfragen.hyper: Eine schnelle und korrekte HTTP-Implementierung in Rust.hyperarbeitet auf einer niedrigeren Ebene als Web-Frameworks und befasst sich direkt mit HTTP/1.1- und HTTP/2-Anfragen und -Antworten. Es behandelt das Parsen eingehender HTTP-Anfragen und das Formatieren ausgehender HTTP-Antworten, abstrahiert die Komplexität des HTTP-Protokolls selbst, aber nicht die Anforderungsbehandlungslogik der Anwendung.async/await: Rusts integrierte Syntax zum Schreiben asynchronen Codes. Sie ermöglicht es uns, nicht-blockierenden Code zu schreiben, der sich wie synchroner Code anfühlt und aussieht, wodurch die asynchrone Programmierung ergonomischer wird.
Das Verständnis, wie diese Teile zusammenpassen, ist entscheidend. tokio bietet die asynchrone Ausführungsumgebung und die Mittel zur Annahme von TCP-Verbindungen. Sobald eine Verbindung hergestellt ist, übernimmt hyper das Parsen des eingehenden Byte-Streams in eine HTTP-Anfrage und serialisiert dann eine HTTP-Antwort zurück in einen Byte-Stream, der über das Netzwerk gesendet werden soll. Unsere Anwendungslogik wird zwischen diesen beiden Schritten angesiedelt sein, die geparste Anfrage verarbeitet und eine entsprechende Antwort generiert.
Erstellen des minimalistischen Servers
Beginnen wir mit der Einrichtung unserer Cargo.toml mit den erforderlichen Abhängigkeiten:
[package] name = "minimal-http-server" version = "0.1.0" edition = "2021" [dependencies] tokio = { version = "1", features = ["full"] } # "full" beinhaltet Laufzeit, Netz, Makros usw. hyper = { version = "0.14", features = ["full"] }
Als Nächstes schreiben wir den Rust-Code für unseren Server. Unser Ziel ist es, einen Server zu erstellen, der auf 127.0.0.1:3000 lauscht und auf alle Anfragen mit einer einfachen "Hallo, Welt!"-Nachricht antwortet.
use std::{convert::Infallible, net::SocketAddr}; use hyper::{Body, Request, Response, Server}; use hyper::service::{make_service_fn, service_fn}; // Die Kernfunktion zur Behandlung von Anfragen async fn handle_request(_req: Request<Body>) -> Result<Response<Body>, Infallible> { // Wir ignorieren die eingehende Anfrage für dieses einfache Beispiel Ok(Response::new("Hello, world!".into())) } #[tokio::main] async fn main() { // Definieren Sie die Adresse, auf der gelauscht werden soll let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); // Ein `Service` ist ein Trait, der eine asynchrone Funktion von einer Anfrage zu einer Antwort darstellt. // `make_service_fn` wird verwendet, um für jede eingehende Verbindung einen neuen `Service` zu erstellen. let make_svc = make_service_fn(|_conn| async { // `service_fn` ist eine Hilfe, um eine `Fn` in einen `Service` umzuwandeln Ok::<_, Infallible>(service_fn(handle_request)) }); // Erstellen Sie den Server mit der angegebenen Adresse und dem Service-Factory let server = Server::bind(&addr).serve(make_svc); println!("Lausche auf http://{{}}", addr); // Führen Sie den Server aus, bis ein Fehler auftritt if let Err(e) = server.await { eprintln!("Serverfehler: {}", e); } }
Lassen Sie uns diesen Code aufschlüsseln:
useAnweisungen: Wir importieren notwendige Typen ausstd,hyperundhyper::service.Infalliblewird fürResult-Typen verwendet, bei denen dieErr-Variante niemals auftreten kann, was die Fehlerbehandlung für unser einfaches Beispiel vereinfacht.handle_requestFunktion: Dieseasync-Funktion ist die Kernlogik unseres Servers. Sie nimmt eineRequest<Body>als Eingabe und gibt einResult<Response<Body>, Infallible>zurück. Für dieses Beispiel ignoriert sie die eingehende Anfrage und gibt immer eineHTTP 200 OK-Antwort mit "Hello, world!" im Body zurück.#[tokio::main]Attribut: Dieses Makro vontokiovereinfacht die Einrichtung der Tokio-Laufzeit. Es wandelt unseremain-Funktion in einen asynchronen Einstiegspunkt um und kümmert sich um die Initialisierung der Laufzeit.SocketAddr::from(([127, 0, 0, 1], 3000)): Dies definiert die IP-Adresse und den Port, auf denen unser Server lauschen wird.make_service_fn: Diese Funktion ist entscheidend fürhyper-Server. Sie nimmt eine Closure entgegen, die für jede neue TCP-Verbindung eineService-Instanz zurückgibt. Dieses Muster ermöglicht verbindungs-spezifischen Zustand, falls erforderlich, obwohl dies in unserem einfachen Fall nicht so ist. Ihre Closure muss ebenfallsasyncsein.service_fn(handle_request): Innerhalb vonmake_service_fnwirdservice_fnverwendet, um unsereasync-Funktionhandle_requestin dieService-Trait-Implementierung vonhyperanzupassen, was der Server erwartet.Server::bind(&addr).serve(make_svc): Diese Zeile erstellt und konfiguriert denhyper-Server.bindgibt die Adresse an, auf der gelauscht werden soll, undservenimmt unseremake_svc-Factory entgegen, um eingehende Verbindungen zu bearbeiten.server.await: Dies startet den Server. Dasawait-Schlüsselwort pausiert die Ausführung vonmain, bis der Server ordnungsgemäß heruntergefahren wird oder ein Fehler auftritt.
Ausführen und Testen
Um diesen Server auszuführen, navigieren Sie im Terminal zu Ihrem Projektverzeichnis und führen Sie aus:
cargo run
Sie sollten "Lausche auf http://127.0.0.1:3000" sehen. Öffnen Sie nun Ihren Webbrowser oder verwenden Sie ein Tool wie curl, um auf den Server zuzugreifen:
curl http://127.0.0.1:3000
Sie sollten die Antwort erhalten: Hello, world!
Dieses einfache Beispiel demonstriert die grundlegenden Bausteine. Sie können handle_request erweitern, um:
- Request-Pfade zu parsen: 
req.uri().path() - Request-Header zu lesen: 
req.headers() - Request-Body zu lesen: 
hyper::body::to_bytes(req.into_body()).await - Dynamische Antworten zu generieren: 
Response<Body>mit unterschiedlichen Statuscodes, Headern und dem Body-Inhalt zu erstellen. 
Fazit
Durch die direkte Nutzung von hyper und tokio haben wir erfolgreich einen minimalistischen HTTP-Server in Rust erstellt, ohne auf höherwertige Frameworks zurückzugreifen. Dieser praxisorientierte Ansatz bietet tiefgreifende Einblicke in die asynchrone Programmierung, das HTTP-Protokoll und die Architekturentscheidungen, die beim Erstellen leistungsfähiger Netzwerkdienste getroffen werden. Er unterstreicht Rusts Fähigkeit, hochleistungsfähige Low-Level-Netzwerkanwendungen bereitzustellen und gleichzeitig Sicherheit und Nebenläufigkeit zu gewährleisten, und befähigt Entwickler, maßgeschneiderte und effiziente Serverlösungen von Grund auf neu zu erstellen.