golang

HTTP/2 / HTTP/3 и gRPC на Rust: пишем учебный Mini-Transport

  • пятница, 2 мая 2025 г. в 00:00:07
https://habr.com/ru/articles/906324/
Rust GO!
Rust GO!

Обновлено: пример полностью собирается на stable Rust (edition 2024) с актуальными версиями крейтов: bytes, anyhow, tokioquinn,rcgen иrustls.

Что сделаем

  1. Разберёмся, как фреймируются HTTP/2 и HTTP/3 (QUIC).

  2. Напишем крошечный мини-фреймворк «Mini-Transport» (≈600 строк) на Rust:
    • чтение/запись HTTP/2-фреймов,
    • gRPC-кодек (без protobuf-codegen),
    • переход на QUIC.

  3. Соберём рабочий echo-пример: клиент шлёт «hello», сервер отвечает «world».

1 | Базовая теория

  • HTTP/2 — бинарный протокол, каждый фрейм = 9-байт заголовок + payload; мультиплексирование по stream ID, HPACK-сжатие заголовков.

  • QUIC / HTTP/3 — тот же набор фреймов, но поверх UDP + TLS 1.3; нет head-of-line blocking.

  • gRPC поверх HTTP/2: каждое сообщение = 1 байт flags (0 — без сжатия) + 4 байта длины + bytes.

2 | Структура проекта

mini-transport/
├─ Cargo.toml              # зависимости
├─ src/
│  ├─ lib.rs               # public mod tree
│  ├─ h2/
│  │  ├─ mod.rs            # pub mod frame|parser|sender
│  │  ├─ frame.rs          # enum Frame + (de)serialize
│  │  ├─ parser.rs         # read_frames()
│  │  └─ sender.rs         # send_frame()
│  ├─ grpc/
│  │  ├─ mod.rs            # codec & service
│  │  ├─ codec.rs          # 5‑byte gRPC wrapper
│  │  └─ service.rs        # handle_echo()
│  └─ h3/
│     ├─ mod.rs            # QUIC wrapper
│     └─ quic.rs           # start_quic_server()
└─ examples/
   ├─ server.rs            # HTTP/2 echo‑сервер
   └─ client.rs            # HTTP/2 echo‑клиент

Cargo.toml

[package]
name = "mini-transport"
version = "0.1.0"
edition = "2024"

[dependencies]
bytes = "1"
anyhow = "1"
tokio = { version = "1", features = ["full"] }
quinn = { version = "0.11.7", features = ["rustls"] }
rcgen = "0.13.2"
rustls = "0.23"

3 | Ключевые модули

3.1 h2/frame.rs (сокращённо)

  use bytes::{Buf, BufMut, Bytes, BytesMut};

#[derive(Debug, Clone)]
pub enum Frame {
    Data { stream_id: u32, end_stream: bool, payload: Bytes },
    Headers { stream_id: u32, end_stream: bool, header_block: Bytes },
    Settings(Vec<(u16, u32)>),
}
// encode()/decode() реализуют RFC 7540 §4.1

3.2 grpc/codec.rs

use bytes::{Buf, BufMut, Bytes, BytesMut};

pub fn encode_message(msg: &[u8], dst: &mut BytesMut) {
    dst.put_u8(0);                     // flags
    dst.put_u32(msg.len() as u32);     // length
    dst.extend_from_slice(msg);
}

pub fn decode_message(src: &mut BytesMut) -> Option<Bytes> {
    if src.len() < 5 { return None; }
    let len = (&src[1..5]).get_u32();
    if src.len() < 5 + len as usize { return None; }
    src.advance(5);
    Some(src.split_to(len as usize).freeze())
}

3.3 h3/quic.rs (сокращённо)

pub async fn start_quic_server<A: ToSocketAddrs>(addr: SocketAddr) -> Result<()> {
    // ── 1. Генерируем самоподписанный сертификат ───────────────
    let cert = generate_simple_self_signed(vec!["localhost".into()])?;
    let cert_der = CertificateDer::from(cert.cert); // Прямо используем Certificate из rcgen
    let key_der = PrivateKeyDer::Pkcs8(cert.key_pair.serialize_der().into()); // Используем PKCS#8

    // ── 2. Собираем конфиг QUIC-сервера (rustls внутри) ─────
    let mut server_config = ServerConfig::with_single_cert(
        vec![cert_der], // Передаем вектор CertificateDer
        key_der,        // Передаем PrivateKeyDer
    )?;
    server_config.transport = Arc::new(quinn::TransportConfig::default());

    // ── 3. Создаем QUIC-эндпоинт ───────────────────────────
    let endpoint = Endpoint::server(server_config, addr)?;

    // ── 4. Обрабатываем входящие подключения ───────────────
    while let Some(conn) = endpoint.accept().await {
        tokio::spawn(async move {
            if let Ok(new_conn) = conn.await {
                while let Ok((mut send, mut recv)) = new_conn.accept_bi().await {
                    let mut data = Vec::new();
                    while let Some(chunk) = recv.read_chunk(usize::MAX, true).await.unwrap() {
                        data.extend_from_slice(&chunk.bytes);
                    }
                    send.write_all(&data).await.unwrap();
                }
            }
        });
    }
    endpoint.wait_idle().await;
    Ok(())
}

Полный листинг в репозитории (см. ниже).

4 | Полные примеры echo

examples/server.rs

//! Запускает упрощенный HTTP/2 echo-сервер на 127.0.0.1:50052
use anyhow::Result;
use bytes::BytesMut;
use mini_transport::{grpc::codec::encode_message, h2::{frame::Frame, sender::send_frame, parser::read_frames}};
use tokio::{net::TcpListener, time::Duration};

#[tokio::main]
async fn main() -> Result<()> {
    let listener = TcpListener::bind("127.0.0.1:50052").await?;
    println!("Server listening on 127.0.0.1:50052");

    loop {
        let (mut stream, addr) = listener.accept().await?;
        println!("New connection from {}", addr);
        // Отправляем HTTP/2 preface
        stream.writable().await?;
        if let Err(e) = stream.try_write(b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n") {
            eprintln!("Failed to send HTTP/2 preface to {}: {}", addr, e);
            continue;
        }
        println!("Sent HTTP/2 preface to {}", addr);

        // Создаем новую задачу для обработки соединения
        tokio::spawn(async move {
            // Даем клиенту время отправить данные
            tokio::time::sleep(Duration::from_millis(3000)).await;
            println!("Attempting to read Data frame from {}", addr);
            match read_frames(&mut stream).await {
                Ok(Frame::Data { payload, .. }) => {
                    println!("Received: {} from {}", String::from_utf8_lossy(&payload), addr);
                    // Отвечаем сообщением "world"
                    let mut response = BytesMut::with_capacity(32);
                    encode_message(b"world", &mut response);
                    let frame = Frame::Data {
                        stream_id: 1,
                        end_stream: true,
                        payload: response.freeze(),
                    };
                    if let Err(e) = send_frame(&mut stream, frame).await {
                        eprintln!("Failed to send response to {}: {}", addr, e);
                    } else {
                        println!("Sent 'world' to {}", addr);
                    }
                }
                Ok(_) => {
                    eprintln!("Received unexpected frame type from {}", addr);
                }
                Err(e) => {
                    eprintln!("Failed to read Data frame from {}: {}", addr, e);
                }
            }
        });
    }
}

examples/client.rs

//! Мини‑клиент: посылает "hello" и печатает ответ
use anyhow::Result;
use bytes::BytesMut;
use mini_transport::{grpc::codec::encode_message, h2::{frame::Frame, sender::send_frame, parser::read_frames}};
use tokio::{net::TcpStream, time::{timeout, Duration}};

#[tokio::main]
async fn main() -> Result<()> {
    let mut stream = TcpStream::connect("127.0.0.1:50052").await?;
    println!("Connected to server at 127.0.0.1:50052");

    // Отправляем HTTP/2 preface
    stream.writable().await?;
    if let Err(e) = stream.try_write(b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n") {
        eprintln!("Failed to send HTTP/2 preface: {}", e);
        return Ok(());
    }
    println!("Sent HTTP/2 preface");

    // Даем серверу время обработать preface
    tokio::time::sleep(Duration::from_millis(3000)).await;

    // Отправляем сообщение "hello"
    let mut payload = BytesMut::with_capacity(32);
    encode_message(b"hello", &mut payload);
    let data = Frame::Data { stream_id: 1, end_stream: true, payload: payload.freeze() };
    if let Err(e) = send_frame(&mut stream, data).await {
        eprintln!("Failed to send 'hello' to server: {}", e);
        return Ok(());
    }
    println!("Sent 'hello' to server");

    // Даем серверу время ответить
    tokio::time::sleep(Duration::from_millis(3000)).await;

    // Читаем ответ с тайм-аутом
    println!("Waiting for response...");
    match timeout(Duration::from_secs(20), read_frames(&mut stream)).await {
        Ok(Ok(Frame::Data { payload, .. })) => {
            println!("response: {}", String::from_utf8_lossy(&payload));
        }
        Ok(Ok(_)) => {
            eprintln!("Received unexpected frame type");
        }
        Ok(Err(e)) => {
            eprintln!("Failed to read response: {}", e);
        }
        Err(_) => {
            eprintln!("Timed out waiting for response");
        }
    }

    Ok(())
}

5 | Сборка и запуск

$ cargo run --example server   # терминал 1
$ cargo run --example client   # терминал 2
response: world

QUIC‑ветка (необязательно)

$ cargo run --example quic_server   # реализуйте сами вызов start_quic_server()

6 | Дальнейшие шаги

  • HPACK/QPACK, flow‑control WINDOW_UPDATE.

  • TLS + ALPN для HTTP/2.

  • Стриминговые gRPC‑методы и трейлеры.

Репозиторий: https://github.com/digkill/mini-transport — содержит весь код

Как это всё работает (версия «для совсем-совсем чайника»)

  1. Сервер запускается
    Он слушает порт 50052 и сразу говорит:
    «Эй, я умею HTTP/2!» (шлёт спец-строку-пролог клиенту).

  2. Клиент подключается
    Он в ответ кивает той же спец-строкой — мол, «понял, тоже HTTP/2».

  3. Клиент кладёт слово “hello” в конверт

    • сначала к слову приклеивается крошечная бирка gRPC:
      0 (флаг) + 5 (длина) + hello;

    • потом всё это завёртывается в больший конверт — DATA-фрейм HTTP/2;

    • конверт отправляется по проводам (TCP).

  4. Сервер ловит конверт, разворачивает

    • видит DATA-фрейм → достаёт из него gRPC бирку;

    • читает «hello».

  5. Сервер кладёт ответ “world” в новый конверт
    Тот же процесс, только вместо «hello» — «world».

  6. Клиент получает конверт
    Разворачивает — видит «world» и выводит в консоль.

Всё. Файлы client.rs и server.rs делают ровно эти 6 шагов — больше никакой магии.

Что почитать и посмотреть дальше — проверенные источники

Тема

Формат

Ссылки

HTTP/2

Спецификация

RFC 7540 — Hypertext Transfer Protocol Version 2

Учебник + демо-код

«HTTP/2 in Action» — Manning (гл. 1-6 читаются без Java)

HTTP/3 / QUIC

Спецификации

RFC 9000 (QUIC transport), RFC 9114 (HTTP/3)

Статья

Martin Thomson — “Quick QUIC intro” (mnot.net)

Видео

Google Chrome Dev Summit 2020 — “HTTP/3 explained”

gRPC протокол

Дизайн-док

grpc/grpc -› doc/PROTOCOL-HTTP2.md (GitHub)

Книга

Kasun Indrasiri — gRPC for Microservices in Action (гл. 2)

Rust & async

Официально

The Rust Async Book (rust-lang.org)

Курс

Tokio-rs.org“Building reliable systems” (HTTP chat server)

Крейты

Документация

tokio (runtime), bytes, quinn, h2, rustls, rcgen — все на docs.rs

Сниффинг трафика

Инструмент

Wireshark – профили QUIC и HTTP2

Практика

Референтный код

cloudflare/quiche (C), envoyproxy/envoy (C++) — промышленные реализации QUIC/H3

Общее

Конспекты

quic.xargs.org — «QUIC notes» (кратко, по разделам RFC)

Совет: сосредоточьтесь на спецификациях + одному реальному репо (например, h2 или quiche). Когда понимаешь wire-формат в одной кодовой базе, остальные протоколы читаются гораздо проще.

Happy hacking 🦀