Начало работы с Actix Web

от автора

Привет, сегодня я попытаюсь объяснить все то, что я хотел бы знать в начале пути разработки на Actix Web.

Немного лирики для начала.

Rust — мультипарадигменный компилируемый язык программирования общего назначения, разрабатываемый Mozilla. Очень рекомендую выучить базовые концепции, типы, синтаксис языка, немного узнать про cargo.

Actix Web — высокопроизводительный web framework для Rust. Собственно о нем и речь в статье.

В этой статье описано как писать базовые функции, использовать app_state, json, path в запросах. Также показано создание middleware

Подготовка.

1. Установка Rust (Если его почему-то нет)

  1. Инициализация проекта и установка зависимостей

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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *