Table of contents
Open Table of contents
TL;DR
hyper 是 Rust 生态中 HTTP/1.1 和 HTTP/2 的底层协议实现, 不是 Web 框架. 1.0 版本做了破坏性重构: 移除内置高层 Server/Client 到 hyper-util, Body 从具体类型变为 trait, 定义了自己的 Service trait (&self 而非 &mut self, 无 poll_ready). hyper 是 axum、reqwest、tonic 的底层引擎.
1. hyper 是什么, 不是什么
1.1 是什么
hyper 是一个 HTTP 协议实现库, 提供:
- HTTP/1.0, HTTP/1.1, HTTP/2 的编解码
- 连接管理 (keep-alive, 多路复用)
- 流式 body 处理
- 零拷贝解析
1.2 不是什么
hyper 不提供:
- 路由 (用 axum / actix-web)
- 模板渲染 (用 askama / tera)
- 中间件框架 (用 tower)
- JSON 序列化 (用 serde_json)
- 连接池 (1.0 后移到 hyper-util)
- 高层 Server 抽象 (1.0 后移到 hyper-util)
hyper 的定位是 building block — 其他框架构建在它之上, 而不是直接用它写应用.
1.3 谁在用 hyper
| 项目 | 用法 |
|---|---|
| axum | HTTP server 底层 |
| reqwest | HTTP client 底层 |
| tonic | gRPC (HTTP/2) 底层 |
| warp | HTTP server 底层 |
| Cloudflare Workers | WASM HTTP 处理 |
2. hyper 0.14 vs 1.0: 架构对比
2.1 版本时间线
- hyper 0.14: 2020 年发布, 长期稳定版本, 大量项目依赖
- hyper 1.0: 2023 年 11 月发布, 第一个 semver 稳定版本
2.2 核心变化总览
| 方面 | hyper 0.14 | hyper 1.0 |
|---|---|---|
| Server | hyper::Server::bind().serve() | 移至 hyper-util, 手动 accept loop |
| Client | hyper::Client::new() 带连接池 | 移至 hyper-util::client::legacy::Client |
| Body 类型 | hyper::Body (具体类型) | hyper::body::Incoming + http_body::Body trait |
| Service trait | 使用 tower::Service | 定义自己的 hyper::service::Service |
| HTTP 版本 | 自动 HTTP/1 和 HTTP/2 | 手动选择 http1::Builder 或 http2::Builder 或 hyper-util::server::conn::auto |
| 运行时 | 内置 tokio 集成 | 运行时无关, 通过 hyper-util::rt 适配 |
| 连接处理 | 请求级 Service | 连接级 Service |
2.3 Server: 从高层到底层
hyper 0.14:
// 0.14: 一行启动 server
use hyper::{Server, service::make_service_fn, service::service_fn};
let make_svc = make_service_fn(|_conn| async {
Ok::<_, hyper::Error>(service_fn(handler))
});
Server::bind(&addr).serve(make_svc).await?;
Server 内部处理了: TCP listener, accept loop, 连接管理, HTTP 版本协商, 优雅关闭.
hyper 1.0:
// 1.0: 手动控制每个步骤
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper_util::rt::TokioIo;
let listener = tokio::net::TcpListener::bind(addr).await?;
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
tokio::spawn(async move {
if let Err(err) = http1::Builder::new()
.serve_connection(io, service_fn(handler))
.await
{
eprintln!("Error: {err:?}");
}
});
}
1.0 需要手动: 创建 listener, accept 连接, 包装 IO, 选择 HTTP 版本, spawn task. 这给了开发者完全的控制权, 但模板代码更多.
hyper-util 的 auto Builder:
// hyper-util: 自动 HTTP/1 + HTTP/2 协商
use hyper_util::server::conn::auto;
use hyper_util::rt::TokioExecutor;
auto::Builder::new(TokioExecutor::new())
.serve_connection(io, service)
.await?;
2.4 Client: 连接池外移
hyper 0.14:
// 0.14: 内置连接池
let client = hyper::Client::new();
let resp = client.get(uri).await?;
hyper 1.0:
// 1.0: 连接池在 hyper-util 中
use hyper_util::client::legacy::Client;
use hyper_util::rt::TokioExecutor;
let client = Client::builder(TokioExecutor::new())
.pool_idle_timeout(Duration::from_secs(30))
.http2_only(true)
.build_http();
hyper-util::client::legacy::Client 功能与 0.14 的 Client 几乎一致, 只是换了位置.
2.5 Service trait: 从 tower 到自有
hyper 0.14 使用 tower::Service:
// 0.14: 复用 tower 的 Service
impl tower::Service<Request<Body>> for MyService {
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { ... }
fn call(&mut self, req: Request<Body>) -> Self::Future { ... }
}
hyper 1.0 定义自己的 Service:
// 1.0: hyper 自己的 Service trait
// crate: hyper
// 模块: hyper::service
pub trait Service<Request> {
type Response;
type Error;
type Future: Future<Output = Result<Self::Response, Self::Error>>;
fn call(&self, req: Request) -> Self::Future;
}
关键差异:
| 差异点 | tower::Service | hyper::service::Service |
|---|---|---|
| self 引用 | &mut self | &self |
| poll_ready | 有 (背压信号) | 无 |
| 设计哲学 | 通用异步函数 + 背压 | HTTP 请求处理, 简单直接 |
hyper 选择 &self 的理由:
- 异步函数天然返回
impl Future, 不需要&mut self来保证互斥 - HTTP server 的背压由 TCP 接收缓冲区自然提供, 不需要应用层
poll_ready - 共享状态已经通过
Arc<_>管理,&mut self增加复杂性但无实际收益
桥接: TowerToHyperService
// hyper-util 提供适配器
use hyper_util::service::TowerToHyperService;
// 将 tower::Service 转换为 hyper::service::Service
let hyper_service = TowerToHyperService::new(tower_service);
3. hyper 1.0 的模块结构
3.1 hyper crate (核心, 最小化)
hyper
├── body
│ ├── Incoming // 入站请求/响应的 body 类型
│ ├── Body trait // re-export from http-body
│ ├── Bytes // re-export from bytes
│ ├── Frame // body 帧 (data 或 trailers)
│ └── SizeHint // body 大小提示
├── client
│ └── conn
│ ├── http1 // HTTP/1 客户端连接
│ └── http2 // HTTP/2 客户端连接
├── server
│ └── conn
│ ├── http1 // HTTP/1 服务端连接
│ │ ├── Builder // 配置 HTTP/1 连接
│ │ ├── Connection // 活跃的 HTTP/1 连接 (Future)
│ │ └── Parts // 连接分解
│ └── http2 // HTTP/2 服务端连接
│ ├── Builder
│ └── Connection
├── service
│ ├── Service trait // hyper 自己的 Service trait
│ └── service_fn // 从 async fn 创建 Service
├── rt
│ ├── Read trait // 异步读 (不绑定 tokio)
│ ├── Write trait // 异步写 (不绑定 tokio)
│ └── Timer trait // 定时器 (不绑定 tokio)
└── Error // hyper 错误类型
3.2 hyper-util crate (高层工具)
hyper-util
├── client
│ └── legacy
│ └── Client // 带连接池的 HTTP 客户端
├── server
│ └── conn
│ └── auto
│ └── Builder // 自动 HTTP/1 + HTTP/2 协商
├── rt
│ ├── TokioIo // tokio::io → hyper::rt::Read/Write 适配器
│ └── TokioExecutor // tokio runtime → hyper executor 适配器
└── service
└── TowerToHyperService // tower::Service → hyper::Service 适配器
4. 连接级 Service vs 请求级 Service
这是 hyper 1.0 最重要的架构概念之一.
4.1 hyper 0.14: 请求级 Service
在 0.14 中, make_service_fn 为每个连接创建一个 Service, 这个 Service 处理该连接上的所有请求:
// 0.14: make_service_fn 在每个连接建立时调用
let make_svc = make_service_fn(|conn: &AddrStream| {
let remote_addr = conn.remote_addr();
async move {
Ok::<_, Error>(service_fn(move |req| {
handle(remote_addr, req)
}))
}
});
两层 Service:
- 外层 (
MakeService): 连接 → Service (每个连接调用一次) - 内层 (
Service): Request → Response (每个请求调用一次)
4.2 hyper 1.0: 连接级 Service
在 1.0 中, serve_connection 直接接收一个 Service, 开发者自己在 accept loop 中决定如何为每个连接创建 Service:
// 1.0: 显式的连接 + service 关联
loop {
let (stream, remote_addr) = listener.accept().await?;
let io = TokioIo::new(stream);
// 每个连接的 service 创建逻辑在这里, 完全由开发者控制
let svc = service_fn(move |req| handle(remote_addr, req));
tokio::spawn(async move {
http1::Builder::new()
.serve_connection(io, svc) // service 绑定到连接
.await
});
}
好处: 开发者对连接生命周期有完全控制, 不需要理解 MakeService 的抽象.
5. HTTP/1 vs HTTP/2 在 hyper 中的处理
5.1 协议选择
| 方式 | 说明 | 用法 |
|---|---|---|
hyper::server::conn::http1::Builder | 只接受 HTTP/1 连接 | 明确知道客户端协议时 |
hyper::server::conn::http2::Builder | 只接受 HTTP/2 连接 | gRPC 等纯 HTTP/2 场景 |
hyper_util::server::conn::auto::Builder | 自动协商 HTTP/1 或 HTTP/2 | 通用 server |
5.2 HTTP/2 多路复用的影响
HTTP/2 在单个 TCP 连接上多路复用多个请求流. 这意味着:
HTTP/1.1: 一个连接 → 串行请求 (keep-alive) → 每个请求依次调用 Service::call
HTTP/2: 一个连接 → 并发请求 (multiplexing) → 多个 Service::call 并发执行
这就是 hyper 1.0 选择 &self 而非 &mut self 的另一个原因: HTTP/2 连接上的 Service 需要被并发调用, &mut self 会要求串行化.
6. Feature Flags
hyper 1.0 通过 feature flag 精细控制功能:
[dependencies]
hyper = { version = "1", features = [
"http1", # HTTP/1 支持
"http2", # HTTP/2 支持
"server", # 服务端 API
"client", # 客户端 API
] }
hyper-util = { version = "0.1", features = [
"tokio", # TokioIo, TokioExecutor
"server-auto", # auto::Builder (自动协商)
"client-legacy", # 带连接池的 Client
"http1", # hyper-util 的 HTTP/1 支持
"http2", # hyper-util 的 HTTP/2 支持
] }
7. 迁移 Checklist: 0.14 → 1.0
- 更新依赖:
hyper = "1"+hyper-util = "0.1"+http-body-util = "0.1" - Body 类型:
hyper::Body→hyper::body::Incoming(入站) + 选择出站类型 (见 Body 文档) - Server:
hyper::Server→ 手动 accept loop +hyper::server::conn::http1::Builder或hyper-util::server::conn::auto::Builder - Client:
hyper::Client→hyper_util::client::legacy::Client - Service trait:
tower::Service需要通过TowerToHyperService适配 - IO 适配: tokio 的
TcpStream需要用TokioIo::new()包装 - Executor: 需要显式传递
TokioExecutor::new(), 不再自动绑定 tokio
8. Pitfalls
8.1 忘记 TokioIo 包装
hyper 1.0 不依赖 tokio, 它定义了自己的 Read/Write trait. tokio 的 TcpStream 不直接实现这些 trait, 必须用 TokioIo 包装:
// 错误: TcpStream 不满足 hyper 的 IO trait bounds
http1::Builder::new().serve_connection(stream, svc); // 编译错误
// 正确: 用 TokioIo 适配
let io = TokioIo::new(stream);
http1::Builder::new().serve_connection(io, svc).await?;
8.2 Body 类型混淆
hyper 0.14 的 hyper::Body 同时用于入站和出站. 1.0 分裂为:
- 入站:
hyper::body::Incoming(不可构造, 只能由 hyper 内部创建) - 出站: 开发者选择 (
http_body_util::Full<Bytes>,http_body_util::Empty<Bytes>,BoxBody, 自定义)
常见错误是尝试用 Incoming 构造出站响应 — 编译器会报类型不匹配.
8.3 tower::Service vs hyper::Service 混用
同一个文件中 use tower::Service 和 use hyper::service::Service 会冲突. 用完全限定路径或重命名导入:
use tower::Service as TowerService;
use hyper::service::Service as HyperService;
8.4 HTTP/2 需要 Executor
hyper 1.0 的 HTTP/2 Builder 需要显式传递 Executor:
// HTTP/1 不需要
http1::Builder::new().serve_connection(io, svc).await?;
// HTTP/2 需要 executor
http2::Builder::new(TokioExecutor::new())
.serve_connection(io, svc)
.await?;
原因: HTTP/2 多路复用需要 spawn 子任务处理各个 stream, 而 hyper 不假设具体的运行时.
8.5 serve_connection 的 graceful shutdown
serve_connection 返回的 Connection 是一个 Future, drop 它会立即断开连接. 优雅关闭需要:
let conn = http1::Builder::new()
.serve_connection(io, svc);
// pin 住连接
tokio::pin!(conn);
// 收到关闭信号后, 调用 graceful_shutdown
conn.as_mut().graceful_shutdown();
// 等待现有请求完成
conn.await?;