Привет, сегодня я попытаюсь объяснить все то, что я хотел бы знать в начале пути разработки на 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] // Делает 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)


  1. MountainGoat
    28.08.2024 10:37

    Годно! Но показали бы ещё по быстрому, как файл отдать.


    1. Persona36LQ Автор
      28.08.2024 10:37

      Спасибо! Сейчас пишу вторую часть статьи, где будет это показано


  1. odisseylm
    28.08.2024 10:37

    Actix-web - мощнейший инструмент

    Чем он мощнее axum или poem?
    Насколько я понимаю, axum самый популярный (не знаю почему, может в своё время был лучше всех разрекламирован?), но poem, судя по фичям, выглядит наиболее вкусно - насколько я понимаю там интеграция из коробки и с OpenAPI, и с grpc, и наверное ещё с чем-то... Для axum, чтобы это выглядело по человечески, нужно напильником всё допиливать...


    1. zoto_ff
      28.08.2024 10:37

      ни разу не слышал про axum

      зато знаю actix, ntex и may-minihttp


    1. medigor
      28.08.2024 10:37

      axum самый популярный (не знаю почему, может в своё время был лучше всех разрекламирован?)

      просто он от разработчиков tokio


    1. domix32
      28.08.2024 10:37
      +1

      actix был практически первым и поэтому к текущему моменту он самый featureful. Axum - попытка сделать API адекватным и без костылей и легаси, которые скопились по мере развития в actix.


    1. Persona36LQ Автор
      28.08.2024 10:37

      "Actix-web - мощнейший инструмент" не означает, что он мощнее axum или poem. Я нахожу его более приятно выглядящим и именно поэтому статья о нем


  1. 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? Посервить статику с файлухи до кучи.


    1. Persona36LQ Автор
      28.08.2024 10:37

      Спасибо за подробный комментарий! Исправил ошибки. Это статья это супер база, которую я хотел бы знать, в последующих статьях покажу работу с базами данных, jwt


      1. domix32
        28.08.2024 10:37

        Остались ещё принты.

        Запрос курла можно оформить в несколько строчек - экранирете перенос и ага

        curl -X POST http://localhost:8080/json/test \
        -H "Content-Type: application/json"          \
        -d '{"field1": "String", "field2": 123}'

        В таком виде оно также спокойно копипастится в терминал как и длинный нечитаемый однострочник.


  1. maxkov36
    28.08.2024 10:37

    Хорошая статья, относительно подробно расписанно всё, очень помогла