跳转到正文
zeno's blog
返回

hyper(一):底层 HTTP 实现与 1.0 迁移

专题: hyper

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 协议实现库, 提供:

1.2 不是什么

hyper 不提供:

hyper 的定位是 building block — 其他框架构建在它之上, 而不是直接用它写应用.

1.3 谁在用 hyper

项目用法
axumHTTP server 底层
reqwestHTTP client 底层
tonicgRPC (HTTP/2) 底层
warpHTTP server 底层
Cloudflare WorkersWASM HTTP 处理

2. hyper 0.14 vs 1.0: 架构对比

2.1 版本时间线

2.2 核心变化总览

方面hyper 0.14hyper 1.0
Serverhyper::Server::bind().serve()移至 hyper-util, 手动 accept loop
Clienthyper::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::Builderhttp2::Builderhyper-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::Servicehyper::service::Service
self 引用&mut self&self
poll_ready有 (背压信号)
设计哲学通用异步函数 + 背压HTTP 请求处理, 简单直接

hyper 选择 &self 的理由:

  1. 异步函数天然返回 impl Future, 不需要 &mut self 来保证互斥
  2. HTTP server 的背压由 TCP 接收缓冲区自然提供, 不需要应用层 poll_ready
  3. 共享状态已经通过 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:

  1. 外层 (MakeService): 连接 → Service (每个连接调用一次)
  2. 内层 (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

  1. 更新依赖: hyper = "1" + hyper-util = "0.1" + http-body-util = "0.1"
  2. Body 类型: hyper::Bodyhyper::body::Incoming (入站) + 选择出站类型 (见 Body 文档)
  3. Server: hyper::Server → 手动 accept loop + hyper::server::conn::http1::Builderhyper-util::server::conn::auto::Builder
  4. Client: hyper::Clienthyper_util::client::legacy::Client
  5. Service trait: tower::Service 需要通过 TowerToHyperService 适配
  6. IO 适配: tokio 的 TcpStream 需要用 TokioIo::new() 包装
  7. 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 分裂为:

常见错误是尝试用 Incoming 构造出站响应 — 编译器会报类型不匹配.

8.3 tower::Service vs hyper::Service 混用

同一个文件中 use tower::Serviceuse 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?;

分享这篇文章:

上一篇
hyper(二):Body trait 与请求响应体
下一篇
可观测性(二):Rust 可观测性体系的架构理解