Привет, сегодня я попытаюсь объяснить все то, что я хотел бы знать в начале пути разработки на 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] // Макрос для адекватной работы async fn main() 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; // Подробнее про 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 { println!("{:?}", 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 }
В консоли можно увидеть результат
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; // Имя 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(); } println!("{}", 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 в целом).
Используйте, если вам нужна гигантская производительность, которую не могут предложить другие языки, иначе — не надо.
Спасибо за прочтение, удачи в освоение нового!
ссылка на оригинал статьи https://habr.com/ru/articles/839158/
Добавить комментарий