Entschlüsselung der Tower-Abstraktionsebene in Axum und Tonic
Olivia Novak
Dev Intern · Leapcell

Einleitung
In der sich rasant entwickelnden Landschaft der Netzwerkprogrammierung ist der Aufbau robuster, skalierbarer und wartbarer Dienste von größter Bedeutung. Rust hat sich mit seinem Fokus auf Leistung und Sicherheit zu einem starken Kandidaten für die Entwicklung solcher Systeme entwickelt. Zwei prominente Frameworks, Axum für Webanwendungen und Tonic für gRPC-Dienste, nutzen eine leistungsstarke zugrunde liegende Abstraktion namens Tower. Tower bietet eine modulare und komponierbare Möglichkeit, Netzwerkdienste zu erstellen und gängige Herausforderungen wie Routing von Anfragen, Fehlerbehandlung und Middleware-Integration zu bewältigen. Dieser Artikel zielt darauf ab, die Kernkomponenten von Tower – Service, Layer und BoxCloneService – zu entmystifizieren und zu veranschaulichen, wie sie das Rückgrat von Axum und Tonic bilden und elegante und erweiterbare Service-Architekturen ermöglichen. Das Verständnis dieser Abstraktionen ist keine rein akademische Übung; es erschließt das volle Potenzial dieser Frameworks und ermöglicht es Entwicklern, hochgradig angepasste und effiziente Dienste zu erstellen.
Das Tower-Kernverständnis
Bevor wir uns damit befassen, wie Axum und Tonic Tower nutzen, wollen wir diese grundlegenden Bausteine klar verstehen.
Der Service-Trait
Im Herzen von Tower steht der Service-Trait. Er repräsentiert eine asynchrone Funktion, die eine Anfrage bearbeitet und eine Future zurückgibt, die zu einer Antwort oder einem Fehler führt. Betrachten Sie ihn als eine generische Schnittstelle für jede Komponente, die ein eingehendes Element verarbeitet und ein ausgehendes Element erzeugt.
pub trait Service<Request>: Sized { type Response; type Error; type Future: Future<Output = Result<Self::Response, Self::Error>>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>; fn call(&mut self, req: Request) -> Self::Future; }
Request: Der Typ der Eingabe, die dieser Dienst akzeptiert.Response: Der Typ der erfolgreichen Ausgabe, die dieser Dienst erzeugt.Error: Der Typ des Fehlers, den dieser Dienst zurückgeben kann.Future: Eine asynchrone Operation, die schließlich mit einerResponseoder einemErrorabgeschlossen wird.poll_ready: Diese Methode ist entscheidend für Backpressure. Sie ermöglicht es dem Dienst, zu signalisieren, ob er bereit ist, eine neue Anfrage zu bearbeiten. Wenn erPoll::Pendingzurückgibt, sollte der Aufrufer warten, bevor ercallaufruft.call: Dies ist die Kernlogik, bei der der Dienst dieRequestverarbeitet und eineFuturezurückgibt, die die endgültigeResponserepräsentiert.
Im Kontext von Axum repräsentiert ein Service oft einen HTTP-Handler, der eine http::Request entgegennimmt und eine http::Response zurückgibt. Für Tonic werden gRPC-Methoden von diesem Dienst verarbeitet, indem eingehende gRPC-Anfragen in Antworten übersetzt werden.
Der Layer-Trait
Während Service eine einzelne Arbeitseinheit definiert, bietet Layer einen Mechanismus zur Komposition und Modifikation von Diensten. Ein Layer ist im Wesentlichen eine höherwertige Funktion für Dienste; er nimmt einen inneren Service entgegen und gibt einen neuen (möglicherweise verpackten) Service zurück, der bereichsübergreifende Belange hinzufügt oder das Verhalten modifiziert.
pub trait Layer<S> { type Service: Service<S::Request, Response = S::Response, Error = S::Error>; fn layer(&self, inner: S) -> Self::Service; }
S: Der Typ des inneren Dienstes, den diese Schicht umschließen wird.Service: Der Typ des neuen, umschlossenen Dienstes, der von dieser Schicht erzeugt wird.layer: Diese Methode nimmt eineninner-Dienst entgegen und gibt einen neuen Dienst zurück.
Layer ist grundlegend für Middleware. Häufige Beispiele sind:
- Logging Layer: Protokolliert eingehende Anfragen und ausgehende Antworten.
 - Rate Limiting Layer: Erzwingt Beschränkungen für die Anzahl der Anfragen, die ein Dienst bearbeiten kann.
 - Authentication Layer: Prüft Anmeldeinformationen, bevor Anfragen an den inneren Dienst weitergeleitet werden.
 - Metrics Layer: Sammelt Leistungsdaten wie die Dauer von Anfragen.
 
Lassen Sie uns dies mit einer einfachen Logging-Schicht veranschaulichen:
use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; use tower::{Service, Layer}; // Eine Dummy-Anfrage und -Antwort zur Veranschaulichung #[derive(Debug)] struct MyRequest(String); struct MyResponse(String); type MyError = std::io::Error; // Einfacher Fehlertyp // Ein Beispiel-Service struct MyService; impl Service<MyRequest> for MyService { type Response = MyResponse; type Error = MyError; type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { Poll::Ready(Ok(())) } fn call(&mut self, req: MyRequest) -> Self::Future { println!(" (Inner Service) Processing request: {}", req.0); Box::pin(async move { Ok(MyResponse(format!("Response to {}", req.0))) }) } } // Unser Logging-Middleware-Typ struct LogLayer; impl<S> Layer<S> for LogLayer where S: Service<MyRequest, Response = MyResponse, Error = MyError> + Send + 'static, S::Future: Send + 'static, { type Service = LogService<S>; fn layer(&self, inner: S) -> Self::Service { LogService { inner } } } // Der von LogLayer erzeugte Service #[derive(Clone)] struct LogService<S> { inner: S, } impl<S> Service<MyRequest> for LogService<S> where S: Service<MyRequest, Response = MyResponse, Error = MyError> + 'static, S::Future: Send + 'static, { type Response = MyResponse; type Error = MyError; type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { self.inner.poll_ready(cx) } fn call(&mut self, req: MyRequest) -> Self::Future { println!("(Log Layer) Incoming request: {:?}", req); let fut = self.inner.call(req); Box::pin(async move { let res = fut.await; println!("(Log Layer) Outgoing response: {:?}", res.as_ref().map(|r| r.0.clone())); res }) } } #[tokio::main] async fn main() { let my_service = MyService; let logged_service = LogLayer.layer(my_service); let res1 = logged_service.call(MyRequest("hello".to_string())).await.unwrap(); println!("Main received: {} ", res1.0); // Hinweis: Der `logged_service` kann hier nicht erneut aufgerufen werden, da `MyService` von `logged_service.call` verbraucht wird. // Dies führt uns zu `BoxCloneService`. }
Dieses Beispiel zeigt, wie LogLayer MyService umschließt, um LogService zu erstellen, das vor und nach der Ausführung des inneren Dienstes Protokollierung hinzufügt. Beachten Sie das Clone bei LogService; dies ist wichtig, da Layer::layer eine neue Service-Instanz zurückgibt, die in realen Anwendungen oft für gleichzeitige Verarbeitung Clonebar sein muss.
Der BoxCloneService-Typ
Der Service-Trait allein ist oft nicht objektsicher. Das bedeutet, dass Sie Box<dyn Service<...>> nicht direkt verwenden können, um seinen Typ zu löschen, was Polymorphismus und dynamische Ausführung einschränkt. Reale Dienste müssen oft für gleichzeitige Verarbeitung oder zum Speichern in verschiedenen Datenstrukturen geklont werden. Tower stellt BoxCloneService zur Verfügung, um diese Herausforderungen zu bewältigen.
BoxCloneService ist ein Typalias für eine Box, die einen Service umschließt, der Send, Sync und Clone ist und dessen Future ebenfalls Send und static ist. Dies ermöglicht dynamische Ausführung und Klonen von Diensten, was für Routing und parallele Ausführung unerlässlich ist.
// Vereinfachte Darstellung pub type BoxCloneService<Request, Response, Error> = Box<dyn Service<Request, Response = Response, Error = Error, Future = Pin<Box<dyn Future<Output = Result<Response, Error>> + Send>>> + Send + Sync + Clone>;
Wichtige Aspekte:
Box: Ermöglicht Heap-Allokation und dynamische Ausführung.dyn Service<...> + Send + Sync + Clone: Bedeutet, dass der zugrunde liegende konkrete Servicetyp dynamisch ausgeführt, sicher zwischen Threads gesendet, zwischen Threads geteilt und geklont werden kann.Future = Pin<Box<dyn Future<...> + Send>>: Stellt sicher, dass die voncallzurückgegebene Future ebenfalls dynamisch ausgeführt undSendist.
Wann würden Sie BoxCloneService verwenden?
- Routing: Wenn Sie verschiedene Dienste haben, zu denen Sie Anfragen basierend auf bestimmten Kriterien weiterleiten möchten und diese Dienste unterschiedliche konkrete Typen haben, können Sie mit 
BoxCloneServicesie in einer gemeinsamen Sammlung speichern. - Middleware-Ketten: Erstellen komplexer Middleware-Ketten, bei denen jede Schicht einen geboxeten Dienst zurückgeben muss.
 - Framework-Interna: Axum und Tonic verwenden intensiv 
BoxCloneServiceintern, um Handler-Funktionen und Implementierungen von gRPC-Methoden zu verwalten, wodurch ihre APIs flexibler werden. 
Wenn wir unser Logging-Beispiel überdenken, wenn MyService nach dem Layering mehrmals aufgerufen werden müsste:
use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; use tower::{Service, Layer}; use tower::ServiceBuilder; // Für einfacheres Layering // ... (MyRequest, MyResponse, MyError, MyService, LogLayer, LogService wie zuvor) ... #[tokio::main] async fn main() { let my_service = MyService; // Verwenden von ServiceBuilder zum einfacheren Klonen und Kombinieren von Schichten let layered_service = ServiceBuilder::new() .layer(LogLayer) // Unsere Logging-Schicht hinzufügen .service(my_service); // Der Basisservice // Jetzt können wir ihn mehrmals aufrufen, da ServiceBuilder `Clone` sicherstellt // (oder verbraucht und klonbare Dienste bei Bedarf neu erzeugt) let res1 = layered_service.call(MyRequest("hello".to_string())).await.unwrap(); println!("Main received: {} ", res1.0); let res2 = layered_service.call(MyRequest("world".to_string())).await.unwrap(); println!("Main received: {} ", res2.0); // Wenn wir es für die Typenlöschung boxen wollten (z. B. in einem Router) let boxed_service = ServiceBuilder::new() .boxed_clone() // Box den Dienst zu einem BoxCloneService .service(MyService); // Der Basisservice let res3 = boxed_service.call(MyRequest("boxed".to_string())).await.unwrap(); println!("Main received: {} ", res3.0); }
Die Methode ServiceBuilder::boxed_clone() ist hier entscheidend. Sie nimmt den konkreten Dienst (nach allen vorherigen Schichten) und packt ihn in einen BoxCloneService, wodurch er polymorph verwendet und nach Bedarf geklont werden kann. Dies ist entscheidend für das Routing von Axum, wo jede Route potenziell eine Anfrage mit einem anderen zugrunde liegenden Service-Typ bearbeiten kann, aber alle vom Router einheitlich behandelt werden müssen.
Wie Axum und Tonic Tower nutzen
Axum: Web-Framework auf Tower aufgebaut
Axums Kernphilosophie ist es, die Komplexität zu minimieren und die Flexibilität durch direkten Aufbau auf Tower zu maximieren.
- Handler als Dienste: In Axum sind Ihre Routen-Handler im Wesentlichen 
Services. Wenn Sieaxum::Router::get("/", handler_fn)definieren, wirdhandler_fnin eineService-Instanz umgewandelt. Axums Extraktoren und Responder (Json,Path,Htmlusw.) funktionieren, indem sie Logik implementieren, die auf Typen operiert oder diese erzeugt, die vomService-Trait oder seinen zugehörigen Typen verarbeitbar sind. - Middleware als Layer: Axums Middleware-Funktionen (
axum::Router::layer,axum::Router::fallback_service) erwarten einenLayer. Dies ermöglicht es Ihnen, problemlos beliebige Tower-kompatible Middleware für Protokollierung, Authentifizierung, Komprimierung usw. einzubinden. - Routing mit BoxCloneService: Axums 
Routerverwaltet intern eine Sammlung von Diensten (Ihre Routen-Handler und ihre zugehörigen Middleware). Um diese verschiedenen Dienste polumorph zu speichern, verwendet erBoxCloneServiceoder ähnliche geboxte Konstrukte, wodurch er eingehende Anfragen dem richtigen Handler zuordnen und diesen Handler dann aufrufen kann. 
// Axum-Beispiel, das nur implizit Tower-Konzepte zeigt use axum::{ routing::{get}, response::IntoResponse, Router, }; use tower_http::trace::TraceLayer; // Eine gängige Tower-Schicht async fn hello_world() -> impl IntoResponse { "Hello, Axum!" } #[tokio::main] async fn main() { // hello_world wird implizit in einen Service umgewandelt let app = Router::new() .route("/", get(hello_world)) // TraceLayer ist eine Tower Layer .layer(TraceLayer::new_for_http()); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app).await.unwrap(); }
Hier fungiert TraceLayer als Layer, der den aus hello_world() erstellten Dienst umschließt, um eine Anfragetracierung hinzuzufügen. Der Router selbst ist ein Service, der Anfragen basierend auf dem Pfad an den entsprechenden internen Dienst weiterleitet.
Tonic: gRPC-Framework für Rust
Tonic, das gRPC-Framework für Rust, stützt sich ebenfalls stark auf Tower.
- gRPC-Methoden als Dienste: Jede gRPC-Methode, die Sie in einem Tonic-Dienst implementieren, ist im Wesentlichen eine 
Service-Instanz. Tonic bietet Makros und Code-Generierung, um Ihre Rust-Funktionen in Tower-kompatible Dienste zu konvertieren, die die Semantik von gRPC-Anfragen/Antworten verarbeiten. - Middleware für gRPC: Genau wie Axum bietet Tonics 
tonic::transport::ServerMethoden zum Anwenden vonLayers auf Ihre gRPC-Dienste. Dies ist von unschätzbarem Wert für die Implementierung gRPC-spezifischer Middleware, wie z. B. Interceptors für Authentifizierung, Autorisierung oder benutzerdefinierte Metrikerfassung für gRPC-Aufrufe. - Service Stack: Tonic erstellt einen Tower-Dienst-Stack, beginnend mit Ihrer gRPC-Methodenimplementierung, wendet Schichten für die Protokollbehandlung (wie HTTP/2) und dann Ihre benutzerdefinierte Middleware an und gibt schließlich einen einzelnen 
Servicefrei, der HTTP/2-Frames verarbeitet. 
// Tonic-Beispiel, das Tower-Layers zeigt use tonic::{transport::Server, Request, Response, Status}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; use tower_http::trace::TraceLayer; pub mod hello_world { tonic::include_proto!("helloworld"); } #[derive(Debug, Default)] pub struct MyGreeter; #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request<HelloRequest>, ) -> Result<Response<HelloReply>, Status> { println!("Got a request from {:?}", request.remote_addr()); let reply = hello_world::HelloReply { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let addr = "[::1]:50051".parse()?; let greeter = MyGreeter::default(); println!("GreeterServer listening on {}", addr); Server::builder() // Eine TraceLayer auf den gRPC-Dienst anwenden .layer(TraceLayer::new_for_grpc()) .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) }
In diesem Tonic-Beispiel ist TraceLayer::new_for_grpc() wieder ein Layer, das den GreeterServer (der selbst einen Service für jede gRPC-Methode implementiert) umschließt. Die Syntax Server::builder().layer(...) spiegelt direkt die Anwendung eines Tower Layer wider.
Fazit
Die Tower-Abstraktionsebene mit ihren Kernkomponenten Service, Layer und BoxCloneService bietet eine unglaublich leistungsstarke und flexible Grundlage für den Aufbau von Netzwerkanwendungen in Rust. Durch das Verständnis dieser Konzepte können Entwickler nicht nur Frameworks wie Axum und Tonic effektiv nutzen, sondern sie auch um benutzerdefinierte Middleware erweitern und verschiedene Service-Komponenten nahtlos integrieren. Tower verkörpert die Rust-Philosophie der Komponierbarkeit und Typsicherheit und ermöglicht die Erstellung von Hochleistungs-, robusten und wartbaren Netzwerkdiensten. Es vereinfacht grundlegend die komplexe Aufgabe des Aufbaus widerstandsfähiger verteilter Systeme.

