Обзор библиотеки Actix в Rust

от автора

Привет, Хабр!

Actix появился на свет благодаря Николаю Киму, также известного в сообществе под ником fafhrd91. Николай создал экосистему, которая позволяет строить масштабируемые, надежные веб-приложения и микросервисы. Actix основан на модели акторов, концепции, которая была впервые предложена в 1973 году для симуляции активных компонентов в распределенных системах.

Actix требует Rust версии 1.39 или выше. Можно легко добавить Actix юзая Cargo. Для этого достаточно добавить зависимости Actix-web в файл Cargo.toml вашего проекта:

[dependencies] actix-web = "4.0"

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

Основные возможности

Основные элементы Actix — это акторы, система и арбитр.

Актор — это объект, который инкапсулирует состояние и поведение. Он взаимодействует с другими акторами исключительно посредством асинхронных сообщений:

use actix::prelude::*;  struct MyActor; impl Actor for MyActor {     type Context = Context<Self>; }  impl MyActor {     fn new() -> Self {         MyActor     } }

Каждый актор в Actix должен реализовать трейт Actor, определяющий контекст, в котором актор исполняется.

System в Actix — это контейнер, который запускает и управляет акторами.

fn main() {     let system = System::new();     system.block_on(async {         let addr = MyActor::new().start();         // отправка сообщений актору и другие операции     }); }

Arbiter — это сущность, управляющая пулом потоков для выполнения асинхронных задач. Каждый Arbiter может запускать несколько акторов, обеспечивая параллельное выполнение их задач:

fn main() {     let system = System::new();     system.block_on(async {         let arbiter = Arbiter::new();         arbiter.spawn(async {             // задачи, выполняемые в контексте нового Arbiter         });     }); }

Как это все взаимодействует:

use actix::prelude::*;  struct Ping; impl Message for Ping {     type Result = String; }  struct PongActor; impl Actor for PongActor {     type Context = Context<Self>; }  impl Handler<Ping> for PongActor {     type Result = String;      fn handle(&mut self, _msg: Ping, _ctx: &mut Context<Self>) -> Self::Result {         "Pong".to_string()     } }  fn main() {     let system = System::new();     system.block_on(async {         let pong_actor_address = PongActor.start();         let result = pong_actor_address.send(Ping).await.unwrap();         println!("Received: {}", result);     }); }

PongActor обрабатывает сообщения типа Ping и отвечает сообщением «Pong». Cоздаем актора, отправляем ему сообщение и асинхронно ожидаем ответа.

Обработчики могут быть простыми, возвращая статические данные, или сложными, выполняя асинхронные операции, такие как запросы к базам данных или внешним API. В Actix Web для объявления обработчика используется макрос #[get("/path")], #[post("/path")] и так далее, в зависимости от HTTP-метода, который обработчик должен обрабатывать:

use actix_web::{web, App, HttpResponse, HttpServer, Responder};  async fn greet(req: web::HttpRequest) -> impl Responder {     let name = req.match_info().get("name").unwrap_or("World");     HttpResponse::Ok().body(format!("Hello, {}!", name)) }

Мидлвари в Actix Web могут модифицировать запросы перед тем, как они достигнут обработчика, или изменять ответы после того, как обработчик их сгенерил, но до того, как они отправлены клиенту:

use actix_web::{middleware, App, HttpServer};  #[actix_web::main] async fn main() -> std::io::Result<()> {     HttpServer::new(|| {         App::new()             .wrap(middleware::Logger::default()) // мидлвари логирования             .service(/* обработчики */)     })     .bind("127.0.0.1:8080")?     .run()     .await }

Асинхронность в Actix

В Actix каждый актор исполняется асинхронно, имея возможность обрабатывать сообщения параллельно с другими акторами. Акторы могут отправлять, получать и обрабатывать сообщения асинхронно, используя async/await Rust:

use actix::prelude::*;  struct MyAsyncActor; impl Actor for MyAsyncActor {     type Context = Context<Self>; }  #[derive(Message)] #[rtype(result = "String")] struct MyMessage;  impl Handler<MyMessage> for MyAsyncActor {     type Result = ResponseFuture<String>;      fn handle(&mut self, _msg: MyMessage, _ctx: &mut Context<Self>) -> Self::Result {         Box::pin(async {             // какая-нибудь длительная асинхронная операция             actix_rt::time::sleep(std::time::Duration::from_secs(1)).await;             "Hello from async actor!".to_string()         })     } }

MyAsyncActor асинхронно обрабатывает сообщение MyMessage, имитируя длительную операцию с использованием sleep. Возвращаемый тип ResponseFuture<String> показывает то что все будет выполнено асинхронно.

С Actix можно асинхронного взаимодействовать между акторами через отправку сообщений. Акторы могут использовать метод .send() для асинхронной отправки сообщений другим акторам и ожидать ответа с помощью await:

#[actix_rt::main] async fn main() {     let addr = MyAsyncActor.start();      let res = addr.send(MyMessage).await;     match res {         Ok(result) => println!("Received response: {}", result),         Err(e) => println!("Encountered an error: {}", e),     } }

Отправляем сообщение MyMessage актору MyAsyncActor и асинхронно ожидаем ответ.

Actix поддерживает асинхронную обработку стримов:

use actix::prelude::*; use futures::stream::{self, StreamExt};  struct StreamActor; impl Actor for StreamActor {     type Context = Context<Self>; }  impl StreamHandler<i32> for StreamActor {     fn handle(&mut self, item: i32, _ctx: &mut Context<Self>) {         println!("Received: {}", item);     } }  #[actix_rt::main] async fn main() {     let addr = StreamActor.start();      // создание простого стрима     let stream = stream::iter(vec![1, 2, 3]);     // отправка стрима актору     addr.add_stream(stream); }

StreamActor подписывается на стрим целые числа и асинхронно обрабатывает каждое поступающее значение.

Прочие возможности

Создадим простейший веб-сервер, который будет отвечать на запросы по корневому пути (/):

use actix_web::{web, App, HttpResponse, HttpServer, Responder};  async fn greet() -> impl Responder {     HttpResponse::Ok().body("Hello, Habr!") }  #[actix_web::main] async fn main() -> std::io::Result<()> {     HttpServer::new(|| {         App::new()             .route("/", web::get().to(greet))     })     .bind("127.0.0.1:8080")?     .run()     .await }

Создаем HTTP-сервер, который слушает на 127.0.0.1:8080 и использует функцию greet для обработки GET-запросов по адресу /. Функция greet возвращает простое приветствие.

Создадим обработчик, который принимает имя пользователя через путь запроса и возвращает персонализированное приветствие:

use actix_web::{web, App, HttpResponse, HttpServer, Responder};  async fn greet_user(name: web::Path<String>) -> impl Responder {     HttpResponse::Ok().body(format!("Zdarova, {}!", name)) }  #[actix_web::main] async fn main() -> std::io::Result<()> {     HttpServer::new(|| {         App::new()             .route("/greet/{name}", web::get().to(greet_user))     })     .bind("127.0.0.1:8080")?     .run()     .await }

Используем web::Path для извлечения параметра name из пути URL и возвращаем пользователю персонализированное сообщение.

Для работы с базами данных в Actix-web часто юзает асинхронные ORM, например, Diesel или sqlx. Используемsqlx для асинхронного взаимодействия с бдшкой PostgreSQL:

use actix_web::{web, App, HttpResponse, HttpServer, Responder}; use sqlx::postgres::PgPoolOptions; use std::env;  async fn get_user(pool: web::Data<sqlx::PgPool>) -> impl Responder {     let row: (String,) = sqlx::query_as("SELECT name FROM users WHERE id = $1")         .bind(1)         .fetch_one(&**pool)         .await         .unwrap();      HttpResponse::Ok().body(format!("Hello, {}!", row.0)) }  #[actix_web::main] async fn main() -> std::io::Result<()> {     let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");     let pool = PgPoolOptions::new()         .connect(&database_url)         .await         .expect("Failed to create pool.");      HttpServer::new(move || {         App::new()             .app_data(web::Data::new(pool.clone()))             .route("/user", web::get().to(get_user))     })     .bind("127.0.0.1:8080")?     .run()     .await }

Создался пул соединений к бд PostgreSQL и он используется для его для асинхронного выполнения запроса к бд в обработчике get_user.

Можно интегрироваться с API. Например с reqwest, можно асинхронно отправлять HTTP-запросы к внешним сервисам:

use actix_web::{web, App, HttpResponse, HttpServer, Responder}; use reqwest::Client;  async fn fetch_data() -> impl Responder {     let client = Client::new();     let res = client.get("https://api.example.com/data")         .send()         .await         .unwrap()         .text()         .await         .unwrap();      HttpResponse::Ok().body(res) }  #[actix_web::main] async fn main() -> std::io::Result<()> {     HttpServer::new(|| {         App::new()             .route("/fetch", web::get().to(fetch_data))     })     .bind("127.0.0.1:8080")?     .run()     .await }

Функция fetch_data использует reqwest для асинхронного получения данных от внешнего API и возвращает полученный ответ клиенту.


Для тех, кто желает узнать больше — Actix GitHub.

А про практические инструменты и другие ЯП лучше всего узнавать от практикующих экспертов, например, в рамках онлайн-курсов.


ссылка на оригинал статьи https://habr.com/ru/articles/795565/


Комментарии

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

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