Введение в Iron

• Арсен Галимов aka «Loo Maclin» • обучение • поддержите на Patreon

Немного об Iron

Iron — это высокоуровневый веб-фреймворк, написанный на языке программирования Rust и построенный на базе другой небезызвестной библиотеки hyper. Iron разработан таким образом, чтобы пользоваться всеми преимуществами, которые нам предоставляет Rust. Iron старается избегать блокирующих операций в своём ядре.

Философия

Iron построен на принципе расширяемости настолько, насколько это возможно. Он вводит понятия для расширения собственного функционала:

С базовой частью модификаторов и промежуточных типажей вы познакомитесь в ходе статьи.

Создание проекта

Для начала создадим проект с помощью Cargo, используя команду:

1
cargo new rust-iron-tutorial --bin

Далее добавим в раздел [dependencies] файла Cargo.toml зависимость iron = "0.4.0".

Пишем первую программу с использованием Iron

Напишем первую простенькую программу на Rust с использованием Iron, которая на любые запросы по порту 3000 будет отвечать текстом «Hello rustycrate!».

extern crate iron;
use iron::prelude::*;
use iron::status;
fn main() {
   Iron::new(|_: &mut Request| {
       Ok(Response::with((status::Ok, "Hello rustycrate!\n")))
   }).http("localhost:3000").unwrap();
}

Запустите код при помощи команды cargo run и после того, как компиляция завершится и программа запустится, протестируйте сервис, например, при помощи curl:

1
2
[loomaclin@loomaclin ~]$ curl localhost:3000
Hello rustycrate!

Давайте разберём программу, чтобы понимать, что тут происходит. В первой строке программы импортируется пакет iron. Во второй строке был подключён модуль-прелюдия, содержащий набор наиболее важных типажей, таких как Request, Response, IronRequest, IronResult, IronError и Iron. В третьей строке подключается модуль status, содержащий списки кодов для ответов на запросы. Iron::new создаёт новый экземпляр Iron’а, который, в свою очередь, является базовым объектом вашего сервера. Он принимает параметром объект, реализующий типаж Handler. В нашем случае мы передаём замыкание, аргументом которого является изменяемая ссылка на переданный запрос.

Указываем mime-type в заголовке ответа

Чаще всего при построении веб-сервисов (SOAP, REST) требуется отсылать ответы с указанием типа контента, который они содержат. Для этого в Iron предусмотрены специальные средства.

Выполним следующее.

Подключаем соответствующую структуру:

use iron::mime::Mime;

Связываем имя content_type, которое будет хранить разобранное при помощи подключённого типажа Mime значение типа:

let content_type = "application/json".parse::<Mime>().unwrap();

Модифицируем строку ответа на запрос следующим образом:

Ok(Response::with((content_type, status::Ok, "{}")))

Запускаем программу и проверяем работоспособность:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[loomaclin@loomaclin ~]$ curl -v localhost:3000
* Rebuilt URL to: localhost:3000/
*   Trying ::1...
* Connected to localhost (::1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.49.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Date: Tue, 12 Jul 2016 19:53:21 GMT
< Content-Length: 2
<
* Connection #0 to host localhost left intact
{}

Управление статус-кодами ответов

В перечислении StatusCode, расположенном в модуле status, располагаются всевозможные статус-коды. Давайте воспользуемся этим и вернём «клиенту» ошибку 404 — NotFound, изменив строку с формированием ответа на запрос:

Ok(Response::with((content_type, status::NotFound)))

Проверка:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[loomaclin@loomaclin ~]$ curl -v localhost:3000
* Rebuilt URL to: localhost:3000/
*   Trying ::1...
* Connected to localhost (::1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.49.1
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Content-Length: 2
< Content-Type: application/json
< Date: Tue, 12 Jul 2016 20:55:40 GMT
<
* Connection #0 to host localhost left intact

Примечание: по сути, весь модуль status является обёрткой для соответствующих перечислений в библиотеке hyper, на которой базируется iron.

Перенаправление запросов

Для редиректа в iron используется структура Redirect из модуля modifiers (не путать с modifier). Она состоит из URL цели, куда необходимо будет произвести перенаправление. Попробуем её применить, проделав следующие изменения:

Подключаем структуру Redirect:

use iron::modifiers::Redirect;

К подключению модуля status добавляем подключение модуля Url:

use iron::{Url, status};

Связываем имя url, которое будет хранить разобранное значение адреса редиректа:

let url = Url::parse("https://rustycrate.ru/").unwrap();

Меняем блок инициализации Iron следующим образом:

   Iron::new(move |_: &mut Request | {
       Ok(Response::with((status::Found, Redirect(url.clone()))))
   }).http("localhost:3000").unwrap();

Проверяем результат:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[loomaclin@loomaclin ~]$ curl -v localhost:3000
* Rebuilt URL to: localhost:3000/
*   Trying ::1...
* Connected to localhost (::1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.49.1
> Accept: */*
>
< HTTP/1.1 302 Found
< Location: https://rustycrate.ru/
< Date: Tue, 12 Jul 2016 21:39:24 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact

Также вы можете воспользоваться ещё одной структурой RedirectRaw из модуля modifiers, для конструирования которой требуется лишь строка.

Работа с типом http-запроса

У структуры Request есть поле method, позволяющее определять тип пришедшего http-запроса. Напишем сервис, который будет сохранять в файл данные, переданные в теле запроса с типом Put, считывать данные из файла и передавать их в ответе на запрос с типом Get.

Аннотируем импортируемый контейнер iron атрибутом macro_use, чтобы в дальнейшем использовать макросы iexpect и itry для обработки ошибочных ситуаций:

#[macro_use]
extern crate iron;

Подключаем модули для работы с файловой системой и вводом/выводом из стандартной библиотеки:

use std::io;
use std::fs;

Подключаем модуль method, содержащий список типов http-запросов:

use iron::method;

Меняем блок инициализации Iron таким образом, чтобы связать полученный запрос с именем req:

Iron::new(|req: &mut Request| {
   ...
   ...
   ...
}.http("localhost:3000").unwrap();

Добавляем в обработку запроса сопоставление с образцом поля method для двух типов запросов Get и Put, а для остальных будем использовать ответ в виде статус-кода BadRequest:

       Ok(match req.method {
           method::Get => {
               let f = iexpect!(fs::File::open("foo.txt").ok(), (status::Ok, ""));
               Response::with((status::Ok, f))
           },
           method::Put => {
               let mut f = itry!(fs::File::create("foo.txt"));
               itry!(io::copy(&mut req.body, &mut f));
               Response::with(status::Created)
           },
           _ => Response::with(status::BadRequest)
       }

В Iron макрос iexcept используется для разворачивания переданного в него объекта типа Option и в случае, если Option содержит None макрос возвращает Ok(Response::new()) с модификатором по умолчанию status::BadRequest. Макрос itry используется для оборачивания ошибки в IronError.

Пробуем запустить и проверить работоспособность.

PUT:

1
2
3
[loomaclin@loomaclin ~]$ curl -X PUT -d my_file_content localhost:3000
[loomaclin@loomaclin ~]$ cat ~/IdeaProjects/cycle/foo.txt
my_file_content

GET:

1
2
[loomaclin@loomaclin ~]$ curl localhost:3000
my_file_content

POST:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[loomaclin@loomaclin ~]$ curl -X POST -v localhost:3000
* Rebuilt URL to: localhost:3000/
*   Trying ::1...
* Connected to localhost (::1) port 3000 (#0)
> POST / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.49.1
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Content-Length: 0
< Date: Tue, 12 Jul 2016 22:29:58 GMT
<
* Connection #0 to host localhost left intact

Реализация сквозного функционала при помощи пре- и пост-процессинга

В iron также присутствуют типажи BeforeMiddleware,AfterMiddleware и AroundMiddleware для сквозного функционала, позволяющие реализовывать логику по обработке запроса до того, как она началась в основном обработчике, и после того, как она там завершилась.

Напишем пример использования AOP’о подобных указанных типажей:

extern crate iron;

use iron::prelude::*;
use iron::{BeforeMiddleware, AfterMiddleware, AroundMiddleware, Handler};

struct SampleStruct;
struct SampleStructAroundHandler<H:Handler> { logger: SampleStruct, handler: H}

impl BeforeMiddleware for SampleStruct {
   fn before(&self, req: &mut Request) -> IronResult<()> {
       println!("До обработки запроса.");
       Ok(())
   }
}

impl AfterMiddleware for SampleStruct {
   fn after(&self, req: &mut Request, res: Response) -> IronResult<Response> {
       println!("После обработки запроса.");
       Ok(res)
   }
}

impl<H: Handler> Handler for SampleStructAroundHandler<H> {
   fn handle(&self, req: &mut Request) -> IronResult<Response> {
       println!("А я ещё один обработчик запроса.");
       let res = self.handler.handle(req);
       res
   }
}

impl AroundMiddleware for SampleStruct {
   fn around(self, handler: Box<Handler>) -> Box<Handler> {
       Box::new(SampleStructAroundHandler {
           logger: self,
           handler: handler
       }) as Box<Handler>
   }
}

fn sample_of_middlewares(_:&mut Request) -> IronResult<Response> {
   println!("В основном обработчике запроса.");
   Ok(Response::with((iron::status::Ok, "Привет, я ответ на запрос!")))
}

fn main() {
   let mut chain = Chain::new(sample_of_middlewares);
   chain.link_before(SampleStruct);
   chain.link_after(SampleStruct);
   chain.link_around(SampleStruct);
   Iron::new(chain).http("localhost:3000").unwrap();
}

В этом примере вводится структура SampleStruct, для которой реализуются типажи BeforeMiddleware с функцией before и AfterMiddleware с функцией after. С их помощью может быть реализована вся сквозная логика. Типаж AroundMiddleware используется совместно с типажом Handler для добавления дополнительного обработчика. Добавление всех реализованных обработчиков в жизненный цикл обработки запроса производится с помощью специального типажа Chain, позволяющего формировать цепь вызовов пре- и пост-обработчиков.

Протестируем программу. В консоли:

1
2
[loomaclin@loomaclin ~]$ curl localhost:3000
Привет, я ответ на запрос!

В выводе программы:

1
2
3
4
5
   Running `target/debug/cycle`
До обработки запроса.
А я ещё один обработчик запроса.
В основном обработчике запроса.
После обработки запроса.

Роутинг

Какое серверное API может обойтись без роутинга? Добавим его =) Модифицируем наш базовый пример из начала статьи следующим образом.

Подключаем коллекцию из стандартной библиотеки:

use std::collections:HashMap;

Объявим структуру для хранения коллекции вида «путь — обработчик» и опишем для этой структуры конструктор, который будет производить инициализацию этой коллекции, и функцию для добавления в коллекцию новых роутов с их обработчиками:

struct Router {

   routes: HashMap<String, Box<Handler>>

}

impl Router {

   fn new() -> Self {

       Router {
           routes: HashMap::new()
       }
   }

   fn add_route<H>(&mut self, path: String, handler: H) where H: Handler {
       self.routes.insert(path, Box::new(handler));
   }
}

Для использования нашей структуры в связке с Iron необходимо реализовать для неё типаж Handler с функцией handle:

impl Handler for Router {
fn handle(&self, req: &mut Request) -> IronResult<Response> {
match self.routes.get(&req.url.path().join("/")) {
Some(handler) => handler.handle(req),
None => Ok(Response::with(status::NotFound))
       }
   }
}

В функции handle мы по переданному в запросе пути находим соответствующий обработчик в коллекции и вызываем обработчик этого пути с передачей в него запроса. В случае если переданный в запросе путь не «зарегистрирован» в коллекции — возвращается ответ с кодом ошибки NotFound.

Последнее, что осталось реализовать, — это инициализация нашего роутера и регистрация в нём необходимых нам путей с их обработчиками:

fn main() {
   let mut router = Router::new();
   router.add_route("hello_rustycrate".to_string(), |_: &mut Request| {
   Ok(Response::with((status::Ok, "Hello Loo Maclin!\n")))
   });
   router.add_route("hello_rustycrate/again".to_string(), |_: &mut Request| {
   Ok(Response::with((status::Ok, "Ты повторяешься!\n")))
   });
   router.add_route("error".to_string(), |_: &mut Request| {
   Ok(Response::with(status::BadRequest))
});
...

Добавление новых путей происходит путём вызова реализованной выше функции. Инициализируем экземпляр Iron с использованием нашего роутера:

Iron::new(router).http("localhost:3000").unwrap();

Тестируем:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[loomaclin@loomaclin ~]$ curl localhost:3000/hello_rustycrate
Hello Loo Maclin!
[loomaclin@loomaclin ~]$ curl localhost:3000/hello_rustycrate/again
Ты повторяешься!
[loomaclin@loomaclin ~]$ curl -v localhost:3000/error
*   Trying ::1...
* Connected to localhost (::1) port 3000 (#0)
> GET /error HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.49.1
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Date: Wed, 13 Jul 2016 21:29:20 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact

Заключение

На этом статья подходит к концу. Помимо рассмотренного функционала, Iron выносит большую часть типичной для веб-фреймворка функциональности по базовым расширениям:

Статья предназначена для базового ознакомления с Iron, и хочется надеяться, что она справляется с этой целью.

Спасибо за внимание!