服务端 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_alive | true | 关掉后每个响应都带 connection: close,处理完一个请求就结束 |
half_close | false | 客户端在请求中途关掉写方向(读到 EOF)时,true 不立刻关连接 |
title_case_headers | false | 响应头名写成 Content-Length 这样的大小写 |
preserve_header_case | false | 记下请求头原来的大小写,存进私有扩展;目前只对代理式转发有用 |
max_headers | 未设,按 100 | 请求头个数上限,超了回 431。未设时用栈上 100 个槽位(DEFAULT_MAX_HEADERS);设了就改为每个请求在堆上分配,文档说性能下降约 5% |
max_header_size | 未设 | 1.12.0 新增。请求行加请求头的总字节数上限,超了回 431;未设时由 max_buf_size 兜底 |
header_read_timeout | 30 秒 | 要配合 timer 才生效,见下文 |
writev | 未设,自动 | 未设时看 io.is_write_vectored() 选排队还是拼平,见 读写缓冲 |
max_buf_size | 约 400 KB | 读缓冲上限,DEFAULT_MAX_BUFFER_SIZE = 8192 + 4096 × 100 = 417 792 字节。设置时小于 8192 会 panic |
auto_date_header | true | 每个响应自动加 date 头 |
pipeline_flush | false | 合并流水线请求的响应再 flush;文档标着「Experimental, may have bugs」 |
timer | 无 | 见 Executor 与 Timer |
allow_multiple_spaces_in_request_line_delimiters | false | 透传给 httparse 的 ParserConfig |
ignore_invalid_headers | false | 同上,开了以后格式不对的头行被静默忽略 |
注意这里没有执行器。HTTP/1 用不着 spawn 任何东西,和 HTTP/2 的 Builder::new(executor) 不同。
几个超时、大小相关的选项各管一段,具体在哪一步检查、超了以后回什么,分别见 Conn 的读写状态机(读头超时)、请求头的解析与编码(max_headers、max_header_size、431)和 读写缓冲(max_buf_size)。
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"] 提醒的就是这件事。
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 在每条连接上调用的东西,怎么在整个服务器层面协调见 优雅停机。
下面两条连接都在 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_timeout30 秒(没 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。