
Обновлено: пример полностью собирается на stable Rust (edition 2024) с актуальными версиями крейтов: bytes, anyhow, tokio
quinn,rcgen иrustls.
Что сделаем
Разберёмся, как фреймируются HTTP/2 и HTTP/3 (QUIC).
Напишем крошечный мини-фреймворк «Mini-Transport» (≈600 строк) на Rust:
• чтение/запись HTTP/2-фреймов,
• gRPC-кодек (без protobuf-codegen),
• переход на QUIC.Соберём рабочий 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 — содержит весь код
Как это всё работает (версия «для совсем-совсем чайника»)
Сервер запускается
Он слушает порт 50052 и сразу говорит:
«Эй, я умею HTTP/2!» (шлёт спец-строку-пролог клиенту).Клиент подключается
Он в ответ кивает той же спец-строкой — мол, «понял, тоже HTTP/2».Клиент кладёт слово “hello” в конверт
сначала к слову приклеивается крошечная бирка gRPC:
0(флаг) +5(длина) +hello;потом всё это завёртывается в больший конверт — DATA-фрейм HTTP/2;
конверт отправляется по проводам (TCP).
Сервер ловит конверт, разворачивает
видит DATA-фрейм → достаёт из него gRPC бирку;
читает «hello».
Сервер кладёт ответ “world” в новый конверт
Тот же процесс, только вместо «hello» — «world».Клиент получает конверт
Разворачивает — видит «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 -› |
Книга | 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) | |
Крейты | Документация |
|
Сниффинг трафика | Инструмент | Wireshark – профили QUIC и HTTP2 |
Практика | Референтный код |
|
Общее | Конспекты | quic.xargs.org — «QUIC notes» (кратко, по разделам RFC) |
Совет: сосредоточьтесь на спецификациях + одному реальному репо (например,
h2илиquiche). Когда понимаешь wire-формат в одной кодовой базе, остальные протоколы читаются гораздо проще.
Happy hacking 🦀
