- PVSM.RU - https://www.pvsm.ru -
Привет, сегодня я попытаюсь объяснить все то, что я хотел бы знать в начале пути разработки на Actix Web.
Немного лирики для начала.
Rust [1]- мультипарадигменный компилируемый язык программирования общего назначения, разрабатываемый Mozilla. Очень рекомендую выучить базовые концепции, типы, синтаксис языка, немного узнать про cargo [2].
Actix Web [3] - высокопроизводительный web framework для Rust. Собственно о нем и речь в статье.
В этой статье описано как писать базовые функции, использовать app_state, json, path в запросах. Также показано создание middleware
Подготовка.
1. Установка Rust [4] (Если его почему-то нет)
Инициализация проекта и установка зависимостей
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 [5]и log [6] - логирование в приложении
Chrono [7] - библиотека для работы со временем
Serde [8]- сериализация и десериализация из различных типов данных. В нашем случае serde_json [9]
Начнем же писать код.
// 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 [10] и ответом всегда будет 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 [10] будет 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 [11] и localhost:8080/req [12]
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 [13] с таким payload
{ “field1”: “String”,“field2”: 123 }
В консоли можно увидеть результат
Json(Test { field1: "String", field2: 123 })
Отправим get запрос на localhost:8080/json/time [14] и получим текущее 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 [15] и localhost:8080/path1/path2 [16]
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 [17]
Добавим 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 [3]
Docs.rs [18]
Примеры [19]
Заключение
Actix-web - мощнейший инструмент. В этой статье я показал, как я считаю, удобный способ его использовать. Но есть еще несколько способов делать то, что я показал.
Важно понимать зачем и когда нужен actix-web (да и Rust в целом).
Используйте, если вам нужна гигантская производительность, которую не могут предложить другие языки, иначе - не надо.
Спасибо за прочтение, удачи в освоение нового!
Автор: Persona36LQ
Источник [20]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/backend/394851
Ссылки в тексте:
[1] Rust : https://ru.wikipedia.org/wiki/Rust_(%D1%8F%D0%B7%D1%8B%D0%BA_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F)
[2] немного узнать про cargo: https://doc.rust-lang.org/cargo/getting-started/first-steps.html
[3] Actix Web: https://actix.rs/
[4] Установка Rust: https://www.rust-lang.org/tools/install
[5] Env_logger : https://docs.rs/env_logger/latest/env_logger/
[6] log: https://docs.rs/log/latest/log/
[7] Chrono: https://docs.rs/chrono/latest/chrono/
[8] Serde : https://docs.rs/serde/latest/serde/
[9] serde_json: https://docs.rs/serde_json/latest/serde_json/
[10] localhost:8080: http://localhost:8080/
[11] localhost:8080/app_name: http://localhost:8080/app_name
[12] localhost:8080/req: http://localhost:8080/req
[13] localhost:8080/json/test: http://localhost:8080/json/test
[14] localhost:8080/json/time: http://localhost:8080/json/time
[15] localhost:8080/path: https://localhost:8080/path
[16] localhost:8080/path1/path2: http://localhost:8080/path1/path2
[17] Вот кстати документация на middleware: https://actix.rs/docs/middleware/
[18] Docs.rs : https://docs.rs/actix-web/latest/actix_web/
[19] Примеры: https://github.com/actix/examples/
[20] Источник: https://habr.com/ru/articles/839158/?utm_source=habrahabr&utm_medium=rss&utm_campaign=839158
Нажмите здесь для печати.