Rust in Everything
基于 hyper 1.12.0

服务端 Builder 与 Connection

主线位置:服务端主线的入口。accept 得到 socket,TokioIo::new(stream) 包成 hyper 的 IO,http1::Builder::serve_connection(io, service) 得到 Connection,之后「字节 → 请求 → 响应 → 字节」的循环全在 poll 这个 Connection 的过程中发生。

flowchart TB
  bld["Builder::new + 选项"] --> sc["serve_connection(io, svc)"]
  sc --> conn["Conn::new + set_*"]
  sc --> srv["dispatch::Server::new"]
  conn --> disp["Dispatcher::new"]
  srv --> disp
  disp --> fut["Connection(future)"]
  fut -->|"await / spawn"| run["poll_loop 处理请求"]
  fut -->|graceful_shutdown| gs["关 keep-alive"]
  fut -->|without_shutdown| parts["Parts: io + read_buf"]
  fut -->|with_upgrades| up["UpgradeableConnection"]

hyper 1.0 去掉了 0.14 的 Server:不再替你 accept、不再替你 spawn。剩下的服务端 API 就是这一个文件 server/conn/http1.rs(HTTP/2 的对应物在 http2.rs,两者合一的 auto 在 hyper-util,见 auto:一个端口两个协议)。一个典型的 accept 循环(片段):

loop {
  let (stream, _) = listener.accept().await?;
  tokio::spawn(async move {
    let io = TokioIo::new(stream);
    if let Err(e) = http1::Builder::new().serve_connection(io, service_fn(handler)).await {
      eprintln!("connection error: {e}");
    }
  });
}

每条连接一个任务,这个任务是你 spawn 的,不是 hyper。

Builder:一包选项

Builder 是 Clone 的普通结构体,选项方法都是 &mut self -> &mut Self,可以链式调用。文档开头先声明:「The default values of options are not considered stable. They are subject to change at any time.」下表的默认值取自 Builder::new 和各方法的文档:

方法默认说明
keep_alivetrue关掉后每个响应都带 connection: close,处理完一个请求就结束
half_closefalse客户端在请求中途关掉写方向(读到 EOF)时,true 不立刻关连接
title_case_headersfalse响应头名写成 Content-Length 这样的大小写
preserve_header_casefalse记下请求头原来的大小写,存进私有扩展;目前只对代理式转发有用
max_headers未设,按 100请求头个数上限,超了回 431。未设时用栈上 100 个槽位(DEFAULT_MAX_HEADERS);设了就改为每个请求在堆上分配,文档说性能下降约 5%
max_header_size未设1.12.0 新增。请求行加请求头的总字节数上限,超了回 431;未设时由 max_buf_size 兜底
header_read_timeout30 秒要配合 timer 才生效,见下文
writev未设,自动未设时看 io.is_write_vectored() 选排队还是拼平,见 读写缓冲
max_buf_size约 400 KB读缓冲上限,DEFAULT_MAX_BUFFER_SIZE = 8192 + 4096 × 100 = 417 792 字节。设置时小于 8192 会 panic
auto_date_headertrue每个响应自动加 date 头
pipeline_flushfalse合并流水线请求的响应再 flush;文档标着「Experimental, may have bugs」
timer无见 Executor 与 Timer
allow_multiple_spaces_in_request_line_delimitersfalse透传给 httparse 的 ParserConfig
ignore_invalid_headersfalse同上,开了以后格式不对的头行被静默忽略

注意这里没有执行器。HTTP/1 用不着 spawn 任何东西,和 HTTP/2 的 Builder::new(executor) 不同。

几个超时、大小相关的选项各管一段,具体在哪一步检查、超了以后回什么,分别见 Conn 的读写状态机(读头超时)、请求头的解析与编码(max_headers、max_header_size、431)和 读写缓冲(max_buf_size)。

📝NOTE
冷知识:max_header_size 是 1.12.0 才有的

#4183(关联 issue #3832)给 HTTP/1 的服务端和客户端 Builder 都加了 max_header_size,第一个包含它的发布是 v1.12.0,也就是本卷的版本。PR 说明讲了之前的问题:「Currently, HTTP/1 header size limits can only be indirectly bounded using max_buf_size. However, setting max_buf_size also restricts the maximum single read buffer for streaming request or response bodies.」想把请求头限制得小一点,就得把整个读缓冲调小,连带影响 body 的读取。

新选项在两处检查:边读边检查(「Enforced in io::Buffered::parse_headers as bytes are read」),以及解析完成后按已解析长度再查一次。PR 还顺手替换了 conn.rs 里一个 // TODO: remove this when we land h1_max_header_size support 的占位,让 chunked trailers 的大小上限也用这个值,这就是上表文档里「This value is also used as the maximum size limit for chunked trailers」那句的来历。

header_read_timeout 是表里最容易误会的一项。默认 30 秒,但 Builder::new() 里的 timer 是空的,没调 .timer(...) 时这 30 秒根本不生效;显式调了 header_read_timeout(d) 却没给 timer,serve_connection 当场 panic。另外它不只管「一个请求头读太慢」,连接空闲着等下一个 keep-alive 请求的时间也算在里面。完整的规则和实测见 Executor 与 Timer。

serve_connection:只组装,不启动

serve_connection 的全部工作是把选项逐个抄到一个新的 Conn 上:

    pub fn serve_connection<I, S>(&self, io: I, service: S) -> Connection<I, S>
    where
        S: HttpService<IncomingBody>,
        S::Error: Into<Box<dyn StdError + Send + Sync>>,
        S::ResBody: 'static,
        <S::ResBody as Body>::Error: Into<Box<dyn StdError + Send + Sync>>,
        I: Read + Write + Unpin,
    {
        let mut conn = proto::Conn::new(io);
        conn.set_h1_parser_config(self.h1_parser_config.clone());
        conn.set_timer(self.timer.clone());
        if !self.h1_keep_alive {
            conn.disable_keep_alive();
        }
        // ...
        if let Some(dur) = self
            .timer
            .check(self.h1_header_read_timeout, "header_read_timeout")
        {
            conn.set_http1_header_read_timeout(dur);
        }
        // ...
        if !self.date_header {
            conn.disable_date_header();
        }
        let sd = proto::h1::dispatch::Server::new(service);
        let proto = proto::h1::Dispatcher::new(sd, conn);
        Connection { conn: proto }
    }

三层东西在这里装好:

  • Conn:持有 IO 和读写缓冲,管协议状态(读到哪、写到哪、还能不能 keep-alive)。见 Conn 的读写状态机。
  • dispatch::Server:持有你的服务,加一个放在途响应 future 的槽位。
  • Dispatcher:把两者粘在一起的循环。见 Dispatcher。

这个函数是同步的,不碰 IO,不读一个字节。唯一可能出事的是 check:显式配置了超时却没给 timer,就在这里 panic。

约束只要求 I: Read + Write + Unpin,不要求 Send 也不要求 'static;服务也不要求 Send。Connection 是不是 Send,完全取决于你给的 IO 和服务。所以在单线程运行时上用 Rc 写的服务,也能直接交给 HTTP/1。

Connection 就是一个 future

Connection 只包着一个 Dispatcher,它的 Future 实现 也只是转发:

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        match ready!(Pin::new(&mut self.conn).poll(cx)) {
            Ok(done) => {
                match done {
                    proto::Dispatched::Shutdown => {}
                    proto::Dispatched::Upgrade(pending) => {
                        // With no `Send` bound on `I`, we can't try to do
                        // upgrades here. In case a user was trying to use
                        // `Body::on_upgrade` with this API, send a special
                        // error letting them know about that.
                        pending.manual();
                    }
                }
                Poll::Ready(Ok(()))
            }
            Err(e) => Poll::Ready(Err(e)),
        }
    }

Output 是 hyper::Result<()>:连接正常结束(对方关了、keep-alive 关了)是 Ok(()),出错是 Err。服务对单个请求返回的错误不会作为 Ok 继续,而是让整个 Connection 以错误结束。

连接上所有的工作,读请求头、调服务、等响应 future、写响应、读下一个请求,都发生在 Dispatcher::poll 里,也就是 poll 这个 Connection 的那个任务里。下面在一条连接上流水线式发三个请求,在服务里打印当前 tokio 任务的 id:

hyper = { version = "=1.12.0", features = ["server", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio"] }
http-body-util = "0.1"
bytes = "1"
tokio = { version = "=1.54.1", features = ["full"] }
use std::convert::Infallible;

use bytes::Bytes;
use http_body_util::Full;
use hyper::{body::Incoming, server::conn::http1, service::service_fn, Request, Response};
use hyper_util::rt::TokioIo;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};

async fn handler(req: Request<Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
  println!("  {} handled in task {}", req.uri().path(), tokio::task::id());
  Ok(Response::new(Full::new(Bytes::from_static(b"ok"))))
}

async fn pair() -> (TcpStream, TcpStream) {
  let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
  let client = TcpStream::connect(listener.local_addr().unwrap()).await.unwrap();
  let (server, _) = listener.accept().await.unwrap();
  (server, client)
}

#[tokio::main]
async fn main() {
  // 1. every request on one connection runs inside the task that polls Connection
  println!("1. three pipelined requests");
  let (srv, mut cli) = pair().await;
  let task = tokio::spawn(async move {
    println!("  connection task {}", tokio::task::id());
    http1::Builder::new().serve_connection(TokioIo::new(srv), service_fn(handler)).await
  });
  cli
    .write_all(
      b"GET /a HTTP/1.1\r\nHost: x\r\n\r\n\
        GET /b HTTP/1.1\r\nHost: x\r\n\r\n\
        GET /c HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n",
    )
    .await
    .unwrap();
  let mut out = String::new();
  cli.read_to_string(&mut out).await.unwrap();
  println!("  result {:?}", task.await.unwrap().map_err(|e| e.to_string()));
  let first = out.split("\r\n\r\n").next().unwrap();
  println!("  first response head: {first:?}");

  // 2. without_shutdown hands back the socket and unread bytes
  println!("2. keep_alive(false) + without_shutdown");
  let (srv, mut cli) = pair().await;
  let task = tokio::spawn(async move {
    let conn = http1::Builder::new()
      .keep_alive(false)
      .serve_connection(TokioIo::new(srv), service_fn(handler));
    let parts = conn.without_shutdown().await.unwrap();
    println!("  read_buf = {:?}", parts.read_buf);
    let mut io = parts.io.into_inner();
    io.write_all(b"raw bytes after HTTP\n").await.unwrap();
  });
  cli
    .write_all(b"GET /a HTTP/1.1\r\nHost: x\r\n\r\nGET /b HTTP/1.1\r\nHost: x\r\n\r\n")
    .await
    .unwrap();
  task.await.unwrap();
  let mut out = String::new();
  cli.read_to_string(&mut out).await.unwrap();
  let lines: Vec<&str> = out
    .split("\r\n")
    .filter(|l| l.starts_with("HTTP/") || l.starts_with("connection") || l.contains("raw"))
    .collect();
  println!("  client got {lines:?}");
}

M2 Max、rustc 1.99.0、--release,连跑 3 次,除 date 头里的时间外输出相同:

1. three pipelined requests
  connection task 13
  /a handled in task 13
  /b handled in task 13
  /c handled in task 13
  result Ok(())
  first response head: "HTTP/1.1 200 OK\r\ncontent-length: 2\r\ndate: Fri, 23 Oct 2026 13:22:01 GMT"
2. keep_alive(false) + without_shutdown
  /a handled in task 14
  read_buf = b"GET /b HTTP/1.1\r\nHost: x\r\n\r\n"
  client got ["HTTP/1.1 200 OK", "connection: close", "okraw bytes after HTTP\n"]

第 1 部分:三个请求都在 13 号任务里处理,就是 spawn 出来 poll Connection 的那个任务。hyper 没有为请求另起任务,服务的 future 是被 Dispatcher 直接 poll 的。响应头里只有 content-length 和 date,date 来自默认开着的 auto_date_header。第 2 部分留到下文 without_shutdown 再看。

这有两个直接后果。第一,同一条连接上的请求是一个接一个处理的,一个慢请求会挡住后面所有的请求(见 Dispatcher)。第二,Connection 不被 poll 就什么都不发生;类型上的 #[must_use = "futures do nothing unless polled"] 提醒的就是这件事。

💡TIP
打个比方:一条连接一个专属服务员

serve_connection 像是给一张桌子(socket)指派一个服务员(Connection),菜单是你的服务。服务员不会另外叫人帮忙:点单(读请求头)、下单给厨房(service.call)、等菜(poll 响应 future)、上菜(写响应)、等下一位点单,全是他一个人按顺序做。客人一口气点三道菜(流水线),他也是记下来,一道一道上。服务员什么时候干活,取决于你让不让他动(poll)。

比方到这里为止:真正的服务员会等待,而 Connection 在等 IO 或响应 future 时返回 Pending,把线程让给别的连接,等被唤醒再接着做。「一个人」是指一个任务,不是一个线程。

graceful_shutdown

graceful_shutdown 只有一行:

    pub fn graceful_shutdown(mut self: Pin<&mut Self>) {
        self.conn.disable_keep_alive();
    }

它不等待、不返回 future,只是改状态;之后要继续 poll 这个 Connection,等它自己结束。到 Dispatcher 里,disable_keep_alive 分两种情况:

    pub(crate) fn disable_keep_alive(&mut self) {
        self.conn.disable_keep_alive();

        // If keep alive has been disabled and no read or write has been seen on
        // the connection yet, we must be in a state where the server is being asked to
        // shut down before any data has been seen on the connection
        if self.conn.is_write_closed() || self.conn.has_initial_read_write_state() {
            self.close();
        }
    }

Conn 那一层的 disable_keep_alive 再看连接是否空闲:空闲就直接关;正在处理请求,就只把 keep-alive 关掉,让当前请求做完。做完以后连接不会回到「等下一个请求」,而是结束。

这是 hyper-util 的 GracefulShutdown 和 axum 的 with_graceful_shutdown 在每条连接上调用的东西,怎么在整个服务器层面协调见 优雅停机。

❗IMPORTANT
动脑筋:关掉一条空连接和一条忙连接,各要多久?

下面两条连接都在 50 ms 后调用 graceful_shutdown。第一条连接上客户端什么都没发;第二条连接上客户端发了一个 /slow 请求,服务要处理 200 ms。依赖同上。

use std::convert::Infallible;
use std::time::{Duration, Instant};

use bytes::Bytes;
use http_body_util::Full;
use hyper::{body::Incoming, server::conn::http1, service::service_fn, Request, Response};
use hyper_util::rt::TokioIo;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};

async fn handler(req: Request<Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
  if req.uri().path() == "/slow" {
    tokio::time::sleep(Duration::from_millis(200)).await;
  }
  println!("  {} handled", req.uri().path());
  Ok(Response::new(Full::new(Bytes::from_static(b"ok"))))
}

async fn pair() -> (TcpStream, TcpStream) {
  let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
  let client = TcpStream::connect(listener.local_addr().unwrap()).await.unwrap();
  let (server, _) = listener.accept().await.unwrap();
  (server, client)
}

#[tokio::main]
async fn main() {
  // graceful_shutdown: an idle connection vs. a request in flight
  for (label, bytes) in [("idle", &b""[..]), ("in flight", &b"GET /slow HTTP/1.1\r\nHost: x\r\n\r\n"[..])] {
    println!("{label}:");
    let (srv, mut cli) = pair().await;
    cli.write_all(bytes).await.unwrap();
    let task = tokio::spawn(async move {
      let conn = http1::Builder::new().serve_connection(TokioIo::new(srv), service_fn(handler));
      tokio::pin!(conn);
      tokio::select! {
        r = conn.as_mut() => println!("  finished before shutdown: {r:?}"),
        _ = tokio::time::sleep(Duration::from_millis(50)) => {
          let t = Instant::now();
          conn.as_mut().graceful_shutdown();
          let r = conn.await;
          println!("  after graceful_shutdown: {:?} in {} ms", r.map_err(|e| e.to_string()), t.elapsed().as_millis());
        }
      }
    });
    let mut out = String::new();
    cli.read_to_string(&mut out).await.unwrap();
    task.await.unwrap();
    let lines: Vec<&str> = out.split("\r\n").filter(|l| l.starts_with("HTTP/") || l.starts_with("connection")).collect();
    println!("  client got {lines:?}");
  }
}

两条连接从调用 graceful_shutdown 到 Connection 结束各用了多少毫秒?第二条连接的客户端能拿到完整响应吗,响应头里会多出什么?

without_shutdown 与 into_parts:把 socket 要回来

Connection 正常结束时会对 IO 调用 poll_shutdown(TCP 上就是关闭写方向)。有时你不想这样:比如处理完 HTTP 之后要在同一个 socket 上说别的协议。without_shutdown 跑到「HTTP 认为这条连接结束了」就停,不关 IO,然后调 into_parts 把东西拆出来:

    pub fn into_parts(self) -> Parts<I, S> {
        let (io, read_buf, dispatch) = self.conn.into_inner();
        Parts {
            io,
            read_buf,
            service: dispatch.into_service(),
        }
    }

Parts 有三个公开字段:原来的 IO、read_buf(已经从 socket 读进来、但还没被当成 HTTP 处理的字节)、你的服务。它是 #[non_exhaustive],以后可以加字段。read_buf 的文档特意提醒:打算继续用这个 IO,就要先检查里面有没有字节。

上面程序的第 2 部分就是这个场景:关掉 keep-alive,客户端一次发两个请求。服务端处理完 /a 就认为连接结束(响应里带了 connection: close),/b 已经被读进缓冲区但没被处理,原样出现在 read_buf 里。服务端随后用拿回的 socket 写了一行 raw bytes after HTTP,客户端紧跟在 /a 响应的 body ok 之后收到了它(输出里的 okraw bytes after HTTP),说明 socket 没有被关。

更底层的 poll_without_shutdown 是同一件事的 poll 版本,要求服务和它的 future 都是 Unpin;without_shutdown 用 poll_fn 包了它,省掉这条限制。

with_upgrades

普通的 Connection 遇到协议升级(比如 WebSocket 的 101 Switching Protocols)时只能调 pending.manual(),因为它对 I 没有 Send 约束,没法把 IO 交给另一个任务里的 OnUpgrade(上面 poll 里的注释)。with_upgrades 换成 UpgradeableConnection,它的 Future 实现要求 I: Send + 'static,在升级时把 IO 和缓冲区拆出来交出去:

                Ok(proto::Dispatched::Upgrade(pending)) => {
                    let (io, buf, _) = self
                        .inner
                        .take()
                        .expect("upgradeable server connection missing after upgrade")
                        .conn
                        .into_inner();
                    pending.fulfill(Upgraded::new(io, buf));
                    Poll::Ready(Ok(()))
                }

buf 就是上一节的 read_buf:升级请求之后客户端可能已经发来了新协议的字节,它们要跟着 IO 一起交出去,不能丢。inner 是 Option,升级后变成 None,所以 UpgradeableConnection 的 graceful_shutdown 和 into_parts 都要先看它还在不在。升级本身怎么工作见 协议升级。

动脑筋答案

M2 Max、rustc 1.99.0、--release,连跑 3 次,第二条连接的耗时分别是 150、150、151 ms,其余相同:

idle:
  after graceful_shutdown: Ok(()) in 0 ms
  client got []
in flight:
  /slow handled
  after graceful_shutdown: Ok(()) in 151 ms
  client got ["HTTP/1.1 200 OK", "connection: close"]
  • idle:连接上一个字节都没收到,读写都还在初始状态,Dispatcher::disable_keep_alive 里的 has_initial_read_write_state() 为真,直接 close()。0 ms 结束,客户端一个字节都没收到,只看到连接关闭。
  • in flight:请求已经在处理,Conn::disable_keep_alive 只关掉 keep-alive。服务还要再睡 150 ms(200 减去已经过去的 50),响应照常写出,多了一个 connection: close 头告诉客户端别再用这条连接,然后 Connection 以 Ok(()) 结束。在途请求没有被打断,这就是「优雅」的含义。

小结

  • http1::Builder 是一包选项,默认值 hyper 声明不稳定;没有执行器,timer 默认为空。几个默认值:keep-alive 开、header_read_timeout 30 秒(没 timer 不生效)、date 头开、请求头最多 100 个、读缓冲上限 417 792 字节;max_header_size 是 1.12.0 新加的。
  • serve_connection 是同步的,只把选项装进 Conn、把服务装进 dispatch::Server、用 Dispatcher 粘起来;只要求 I: Read + Write + Unpin,不要求 Send。
  • Connection 就是一个 future,连接上所有请求都在 poll 它的那个任务里处理,实测三个流水线请求的任务 id 相同;不 poll 就不干活。
  • graceful_shutdown 只是关掉 keep-alive:空连接立刻关,忙连接做完当前请求、带上 connection: close 再关。
  • without_shutdown / into_parts 交还 IO、未处理的 read_buf 和服务,不关 socket;with_upgrades 要求 I: Send + 'static,升级时把 IO 和缓冲区一起交给 Upgraded。

💬 关联讨论

发起讨论
加载中...