Im Laufe der letzten zehn Jahre wurde eine ganze Armada von Web-Frameworks für Rust veröffentlicht - mit jeweils leicht unterschiedlichen Benutzeranforderungen und Funktionen. In diesem Artikel werfen wir einen Blick auf die fünf populärsten Vertreter dieser Gattung. Sie alle bieten grundlegende Elemente für Web-basierte Dienstleistungen in Form von:
Routing,
Request Handling,
verschiedenen Antwort-Typen und
Middleware.
Allerdings bieten die hier besprochenen Web-Frameworks für Rust kein Templating. Das wird im Regelfall durch separate Crates abgedeckt.
Actix Web
Das mit Abstand beliebteste Web-Framework für Rust ist Actix Web. Das Rahmenwerk erfüllt dabei so gut wie alle wichtigen Anforderungen:
Es ist hochperformant,
unterstützt eine breite Palette von Serverfunktionen und
macht es den Benutzern einfach, simple Webseiten zu erstellen.
Sämtliche Funktionen von Actix Web sind auf dem Stable Branch von Rust verfügbar. Im Folgenden betrachten wir eine simple "hello world"-App in Actix Web:
use actix_web::{get, App, HttpResponse, HttpServer, Responder};
#[get("/")]
async fn hello() -> impl Responder {
HttpResponse::Ok().body("Hello world!")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(hello))
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Das get()
-Attribut der hello()
-Funktion zeigt an, welche Route damit bedient werden soll. Allerdings wird sie erst aktiv, sobald sie mit Hilfe der .service()
-Methode dem App
-Objekt hinzugefügt wurde. Davon abgesehen, unterstützt Actix Web auch erweiterte Route-Konstruktionen: Es ist beispielsweise möglich, Positionsvariablen in der URL zu erfassen, um Requests an Funktionen zu übergeben, die nicht auf get()
setzen.
Ein wesentlicher Pluspunkt für Actix Web ist seine Performance: Sämtliche Requests und Antworten werden als unterschiedliche Typen behandelt. Der Server nutzt einen Thread Pool um Requests zu händeln. Dabei werden zwischen den Threads keine Daten geteilt werden, um die Leistung zu maximieren. Den State können Sie bei Bedarf mit Hilfe von Arc<>
manuell sharen, allerdings rät das Team hinter Actix Web von allen Aktionen ab, die Worker Threads blockieren und sich damit negativ auf die Performanz auswirken. Für langfristige, nicht CPU-gebundene Tasks empfiehlt es sich futures oder async einzusetzen.
Darüber hinaus bietet Actix Web auch Typ-basierte Handler für Error Codes und nutzt ein Middleware-System, um Logging zu implementieren. Das Framework nutzt ein allgemein ausgerichtetes User Session Management System mit Cookies als standardmäßigen Storage-Type - Sie können auch weitere ergänzen. Statische Dateien und Verzeichnisse können zudem mit eigenen, dedizierten Handlern bedient werden.
In Sachen Web Service bietet das Rust Web Framework einige gängige - und auch einige weniger gängige - Funktionen. Beispielsweise kann das Tool:
URL-kodierte Formulare verarbeiten,
automatisch auf HTTPS/2 umstellen,
brotli
-,gzip
-,deflate
- undzstd
-Dateien dekomprimieren sowieChunked Encoding.
Was WebSockets angeht, kommt die einzige wesentliche Abhängigkeit von Actix Web ins Spiel: die actix-web-actors
-Crate. Für Multipart-Streams ist zudem die actix-multipart
-Crate nötig. Für die Konvertierung von und nach JSON verwendet Actix Web serde
und serde_json
.
Im Jahr 2020 sorgte der Ausstieg eines der Hauptverantwortlichen des Projekts für Furore, der sich angeblich in der Kritik an der Nutzung von unsafe
-Code begründete. Das verbliebene Team setzte die Arbeit an dem Framework dennoch fort, das Gros des unsafe
-Codes ist mittlerweile entfernt.
Rocket
Das Alleinstellungsmerkmal von Rocket ist, dass es bei geringem Code-Aufwand umfassende Ergebnisse liefert. Entsprechend erfordert es relativ wenige Zeilen, um in Rocket eine einfache Web-App zu schreiben. Möglich wird das, indem Rocket das Type-System von Rust nutzt, um diverse Behaviors zu beschreiben - so dass diese zur Kompilierzeit durchgesetzt und kodiert werden.
Im Folgenden eine simple "hello world"-App in Rocket:
#[macro_use] extern crate rocket;
#[get("/")]
fn hello_world() -> &'static str {
"Hello, world!"
}
#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![hello_world])
}
Die Prägnanz von Rocket ist im Wesentlichen dem Umgang des Frameworks mit Attributen geschuldet: Routes werden mit den Attributen dekoriert, die sie für Methods und URL Patterns benötigen. Wir Sie im obigen Beispiel sehen können, gibt das #[launch]
-Attribut an, welche Funktion zum Einsatz kommt, um die Routes zu mounten und die Applikation Request-ready zu machen.
Auch wenn die Routes im obigen Beispiel synchron sind: Sie können - und sollten wenn möglich auch - asynchron sein. Standardmäßig nutzt Rocket die tokio
-Runtime um synchrone Prozesse in asynchrone umzuwandeln. Davon abgesehen unterstützt Rocket auch diverse gängige Request-Handling-Funktionen, etwa, Variablen aus URL-Elementen zu extrahieren. Ein Alleinstellungsmerkmale sind dabei sogenannte "Request Guards". Dabei kommen Rust-Typen und FromRequest
zum Einsatz, um Validierungsrichtlinien für Routes zu beschreiben. Sie könnten zum Beispiel einen benutzerdefinierten Typ erstellen, um zu verhindern, dass eine Route ausgelöst wird, wenn bestimmte Informationen im Request-Header nicht vorhanden sind und so auch nicht validiert werden können - wie zum Beispiel ein Cookie mit einer bestimmten damit verknüpften Berechtigung.
Ein weiteres nützliches Feature ist Fairing
, die Rocket-Version von Middleware: Typen, die dieses Attribut implementieren, können genutzt werden, um Callbacks zu Events hinzuzufügen - beispielsweise Requests oder Antworten. Allerdings ist es nicht möglich, über Fairings Anfragen zu verändern oder zu stoppen - obwohl sie auf Kopien der Request-Daten zugreifen können. Deshalb eignen sich Fairings am besten für Dinge, die Behavior Loggings, Performance-Metriken oder Security-Richtlinien beinhalten. Für Authentifizierungszwecke empfiehlt sich die Verwendung eines Request Guard.
Warp
Von anderen Rust-Web-Frameworks hebt sich Warp in erster Linie durch seine "Composable Components" ab - oder "Filters", wie diese im Warp-Sprech heißen. Diese können miteinander verkettet werden, um Services zu erstellen.
Das folgende, simple "hello world"-Beispiel ist nicht geeignet, um diese Funktion zu demonstrieren - wohl aber, um die Prägnanz des Rust Frameworks zu veranschaulichen:
use warp::Filter;
#[tokio::main]
async fn main() {
let hello = warp::path!().map(|| "Hello world");
warp::serve(hello).run(([127, 0, 0, 1], 8080)).await;
}
Filter implementieren die Filter
-Eigenschaft, wobei jeder davon in der Lage ist, den Output an einen anderen zu übergeben, um dessen Verhalten zu modifizieren. Im obigen Beispiel stellt warp::path
einen Filter dar, der mit weiteren Operationen (beispielsweise .map()
) verkettet werden kann, um eine Funktion anzuwenden.
Ein weiteres Beispiel aus der Warp-Dokumentation demonstriert das Filtersystem en détail:
use warp::Filter;
let hi = warp::path("hello")
.and(warp::path::param())
.and(warp::header("user-agent"))
.map(|param: String, agent: String| {
format!("Hello {}, whose agent is {}", param, agent)
});
Hier werden mehrere Filter miteinander verknüpft, um ein Behavior zu erzeugen - und zwar in dieser Reihenfolge:
Richten Sie mit dem
hello
-Pfad einen Endpunkt ein.Fügen Sie am Ende des Pfades einen Parameter hinzu, so dass dieser die Form
/hello/<something>
aufweist. Die.and()
-Methode ist eine von mehreren Kompositionsmöglichkeiten in Warp.Fügen Sie einen Parser für den
user-agent
-Header hinzu, so dass jeder eingehende Request der diesen vermissen lässt, nicht verarbeitet wird.Wenden Sie das
format!
-Makro auf die Parameterparam
(die gesammelten Parameter) undagent
(deruser-agent
-String) an, um einen String zu erzeugen und diesen an den Client zurückzugeben.
Entwickler, die dem kompositorischen Entwicklungsansatz gewogen sind, werden Warp zu schätzen wissen. Eine Konsequenz dieses Ansatzes bei diesem Web Framework: Es gibt mehrere Wege zur gleichen Lösung - und nicht alle davon sind intuitiv. Einen Blick auf die Beispiele im Repository von Warp zu werfen, lohnt sich deshalb.
Eine weitere Konsequenz ergibt sich aus der Art und Weise, wie Filter funktionieren: Viele Routes aus vielen verschiedenen Filtern zusammenzustellen, kann die Kompilierzeit erhöhen. Eine weitere Option für Sites mit vielen Routes ist es, die Funktion "Dynamic Dispatch" zu nutzen.
Axum
Das Rust-Framework Axum baut auf dem tower
-Crate-Ökosystem für Client/Server-Anwendungen aller Art auf und nutzt tokio
für async-Funktionen. Falls Sie bereits tower
-Erfahrungen vorweisen können, ist das an dieser Stelle von Vorteil.
Im Folgenden sehen Sie eine simple "hello world"-Anwendung - entnommen aus der Axum-Dokumentation. Sie werden feststellen, dass sie sich eher unwesentlich vom eingangs betrachteten Actix-Beispiel unterscheidet:
use axum::{
routing::get,
Router,
};
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(|| async { "Hello, World!" }));
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Wie Sie sehen, kommen in Axum viele derselben Patterns zur Anwendung wie bei Actix. Route-handler-Funktionen werden einem Router-Objekt mit der .route()
-Methode hinzugefügt - das axum::extract
-Modul enthält Typen, um URL-Komponenten zu extrahieren (oder für POST
-Payloads). Antwroten implementieren die IntoResponse-Eigenschaft, während Fehler über den tower
-eigenen tower::ServiceError
-Type behandelt werden.
Auf Router, Methoden und einzelne Handler kann Middleware über verschiedene .layer
-Methoden in tower
-Objekten angewendet werden. Sie können auch tower::ServiceBuilder
verwenden, um mehrere Layer zu aggregieren und sie gemeinsam anzuwenden.
Für andere gängige Web-Service-Patterns bringt Axum ein eigenes Toolset mit. Möglichkeiten, um typische Szenarien wie beispielsweise "Graceful Suhtdowns" oder auch Datenbankkonnektivität zu implementieren, finden Sie im Examples-Verzeichnis von Axum.
Poem
Für die allermeisten Programmiersprachen gibt es ein maximalistisches Web Framework mit vollem Funktionsumfang (etwa Django für Python) und einen minimalistischen Gegenpart (im Fall von Python wäre das Bottle). Poem vertritt letztgenannte Gruppe in Sachen Rust und bietet gerade genug Funktionalitäten, um einen grundlegenden Web Service auf die Beine zu stellen.
Im Folgenden ein "hello world"-Beispiel, das den Benutzernamen als Echo ausgibt, wenn dieser in der URL enthalten ist:
use poem::{get, handler, listener::TcpListener, web::Path, Route, Server};
#[handler]
fn hello(Path(name): Path<String>) -> String {
format!("hello: {}", name)
}
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
let app = Route::new().at("/hello/:name", get(hello));
Server::new(TcpListener::bind("0.0.0.0:3000"))
.run(app)
.await
}
Viele der Funktionen in dieser Anwendung sollten Ihnen aus den anderen Frameworks und Beispielen bekannt sein.
Um die Kompilierzeit niedrig zu halten, unterstützt Poem einige Funktionen standardmäßig nicht. Unter anderem müssen folgende Features manuell aktiviert werden:
Cookies,
CSRF-Projection,
HTTP über TLS,
WebSockets,
Internationalization, sowie
Request/Response Compression und -Decompression.
Seiner Schlankheit zum Trotz bietet Poem eine Fülle nützlicher Funktionen. Beispielsweise enthält das Rust Web Framework eine Reihe gängiger Middleware Pieces, die Sie auch relativ einfach selbst implementieren können. Eine durchdachte Annehmlichkeit ist der NormalizePath
-Mechanismus, der Request-Pfade konsistent gestaltet.
Die Example Directory von Poem ist im Vergleich zu anderen Frameworks eher überschaubar und fokussiert vor allem auf Beispielen, die eine detaillierte Dokumentation erfordern. Zum Beispiel, Poem in Kombination mit AWS Lambda zu verwenden oder APIs zu generieren, die der OpenAPI-Spezifikation entsprechen. (fm)
Dieser Beitrag basiert auf einem Artikel unserer US-Schwesterpublikation Infoworld.