Привет, сегодня я попытаюсь объяснить все то, что я хотел бы знать в начале пути разработки на Actix Web.
Немного лирики для начала.
Rust - мультипарадигменный компилируемый язык программирования общего назначения, разрабатываемый Mozilla. Очень рекомендую выучить базовые концепции, типы, синтаксис языка, немного узнать про cargo.
Actix Web - высокопроизводительный web framework для Rust. Собственно о нем и речь в статье.
В этой статье описано как писать базовые функции, использовать app_state, json, path в запросах. Также показано создание middleware
Подготовка.
1. Установка Rust (Если его почему-то нет)
Инициализация проекта и установка зависимостей
cargo init --bin actix_test # Инициализация проекта
cd actix_test
Добавим необходимые зависимости
cargo add actix-web env_logger log \
chrono --features chrono/serde \
serde --features serde/derive serde_json \
Немного пробежимся по зависимостям.
Env_logger и log - логирование в приложении
Chrono - библиотека для работы со временем
Serde - сериализация и десериализация из различных типов данных. В нашем случае serde_json
Начнем же писать код.
// main.rs
// Базовая структура проекта на actix web
// Импорты
use actix_web::{App, HttpServer};
use actix_web::middleware::Logger;
use log::info;
#[actix_web::main] // Делает boilerplate для запуска за нас
// https://docs.rs/actix-web/4.9.0/actix_web/rt/index.html
// https://docs.rs/actix-web/latest/actix_web/attr.main.html
async fn main() -> std::io::Result<()> {
// Для работы библиотеки log
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
// Просто чтобы понимать, что сервер запущен. Полезно в контейнерах
info!("Successfully started server");
// Грубо говоря конфигурация HttpServer
HttpServer::new(|| {
// Собственно само приложение со всеми handlers, middleware,
// информации приложения и т.д
App::new()
// .wrap() позволяет добавить middleware (промежуточную функцию) приложению
.wrap(Logger::default())
}).bind("0.0.0.0:8080")
.unwrap()
.run()
.await
}
После компиляции и запуска проекта можно отправить любой запрос на localhost:8080 и ответом всегда будет 404.
Исправим это написав простой handler, который будет возвращать Hello!
// main.rs
// Нужные импорты, не надо изменять предыдущие.
use actix_web::{get, Responder};
#[get("/")] // указывается тип запроса("/путь"),
// У этого handler запрос будет на http://localhost:8080
async fn hello() -> impl Responder // Responder это trait, который позволяет
// преобразовывать тип данных в HttpResponse. В основном он используется для
// примитивных функций
{
"Hello!"
}
// Добавим handler в App
#[actix_web::main] // Макрос для адекватной работы async fn main()
async fn main() -> std::io::Result<()> {
// Прежний код
HttpServer::new(|| {
App::new()
.wrap(Logger::default())
// .service() необходим для создания handlers
.service(hello)
}) // Прежний код
}
После компиляции при посещение localhost:8080 будет HttpResponse с кодом 200 и текстом, который мы написали.
Состояние приложения
Создадим struct в main.rs.
// main.rs
use actix_web::web::Data;
use std::sync::Mutex;
// Прежний код
// В этом struct нужно прописывать все, что может понадобиться
pub(crate) struct AppState {
app_name: String,
req_counter: Mutex<u32>
}
// Если переменная должна быть мутабельной, то надо использовать
// name: Mutex<T>
// После вызывая app_state.name.lock().unwrap() для изменений
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Прежний код
let app_state = Data::new(AppState {
app_name: "test".to_string(),
req_counter: Mutex::new(0)
});
info!("Successfully started server");
HttpServer::new(move || { // Необходимо добавить move
App::new()
.wrap(Logger::default())
.app_data(app_state.clone())
.service(hello)
}) // Прежний код
}
Сделаем отдельный файл app_state.rs и привяжем к main.rs
// main.rs
mod app_state;
// app_state.rs
use actix_web::{get, Responder};
use actix_web::web::Data;
use crate::AppState;
#[get("/app_name")]
pub(crate) async fn app_name(app_state: Data<AppState>) -> impl Responder {
// Возвращаем имя из app_state
app_state.app_name.clone()
}
#[get("/req")]
pub(crate) async fn req_counter(app_state: Data<AppState>) -> impl Responder {
let mut req_counter = app_state.req_counter.lock().unwrap();
*req_counter += 1;
format!("Requests sent: {}", req_counter)
}
Добавим handler
//main.rs
use app_state::{app_name, req_counter};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Прежний код
HttpServer::new(move || {
App::new()
.wrap(Logger::default())
.app_data(app_state.clone())
.service(hello)
.service(app_name)
.service(req_counter)
}) // Прежний код
}
Запускаем и отправляем запросы на localhost:8080/app_name и localhost:8080/req
JSON
Сделаем отдельный файл json.rs и привяжем к main.rs
// main.rs
mod json;
// json.rs
use actix_web::{HttpResponse, post};
#[post("/register")]
pub(crate) async fn json_test() -> HttpResponse
// HttpResponse это ответ сервера, который содержит статус код и информацию ответа
{
// Пустой Ok (200) ответ
HttpResponse::Ok().finish()
}
В actix_web есть специальный тип для json. Он имеет такую границу типа <T: serde::de::Deserialize>
// json.rs
use actix_web::{HttpResponse, post};
use actix_web::web::Json;
use serde::Deserialize;
use log::log;
// Подробнее про derive можно почитать тут
// https://doc.rust-lang.org/reference/procedural-macros.html#derive-macros
// TL;DR генерация кода
#[derive(Deserialize, Debug)]
struct Test{
field1: String,
field2: u32
}
#[post("/json/test")]
pub(crate) async fn json_test(json: Json<Test>) -> HttpResponse {
log!("{:?}", json);
HttpResponse::Ok().finish()
}
Теперь вернем информацию в формате Json
use actix_web::get;
#[get("/json/time")]
pub(crate) async fn json_time() -> HttpResponse {
let current_utc = chrono::Utc::now();
HttpResponse::Ok().json(current_utc)
}
Добавим handlers
// main.rs
use json::{json_test, json_time};
// Прежний код
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Прежний код
HttpServer::new(move || {
App::new()
.wrap(Logger::default())
.app_data(app_state.clone())
.service(hello)
.service(app_name)
.service(req_counter)
.service(json_test)
.service(json_time)
}) // Прежний код
}
Компилируем, запускаем, тестим.
Отправим post запрос на localhost:8080/json/test с таким payload
{ “field1”: “String”,“field2”: 123 }
curl -X POST http://localhost:8080/json/test \
-H "Content-Type: application/json" \
-d '{"field1": "String", "field2": 123}'
В консоли можно увидеть результат
Json(Test { field1: "String", field2: 123 })
Отправим get запрос на localhost:8080/json/time и получим текущее UTC время
Пути в url
Создадим path.rs
// main.rs
mod path;
// path.rs
use actix_web::{get, HttpResponse, web};
// Для web::Path можно указывать другие типы данных, например u32
#[get("/{path}")]
pub(crate) async fn single_path(path: web::Path<String>) -> HttpResponse {
HttpResponse::Ok().body(format!("You looked for {}", path))
}
#[get("/{path1}/{path2}")]
pub(crate) async fn multiple_paths(path: web::Path<(String, String)>) -> HttpResponse {
let (path1, path2) = path.into_inner();
HttpResponse::Ok().body(format!("You looked for {}/{}", path1, path2))
}
Добавим handlers
// main.rs
use path::{single_path, multiple_paths};
// Прежний код
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Прежний код
HttpServer::new(move || {
App::new()
.wrap(Logger::default())
.app_data(app_state.clone())
.service(hello)
.service(app_name)
.service(req_counter)
.service(json_test)
.service(json_time)
.service(single_path)
.service(multiple_paths)
}) // Прежний код
}
Немного тестов
localhost:8080/path и localhost:8080/path1/path2
Middlewares
Я нахожу их написание странными.
Для написания middleware, добавим еще одну библиотеку
cargo add futures-util
Создадим файл middleware.rs
// main.rs
mod middleware;
// middleware.rs
use std::future::{Ready, ready};
use actix_web::body::EitherBody;
use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform};
use actix_web::{Error, HttpResponse};
use futures_util::future::LocalBoxFuture;
use futures_util::FutureExt;
use log::info;
// Имя middleware
pub struct Test;
// Если интересно, то можно почитать тут
// https://docs.rs/actix-service/latest/actix_service/trait.Transform.html
// Если нет, то смотри ниже
impl<S, B> Transform<S, ServiceRequest> for Test
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type InitError = ();
type Transform = TestMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(TestMiddleware { service }))
}
}
// Рекомендуется использовать имя Middleware + слово Middleware
pub struct TestMiddleware<S> {
service: S,
}
// https://docs.rs/actix-service/latest/actix_service/trait.Service.html
impl<S, B> Service<ServiceRequest> for TestMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
// В целом тут прописывается вся логика
if req.headers().contains_key("random-header-key") {
// Логика ошибки
let http_res = HttpResponse::BadRequest().body("Not allowed to have \'random-header-key\' header");
let (http_req, _) = req.into_parts();
let res = ServiceResponse::new(http_req, http_res);
return (async move { Ok(res.map_into_right_body()) }).boxed_local();
}
info!("{}", req.method());
let fut = self.service.call(req);
Box::pin(async move {
let res = fut.await?;
Ok(res.map_into_left_body())
})
}
}
Выглядит страшно, но все нормально, наверное. Вот кстати документация на middleware
Добавим middleware
// main.rs
use middleware::Test;
// Прежний код
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Прежний код
HttpServer::new(move || {
App::new()
// Middleware идут по очереди по обратному порядку определения
// Тоесть сначала отработет Test и потом Logger
.wrap(Logger::default())
.wrap(Test)
.app_data(app_state.clone())
.service(hello)
.service(app_name)
.service(req_counter)
.service(json_test)
.service(json_time)
.service(single_path)
.service(multiple_paths)
}) // Прежний код
}
Полезные ссылки
Документация actix_web
Docs.rs
Примеры
Заключение
Actix-web - мощнейший инструмент. В этой статье я показал, как я считаю, удобный способ его использовать. Но есть еще несколько способов делать то, что я показал.
Важно понимать зачем и когда нужен actix-web (да и Rust в целом).
Используйте, если вам нужна гигантская производительность, которую не могут предложить другие языки, иначе - не надо.
Спасибо за прочтение, удачи в освоение нового!
Комментарии (11)
odisseylm
28.08.2024 10:37Actix-web - мощнейший инструмент
Чем он мощнее axum или poem?
Насколько я понимаю, axum самый популярный (не знаю почему, может в своё время был лучше всех разрекламирован?), но poem, судя по фичям, выглядит наиболее вкусно - насколько я понимаю там интеграция из коробки и с OpenAPI, и с grpc, и наверное ещё с чем-то... Для axum, чтобы это выглядело по человечески, нужно напильником всё допиливать...medigor
28.08.2024 10:37axum самый популярный (не знаю почему, может в своё время был лучше всех разрекламирован?)
просто он от разработчиков tokio
domix32
28.08.2024 10:37+1actix был практически первым и поэтому к текущему моменту он самый featureful. Axum - попытка сделать API адекватным и без костылей и легаси, которые скопились по мере развития в actix.
Persona36LQ Автор
28.08.2024 10:37"Actix-web - мощнейший инструмент" не означает, что он мощнее axum или poem. Я нахожу его более приятно выглядящим и именно поэтому статья о нем
domix32
28.08.2024 10:37+1>
#[actix_web::main] // Макрос для адекватной работы async fn main()
Это синхронная обёртка над асинхронным кодом. Там весь бойлерплейт для старта асинхронной функции.
mod app_state;
начиная с 2018 edition это можно не писать вроде.
тип <T: serde::de::Deserialize>
serde::de::Deserialize это граница типа, то бишь требование к нему.
Отправим post запрос на localhost:8080/json/test с таким payload
ну вы там для постмана json какие сварганили или curlом команды подёргали. Интеграционные тесты те же написали бы.
println!("{}", req.method());
а логгеры тогда зачем добавляли?
Ну и как-то очень новичково это все, SPAшки захардкоженные сервить. А где с базой работа? Где примеры с каким-нибудь простым состоянием, куками или JWT? Посервить статику с файлухи до кучи.
Persona36LQ Автор
28.08.2024 10:37Спасибо за подробный комментарий! Исправил ошибки. Это статья это супер база, которую я хотел бы знать, в последующих статьях покажу работу с базами данных, jwt
domix32
28.08.2024 10:37Остались ещё принты.
Запрос курла можно оформить в несколько строчек - экранирете перенос и ага
curl -X POST http://localhost:8080/json/test \ -H "Content-Type: application/json" \ -d '{"field1": "String", "field2": 123}'
В таком виде оно также спокойно копипастится в терминал как и длинный нечитаемый однострочник.
MountainGoat
Годно! Но показали бы ещё по быстрому, как файл отдать.
Persona36LQ Автор
Спасибо! Сейчас пишу вторую часть статьи, где будет это показано