Интеграция с Tower
Tower — это экосистема модульных компонентов для построения надежных клиент-серверных приложений в Rust. Axum глубоко интегрирован с Tower, что обеспечивает мощные возможности для композиции middleware и создания гибких HTTP-сервисов. В этом разделе мы рассмотрим, как Axum работает с Tower и как эффективно использовать эту интеграцию.
Основные концепции Tower
Архитектура Tower
Tower построен вокруг трех ключевых абстракций:
- Service — это центральный типаж (trait) в Tower, который определяет сущность, способную обрабатывать запросы:
pub trait Service<Request> {
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;
}
- Layer — типаж, который применяет middleware к сервису:
pub trait Layer<S> {
type Service;
fn layer(&self, inner: S) -> Self::Service;
}
- MakeService — фабрика, создающая сервисы:
pub trait MakeService<Target, Request> {
type Response;
type Error;
type Service: Service<Request, Response = Self::Response, Error = Self::Error>;
type MakeError;
type Future: Future<Output = Result<Self::Service, Self::MakeError>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::MakeError>>;
fn make_service(&mut self, target: Target) -> Self::Future;
}
Типы сервисов в Tower
Tower предоставляет несколько типов сервисов, которые можно использовать для построения приложений:
- ServiceBuilder — удобный интерфейс для композиции слоев
- BoxService — тип-обертка, который стирает конкретный тип сервиса
- Buffer — буферизует запросы, когда внутренний сервис не готов
- Timeout — ограничивает время выполнения запроса
- Retry — повторяет запросы при возникновении ошибок
- Rate — ограничивает скорость обработки запросов
- Load — распределяет нагрузку между несколькими сервисами
Интеграция Axum с Tower
Router как реализация Service
Центральный компонент Axum — Router
— реализует типаж Service
. Это позволяет применять к нему Tower middleware и интегрировать его с другими компонентами экосистемы Tower.
use axum::{Router, routing::get};
use tower::ServiceBuilder;
use tower_http::trace::TraceLayer;
let app = Router::new()
.route("/", get(handler));
// Применение Tower middleware с помощью ServiceBuilder
let app = ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.service(app);
Трансформация Router в MakeService
Для работы с HTTP-сервером Axum необходимо преобразовать Router в MakeService. Для этого используется метод .into_make_service()
:
use axum::{Router, routing::get};
use std::net::SocketAddr;
let app = Router::new()
.route("/", get(handler));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
Этот метод создает MakeService, который для каждого соединения создает новый экземпляр Router.
Применение Tower middleware
В Axum есть несколько способов применения Tower middleware:
- Метод
.layer()
— добавляет слой к роутеру:
use axum::{Router, routing::get};
use tower_http::compression::CompressionLayer;
let app = Router::new()
.route("/", get(handler))
.layer(CompressionLayer::new());
- ServiceBuilder — более гибкий подход для композиции нескольких слоев:
use axum::{Router, routing::get};
use tower::ServiceBuilder;
use tower_http::{
compression::CompressionLayer,
trace::TraceLayer,
timeout::TimeoutLayer,
};
use std::time::Duration;
let app = Router::new()
.route("/", get(handler));
let app = ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(CompressionLayer::new())
.layer(TimeoutLayer::new(Duration::from_secs(30)))
.service(app);
Использование tower-http
Пакет tower-http содержит множество полезных HTTP-специфичных middleware для Tower, которые можно использовать с Axum:
use axum::{Router, routing::get};
use tower::ServiceBuilder;
use tower_http::{
compression::CompressionLayer,
trace::TraceLayer,
cors::CorsLayer,
limit::RequestBodyLimitLayer,
catch_panic::CatchPanicLayer,
timeout::TimeoutLayer,
add_extension::AddExtensionLayer,
sensitive_headers::SetSensitiveHeadersLayer,
};
use std::time::Duration;
let app = Router::new()
.route("/", get(handler));
let app = ServiceBuilder::new()
// Трассировка запросов
.layer(TraceLayer::new_for_http())
// Перехват паники в обработчиках
.layer(CatchPanicLayer::new())
// Таймаут для запросов
.layer(TimeoutLayer::new(Duration::from_secs(30)))
// CORS
.layer(CorsLayer::permissive())
// Ограничение размера тела запроса
.layer(RequestBodyLimitLayer::new(1024 * 1024)) // 1 MB
// Скрытие чувствительных заголовков из логов
.layer(SetSensitiveHeadersLayer::new(vec!["authorization", "cookie"]))
// Добавление данных в расширения запроса
.layer(AddExtensionLayer::new(MyAppState::default()))
// Сжатие ответов
.layer(CompressionLayer::new())
.service(app);
Создание собственных Tower middleware
Вы можете создавать собственные Tower middleware путем реализации типажей Layer
и Service
:
use std::{
pin::Pin,
future::{Future, Ready},
task::{Context, Poll},
marker::PhantomData,
};
use tower::{Layer, Service};
use axum::http::{Request, Response};
// Определение Layer
#[derive(Clone)]
struct LoggingLayer {
target: &'static str,
}
impl LoggingLayer {
pub fn new(target: &'static str) -> Self {
Self { target }
}
}
// Реализация Layer
impl<S> Layer<S> for LoggingLayer {
type Service = LoggingMiddleware<S>;
fn layer(&self, inner: S) -> Self::Service {
LoggingMiddleware {
inner,
target: self.target,
}
}
}
// Определение Service
#[derive(Clone)]
struct LoggingMiddleware<S> {
inner: S,
target: &'static str,
}
// Реализация Service
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for LoggingMiddleware<S>
where
S: Service<Request<ReqBody>, Response = Response<ResBody>>,
S::Future: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
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, request: Request<ReqBody>) -> Self::Future {
println!("[{}] {} {}", self.target, request.method(), request.uri());
let start = std::time::Instant::now();
let future = self.inner.call(request);
Box::pin(async move {
let response = future.await?;
let duration = start.elapsed();
println!("[{}] Completed in {:?}", self.target, duration);
Ok(response)
})
}
}
// Использование
let app = Router::new()
.route("/", get(handler))
.layer(LoggingLayer::new("api"));
Комбинирование Tower middleware
Tower позволяет комбинировать middleware различными способами:
Последовательное применение слоев
use tower::ServiceBuilder;
let app = ServiceBuilder::new()
.layer(layer1)
.layer(layer2)
.layer(layer3)
.service(app);
Условное применение слоев
use tower::ServiceBuilder;
let mut builder = ServiceBuilder::new();
if config.tracing_enabled {
builder = builder.layer(tower_http::trace::TraceLayer::new_for_http());
}
if config.compression_enabled {
builder = builder.layer(tower_http::compression::CompressionLayer::new());
}
let app = builder.service(app);
Оптимизация производительности
Tower предоставляет middleware для оптимизации производительности:
use tower::ServiceBuilder;
use tower::limit::{RateLimitLayer, ConcurrencyLimitLayer};
use tower::buffer::BufferLayer;
use tower::load_shed::LoadShedLayer;
use std::time::Duration;
let app = ServiceBuilder::new()
// Ограничивает количество одновременных запросов
.layer(ConcurrencyLimitLayer::new(100))
// Ограничивает скорость запросов
.layer(RateLimitLayer::new(50, Duration::from_secs(1)))
// Отклоняет запросы при перегрузке
.layer(LoadShedLayer::new())
// Буферизует запросы
.layer(BufferLayer::new(1024))
.service(app);
Поддержка сервисов не-HTTP типов
Tower не ограничивается HTTP, и вы можете использовать его типы для любых клиент-серверных абстракций:
use tower::{Service, ServiceBuilder, buffer::BufferLayer, timeout::TimeoutLayer};
use std::time::Duration;
use futures::future::BoxFuture;
// Определяем собственный тип запроса
struct CustomRequest {
id: u64,
payload: Vec<u8>,
}
// Определяем сервис для обработки этих запросов
struct CustomService;
impl Service<CustomRequest> for CustomService {
type Response = Vec<u8>;
type Error = anyhow::Error;
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: CustomRequest) -> Self::Future {
println!("Processing request with ID: {}", req.id);
Box::pin(async move {
// Обработка запроса
Ok(req.payload)
})
}
}
// Применяем Tower middleware
let service = ServiceBuilder::new()
.layer(TimeoutLayer::new(Duration::from_secs(5)))
.layer(BufferLayer::new(100))
.service(CustomService);
Лучшие практики работы с Tower
Типизация и стирание типов
Tower сильно полагается на статическую типизацию, что может приводить к сложным типам при композиции слоев. Для упрощения можно использовать стирание типов:
use tower::util::ServiceExt;
use tower::BoxService;
// Создаем сервис со стертым типом
let app: BoxService<Request<Body>, Response<Body>, hyper::Error> =
app.map_response(|res| res.map(Body::from))
.boxed();
Обработка ошибок
Обработка ошибок в Tower middleware может быть сложной. Рекомендуется использовать типаж Map для преобразования ошибок:
use tower::util::ServiceExt;
let app = app
.map_err(|e| {
eprintln!("Service error: {:?}", e);
axum::http::StatusCode::INTERNAL_SERVER_ERROR
});
Тестирование сервисов
Tower облегчает тестирование HTTP-сервисов без запуска реального сервера:
use tower::ServiceExt;
use axum::http::{Request, StatusCode};
use axum::body::Body;
#[tokio::test]
async fn test_service() {
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }))
.layer(tower_http::trace::TraceLayer::new_for_http());
// Создаем запрос
let request = Request::builder()
.uri("/")
.body(Body::empty())
.unwrap();
// Тестируем как сервис напрямую
let response = app
.oneshot(request)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
Заключение
Интеграция Axum с Tower предоставляет мощный и гибкий фундамент для построения HTTP-сервисов. Используя Tower, вы получаете доступ к богатой экосистеме middleware, которые могут быть скомпонованы различными способами для достижения нужной функциональности.
Эта интеграция дает Axum несколько ключевых преимуществ:
- Модульность — каждый middleware решает одну конкретную задачу и может быть применен независимо
- Компонуемость — middleware можно комбинировать для создания сложных сервисов
- Типобезопасность — система типов Rust гарантирует корректность композиции
- Производительность — Tower оптимизирован для высокой пропускной способности
- Расширяемость — возможность легко создавать собственные middleware
Эффективное использование Tower позволяет создавать надежные и высокопроизводительные веб-приложения в Axum, способные масштабироваться до высоких нагрузок.