← Оглавление

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 - точка, где функция говорит: “я жду, займись пока чем-то другим”.

Три правила для использования:

Понимание того, что происходит внутри - Future, Poll, wakers - отдельная наука. Пользоваться можно уже сейчас.