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

в 8:15, , рубрики: actix-web, backend, Rust

Привет, сегодня я попытаюсь объяснить все то, что я хотел бы знать в начале пути разработки на 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 в целом).
Используйте, если вам нужна гигантская производительность, которую не могут предложить другие языки, иначе - не надо.

Спасибо за прочтение, удачи в освоение нового!

Автор: Persona36LQ

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js