Найти тему
Ржавый код

Начало в Rust с подробного изучения очень простого веб-API Axum

Оглавление

Здесь мы рассмотрим концепции в том виде, в каком они возникают в контексте простого веб-API Axum. Цель этой статьи не в том, чтобы научить вас всему Rust, а в том, чтобы вы начали работать и действительно создали что-то реальное с использованием Rust.

Поскольку цель здесь состоит в том, чтобы дать вам возможность самостоятельно решать будущие проблемы, эта статья может показаться немного отсталой. Сначала мы рассмотрим некоторый код, затем подробно рассмотрим код представленный в остальной части статьи. Это отличается от обычного подхода "сделай это, затем это" в большинстве руководств. Что больше похоже на вождение с помощью GPS, конечно, вы доберетесь туда, но помните ли вы как? Если формат не работает, пожалуйста, выделите проблемы в комментариях, и я внесу коррективы.

Если вы хотите запустить код, вам понадобится программа установки rust toolchain.

Что такое Аксум?

Axum - это платформа веб-приложений, в которой приоритет отдается простоте использования и модульности. Он предлагает такие функции, как маршрутизация запросов обработчикам, декларативный синтаксический анализ запросов, предсказуемая обработка ошибок и генерация ответов с минимальными усилиями. Axum построен поверх hyper с очень низкими накладными расходами, что приводит к низкому потреблению памяти и высокой производительности. Вся реализация выполнена в безопасном Rust.

Cargo new

В корневой папке нашего проекта мы создаем новый проект rust, используя cargo следующим образом: cargo new backend.
Это дает нам стандартный `src/main.rs`, который на момент написания выглядит следующим образом:

-2

main функция - это место, где запускаются приложения rust. Программа выводит ”Hello, world!” на стандартный вывод (консоль) и затем успешно завершает работу. Не очень впечатляет, но мы знаем, с чего начинается приложение, и мы должны написать наш код.

Зависимости

Другой файл, который генерируется, - это cargo.toml, который содержит нашу конфигурацию сборки. В этой серии мы будем работать только с разделом зависимостей, остальное пока игнорируем.

Библиотеки в rust называются crates, чтобы использовать crates в нашем приложении, мы добавляем их в качестве зависимостей в наш файл cargo.toml. Это пакеты, от которых мы зависим в этой статье.

-3

axum - это веб-фреймворк, который мы используем. Мы добавили его в зависимости, используя сокращенный формат, для которого нужна только версия.

tokio - самая популярная асинхронная среда выполнения, она также создана той же командой, что и axum. Мы используем другой формат для включения tokio, чем в axum. Причина в том, что нам нужно указать, какие функции мы хотим включить, а сокращенный формат этого не поддерживает. Хотя название и версия говорят сами за себя, функции могут быть более расплывчатыми. Они позволяют авторам пакетов включать код, который понадобится только подмножеству пользователей, не оказывая негативного влияния на тех, кому он не нужен. Если вы опустите функции, вы просто получите функции модуля по умолчанию. Любая добавленная вами функция будет добавлена поверх функций по умолчанию. И вы можете, если захотите, отказаться от функций по умолчанию.

serde - это расшифровывается как сериализация и десериализация. В этой статье мы будем только сериализовать / десериализовать в/из JSON, но если вам понадобится какой-либо другой формат в будущем, велика вероятность, что есть пакет, позволяющий заставить его работать с serde. Мы добавляем функцию derive, подробнее о derive позже.

Вот и все, теперь вы очень мало знаете о файле cargo.toml, но вы знаете достаточно для создания web.

Зачем нам нужна асинхронная среда выполнения?

В отличие от других популярных языков, Rust не поставляется со своей собственной асинхронной средой выполнения, только с конструкциями, которые делают его удобным для разработчиков. А пока просто думайте: tokio делает асинхронную работу в нашей программе.

Дополнительное замечание об асинхронном rust: Если вы работали с promises в javascript, вы, возможно, знаете, что они начинают выполняться сразу после их создания. В Rust это не так. Выполнение не начнется, если что-то активно не управляет им, что обычно происходит с помощью вызова await. А фактическое управление осуществляется асинхронной средой выполнения, в нашем случае tokio.

Приложение

Мы заменяем все в `src/main.rs` на это:

-4

Не имея особых знаний о Rust, вы, вероятно, уже знаете, что происходит, когда мы запускаем этот код. Чтобы быть уверенным, что все находятся на одной странице, я все равно напишу это здесь: запуск `cargo run` в папке `backend` запустит веб-сервер на порту 3030, который ответит `{“message”: ”Привет, мир!”}`, если мы выполним `GET` запрос по маршруту `/`.

Мы можем пока пропустить инструкции по использованию и сосредоточиться на нашей основной функции. Как и в прошлый раз, выполнение начинается в main, но на этот раз перед ним стоит async. Следовательно, это асинхронная функция, и мы можем использовать await внутри нее при вызове других асинхронных функций.

-5

Строка `#[tokio::main]` над новой основной функцией очень важна, это макрос, и он обернет нашу асинхронную основную функцию внутри обычной основной функции, подключит многопоточную асинхронную среду выполнения и запустит ее для нас. Вся эта перетасовка кода происходит во время компиляции. Если мы удалим макрос, мы получим сообщение о том, что функции `main` не разрешено быть асинхронной. В Интернете есть частично написанная книга об асинхронном Rust, не стесняйтесь изучать ее в будущем. Но не сейчас, мы только наполовину прочитали эту статью, давайте двигаться дальше.

-6

Это утверждение, которое подключает наш первый маршрутизатор, выглядит очень простым, но в нем содержится больше информации, чем кажется на первый взгляд. Во-первых, мы используем функцию `new()`, которая выглядит как конструктор на других языках, но в Rust нет конструкторов, использование `new` в качестве имени функции, которая создает структуру, является просто условностью.

Затем у нас есть вызов для маршрутизации. Возможно, вы слышали о концепции владения, это владение в действии, поскольку функция `route` принимает изменяемое право собственности на вновь созданную структуру и возвращает ее после добавления маршрута и обработчика метода. Но, как вы можете видеть, система заимствования всегда существует, чтобы защитить нас от ошибок, большую часть времени она невидима, и нам не нужно об этом думать.

Если бы мы переписали приведенное выше утверждение так, чтобы оно состояло из двух строк, подобных этой:

-7

Вы больше не сможете использовать `app` после вызова `route`, поскольку `route` стал владельцем приложения и перенес его в свою функцию. Затем, в конце своего выполнения, он вернул его, но мы не захватили его, поэтому он был удален и больше не используется.

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

-8

Это выглядит не очень красиво, но теперь мы снова можем использовать приложение. Здесь вы также видите, что в rust вам разрешено повторно использовать имена переменных, поэтому нет необходимости указывать такие имена переменных, как `resultStr`, а затем `resultObj` после того, как что-то было проанализировано. Это может быть `result`, а затем снова `result`, даже если тип теперь другой.

Давайте теперь вернемся к нашему исходному обработчику маршрута и другим ссылочным строкам в том же блоке кода:

-9

Давайте сосредоточимся на части get(обработчик) этого кода. get - это функция, которой у нас нет в нашем файле, она находится в другом модуле, внутри модуля Axum. Нам не нужно вводить полный путь к модулю, благодаря инструкции use, которая есть у нас в начале файла. Каждый `::` в инструкции use - это граница пути, это то, как вы детализируете дочерние модули. И `{ Something, SomethingElse }` используется для добавления функционала из того же модуля. Я редко набираю их сам, `rust analyzer` справляется с этим очень хорошо, когда вы выбираете что-то из автозаполнения, он также добавляет инструкцию use. Rust analyzer - это языковой сервис Rust, который обеспечивает поддержку rust в большинстве редакторов, включая VS Code, который я использую.

Функция get принимает аргумент, который является другой функцией, что показывает, что вы также можете отправлять ссылки на другие функции в Rust.
Наша функция обработчика выглядит простой, но она тоже содержит много информации.

-10

Одна вещь, которую вы могли бы заметить, это то, что в ней нет символа `;` после закрывающих круглых скобок структуры Json. Опуская `;`, она становится оператором return. Тот же код мог бы быть написан менее идиоматичным способом, подобным этому:

-11

Символ `-> Json<Message>` обозначает, что функция возвращает структуру Json, которая является универсальной, и что общим типом является `Message`. Для нас это означает, что это ответ в формате JSON, содержащий сообщение.

Создание экземпляра структуры в rust очень простое, нет необходимости в каких-либо новых ключевых словах. Просто напиши, что ты хочешь. Здесь мы создаем новую структуру Json вместе с ее внутренним сообщением за один раз:

-12

`Json(Message)` выглядит как вызов метода, но это не так. Это структура кортежа с одним значением. Его определение выглядит следующим образом.

-13

Думайте об этом как о структуре с одним неназванным полем, доступ к которому вы можете получить через `.0`. И значение внутри структуры может быть любого типа. Однако не обманывайтесь, даже если структура `Json<T>` принимает любой тип в качестве своего единственного параметра. Обработчик должен вернуть что-то, что может быть превращено в ответ. И если мы посмотрим на реализацию `IntoResponse` для типа `Json<T>`, мы обнаружим это:

-14

`where T: Serialize` показывает, что существует требование, чтобы `T` реализовывал признак `Serialize` из serde. И `T` в нашем случае - это сообщение.

К счастью, нашу структуру сообщения можно использовать внутри Json в качестве ответа, потому что мы реализовали сериализацию с использованием макроса `derive`:

-15

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

-16

Серверу необходимо прослушивать запросы, поэтому нам нужно создать адрес сокета. Мы создаем адрес нашего сокета, используя кортеж из двух значений `([127, 0, 0, 1], 3030)`, где первое значение представляет собой фрагмент из 4 `u8` `([127, 0, 0, 1])`, а второй - `u16` `(3030)`. `u` в `u8` и `u16` обозначает целое число без знака (только положительные числа), а число представляет длину / размер в битах (8 бит и 16 бит). Многие языки требуют, чтобы вы указывали размер числовых типов, но если вы используете javascript, то это что-то новое для вас.

Создав адрес сокета, мы готовы запустить наш сервер.

-17

Перед запуском сервера мы выводим на консоль, что мы запустили сервер, используя `println!` макрос. В этой `println!` мы используем новый формат. Поскольку новый формат был недавно добавлен, мы также должны взглянуть на старый формат, чтобы убедиться, что вы узнаете его, когда увидите.

Вот два формата рядом друг с другом, эти две строки дадут один и тот же результат:

-18

Что касается инструкции, фактически запускающей сервер. Мы начинаем с отправки неизменяемой ссылки на адрес нашего сокета в функцию `bind`. Это был первый раз, когда мы использовали неизменяемую ссылку, которая в `rust speak` называется заимствованием.

-19

Затем `bind` возвращает конструктор, в котором мы регистрируем наше приложение/маршрутизатор, отправляя производную службу для обслуживания.

-20

Мы еще не совсем закончили с этим. Нам нужно действительно запустить наш сервер, помните, что мы говорили, что асинхронность в rust не похожа на другие языки. В rust ничего не происходит, если этим что-то не управляет. Чтобы заставить его ”двигаться”, нам нужно вызвать await. Если мы не будем использовать await, приложение никогда не запустит сервер, оно просто завершит работу, как основная функция, которую мы получили от `cargo new`.

-21

Последнее, что мы делаем в нашей программе, - это вызываем expect. Это не требуется, код все равно будет скомпилирован с предупреждением о неиспользуемом "результате", который необходимо использовать, если мы его опустим. Мы получаем это предупреждение, поскольку ожидание в данном случае возвращает результат, который был отмечен атрибутом `must_use`, что заставляет компилятор запускать это предупреждение. В нашем случае мы ожидаем, что это никогда не подведет. Поэтому мы вызываем expect и выдаем ему сообщение для отображения в случае сбоя сервера. Вы также могли бы использовать здесь `unwrap` или обработать это в инструкции `match`, но в данном случае, я чувствую, что `expect` - правильный выбор. Это лаконично, и если это не удастся, мы получим сообщение, которое легко поймем.

Как вы заметили, даже этот короткий фрагмент кода Rust содержал множество лакомых кусочков для изучения.

Статья на list-site.