6.2 Порт в мире
Разница между “слушает” и “отвечает” примерно такая же, как между “дверь открыта” и “кто-нибудь дома”. Первое - факт. Второе - вопрос. Приходящий с делом хочет знать ответ на второй, но проверяет первый. Порт открыт.
Что нужно
tq-cli отвечает на команды. tq-api будет отвечать на HTTP-запросы - от любого клиента,
который знает адрес. Оба входа используют одно хранилище в tq-core. В этой главе появляется
сам вход: маршрут, адрес, сервер.
Для HTTP-сервера выбран axum - современный Rust-фреймворк, построенный на tokio. tokio
управляет задачами, которые ждут сети, не блокируя поток. Это называется async.
Минимум, который нужно знать прямо сейчас: async fn - функция, которая умеет ждать;
.await - подождать здесь; #[tokio::main] - чтобы async fn main() и .await в ней работали. Всё остальное
tokio берёт на себя.
Сборка
Workspace получает третий крейт. В корневом Cargo.toml - один новый элемент:
# Cargo.toml - CHANGED
[workspace]
members = ["crates/core", "crates/cli", "crates/api"]
resolver = "2"
Создайте директорию:
$ mkdir -p crates/api/src
crates/api/Cargo.toml - бинарный крейт с двумя зависимостями:
# crates/api/Cargo.toml - NEW
[package]
name = "tq-api"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "tq-api"
path = "src/main.rs"
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
features = ["full"] подключает весь функционал из tokio. Точнее настроить можно
позже; сейчас важно не отвлекаться на детали.
Сервер - в crates/api/src/main.rs:
// crates/api/src/main.rs - NEW
use axum::{routing::get, Router};
async fn health() -> String {
"tq ok".to_string()
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(health));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Router::new().route("/", get(health)) - маршрут: GET-запрос на / передаётся функции health.
TcpListener::bind("0.0.0.0:3000") сообщает ОС: входящие запросы на порт 3000 - для нас.
Порт - число, по которому клиент находит нужный сервис на машине; 3000 - просто наш выбор.
axum::serve(listener, app) запускает цикл обработки запросов. Без .await он бы не начался;
с ним - работает, пока не прервать.
Добавьте в Makefile новую цель:
## serve: start the HTTP server
.PHONY: serve
serve:
@cargo run -p tq-api
Результат
Запустите сервер в одном терминале:
$ make serve
Проверить работу можно двумя способами: откройте http://localhost:3000/ в браузере -
страница покажет tq ok. Или в другом терминале:
$ curl localhost:3000/
tq ok
Порт открыт. Запрос пришёл, обработчик ответил, соединение закрылось - сервер остался
ждать следующего. Чтобы остановить его, нажмите Ctrl+C в терминале или закройте окно.
make ci проходит: cargo build собирает все три крейта, тесты - те же 38.
Полный код
tqдля этой главы -6-the-port/02-a-port-in-the-world/.
Lore: Async в двух словах
Обычная функция возвращает значение или блокирует поток до завершения. Сервер, который блокирует поток на каждом запросе, обрабатывает их по одному - при любой нагрузке это неприемлемо.
async fn возвращает future - обещание результата. Tokio держит пул потоков и решает,
какую future продвигать в каждый момент. .await - точка, где функция говорит: “я жду,
займись пока чем-то другим”.
Три правила для использования:
async fn foo()- функция, которая умеет ждатьfoo().await- подождать результата#[tokio::main]- чтобыasync fn main()и.awaitв ней работали
Понимание того, что происходит внутри - Future, Poll, wakers - отдельная наука. Пользоваться можно уже сейчас.