HTTP/2 / HTTP/3 и gRPC на Rust: пишем учебный Mini-Transport
- пятница, 2 мая 2025 г. в 00:00:07
Обновлено: пример полностью собирается на 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».
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.
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‑клиент
[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"
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
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())
}
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(())
}
Полный листинг в репозитории (см. ниже).
//! Запускает упрощенный 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);
}
}
});
}
}
//! Мини‑клиент: посылает "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(())
}
$ cargo run --example server # терминал 1
$ cargo run --example client # терминал 2
response: world
$ cargo run --example quic_server # реализуйте сами вызов start_quic_server()
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 🦀