HTTP/2 服务端
主线位置:服务端主线的 HTTP/2 版本。「字节 → 请求」交给 h2:它解析帧、拼出
http::Request;hyper 在连接任务里调用服务,把「等响应 → 写响应头 → 写 body」放进每个流自己的任务。HTTP/1 那条线上Dispatcher一个 future 包办的事,在这里分到了 1 + N 个任务里。
flowchart TB sc["serve_connection"] --> hs["State::Handshaking"] hs --> sv["State::Serving"] sv --> acc["poll_accept 收到新流"] acc --> callreq["service.call:连接任务里"] callreq --> spawn["execute_h2stream"] spawn --> st["H2Stream 任务:poll 服务 future"] st --> head["send_response 写响应头"] head --> pipe["PipeToSendStream 写 body"] acc --> acc sv --> gs["graceful_shutdown:GOAWAY"]
帧的编解码、HPACK、流状态、流控记账都在 h2 crate(本卷对照 h2 0.4.20)里。hyper 的 proto/h2/server.rs 只有 556 行,做三件事:握手、收流并把每个流交给执行器、把服务的响应写回 h2。
Builder 与 Connection:一层壳
http2::Builder 只有三个字段:
pub struct Builder<E> {
exec: E,
timer: Time,
h2_builder: proto::h2::server::Config,
}
和 http1::Builder 不同,Builder::new 必须传一个执行器。timer 从 Time::Empty 开始,只有 keep-alive 非要它不可(BDP 只取 now(),没有 timer 时退回 Instant::now()),见 ping:保活与 BDP。serve_connection 用这三样建一个 proto::h2::Server,包进 Connection 返回,和 HTTP/1 一样不 spawn 任何东西。Connection::poll 把内部的 Dispatched 丢掉,只剩 Result<()>:HTTP/2 的 CONNECT 隧道只占一个流,不会把整条连接的 IO 交出去,所以内部永远只返回 Dispatched::Shutdown,留着这个类型是历史原因(源码里一条 TODO 这么说)。
执行器的约束写成 E: Http2ServerConnExec<S::Future, Bd>,它为什么是一个 sealed trait、用 TokioExecutor 时为什么服务的 future 和响应 body 都必须是 Send,见 Executor 与 Timer。hyper-util 的 auto::Builder 里的 HTTP/2 分支用的就是这个 Builder,见 auto:一个端口两个协议。
| 选项 | 默认 | 说明 |
|---|---|---|
initial_connection_window_size | 1 MiB | 规范默认是 65535,源码注释说这对性能太小 |
initial_stream_window_size | 1 MiB | 同上 |
max_frame_size | 16 KiB | |
max_send_buf_size | 400 KiB | 每个流在 h2 里缓冲待发数据的上限 |
max_header_list_size | 16 KiB | |
max_concurrent_streams | 200 | 1.0.0 起(#3362),之前不限 |
max_local_error_reset_streams | 1024 | |
keep_alive_interval | 无 | 设了就要 timer |
keep_alive_timeout | 20 秒 | |
adaptive_window | 关 | 打开时两个窗口都改成 65535,由 BDP 往上调 |
auto_date_header | 开 |
两个窗口选项和 adaptive_window 互相覆盖:调 initial_stream_window_size 会把 adaptive_window 关掉,调 adaptive_window(true) 会把两个窗口改回 SPEC_WINDOW_SIZE(65535)。后调的那个说了算。
Server:握手,然后收流
enum State<T, B>
where
B: Body,
{
Handshaking {
ping_config: ping::Config,
hs: Handshake<Compat<T>, SendBuf<B::Data>>,
},
Serving(Serving<T, B>),
}
Server::new 把 Config 一项项抄进 h2::server::Builder,然后 builder.handshake(Compat::new(io))。h2 要的是 tokio 的 AsyncRead / AsyncWrite,hyper 手里是自己的 rt::Read / rt::Write,Compat 就是两者之间的转接(两个 trait 为什么不同,见 IO trait)。h2 依赖 tokio 的 io-util,所以开了 http2 特性,依赖树里就会有 tokio 的 IO trait,但仍然没有 tokio 的运行时。
poll 是一个两步的状态机:
let next = match &mut me.state {
State::Handshaking { hs, ping_config } => {
let mut conn = ready!(Pin::new(hs).poll(cx).map_err(crate::Error::new_h2))?;
let ping = if ping_config.is_enabled() {
let pp = conn.ping_pong().expect("conn.ping_pong");
Some(ping::channel(pp, ping_config.clone(), me.timer.clone()))
} else {
None
};
State::Serving(Serving {
ping,
conn,
closing: None,
date_header: me.date_header,
})
}
State::Serving(srv) => {
// graceful_shutdown was called before handshaking finished,
if me.close_pending && srv.closing.is_none() {
srv.conn.graceful_shutdown();
}
ready!(srv.poll_server(cx, &mut me.service, &mut me.exec))?;
return Poll::Ready(Ok(Dispatched::Shutdown));
}
};
握手完成时,只要开了 keep-alive 或自适应窗口,就从 h2 连接上取出 PingPong,建 ping 通道;这一步会调 timer.sleep,没给 timer 就在这里 panic(Executor 与 Timer 的实验 1)。close_pending 见本页最后一节。
poll_server:一个流一个任务
poll_server 是服务端的主循环:
if self.closing.is_none() {
loop {
self.poll_ping(cx);
match ready!(self.conn.poll_accept(cx)) {
Some(Ok((req, mut respond))) => {
trace!("incoming request");
let content_length = headers::content_length_parse_all(req.headers());
let ping = self
.ping
.as_ref()
.map_or_else(ping::disabled, |ping| ping.0.clone());
// Record the headers received
ping.record_non_data();
let is_connect = req.method() == Method::CONNECT;
let (mut parts, stream) = req.into_parts();
let (mut req, connect_parts) = if !is_connect {
(
Request::from_parts(
parts,
IncomingBody::h2(stream, content_length.into(), ping),
),
None,
)
} else {
// ... CONNECT:body 为空,extensions 里放 OnUpgrade
};
// ...
let fut = H2Stream::new(
service.call(req),
connect_parts,
respond,
self.date_header,
exec.clone(),
);
exec.execute_h2stream(fut);
}
Some(Err(e)) => {
return Poll::Ready(Err(crate::Error::new_h2(e)));
}
None => {
// no more incoming streams...
if let Some((ping, _)) = &self.ping {
ping.ensure_not_timed_out()?;
}
trace!("incoming connection complete");
return Poll::Ready(Ok(()));
}
}
}
}
每圈先 poll_ping(keep-alive 和 BDP 的结果在这里生效,见 ping),再向 h2 要下一个新流。h2 的 poll_accept 在内部推动整条连接:读 socket、解帧、把 DATA 分给各个流、把各个流排队的帧写出去。所以这个 Connection future 虽然只「收流」,却是整条连接 IO 的驱动者,必须一直被 poll。
拿到一个新流,hyper 做的事:
- 请求 body 包成
Incoming的 h2 变体(IncomingBody::h2),带上Content-Length解析出的长度和一个 pingRecorder。 - 调
service.call(req),同步地,就在连接任务里。 - 把返回的 future、h2 给的
SendResponse(respond)和一份执行器包成H2Stream,交给执行器。
poll_accept 返回 None 表示连接上不会再有新流(对端关了、GOAWAY 走完了、或者 keep-alive 超时后 hyper 自己关了连接),poll_server 正常返回,Connection 以 Ok(()) 结束;唯一的例外是 keep-alive 超时,ensure_not_timed_out 把它变成错误。
实验:任务归属与并发
和 Dispatcher 那页的 HTTP/1 实验一样,三个请求、服务每个睡 200ms,这次走 HTTP/2,客户端用 hyper 自己的 HTTP/2 客户端在同一条连接上并发发出。两边的执行器都换成 LogExec:它在每次 execute 时打印 future 的类型名和 tokio 分配的任务 id。服务端把「call 在哪个任务里」和「返回的 future 在哪个任务里被 poll」分开打印。
hyper = { version = "=1.12.0", features = ["server", "client", "http2"] }
hyper-util = { version = "=0.1.21", features = ["tokio"] }
http-body-util = "0.1"
tokio = { version = "=1.54.1", features = ["full"] }
use std::{convert::Infallible, future::Future, time::{Duration, Instant}};
use http_body_util::{BodyExt, Empty, Full};
use hyper::{body::{Bytes, Incoming}, client, rt::Executor, server, service::service_fn, Request, Response};
use hyper_util::rt::TokioIo;
use tokio::net::{TcpListener, TcpStream};
// An executor that logs the type of every future hyper hands it.
#[derive(Clone)]
struct LogExec(&'static str);
impl<F> Executor<F> for LogExec
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
fn execute(&self, fut: F) {
let name = std::any::type_name::<F>();
let short = name.split('<').next().unwrap().rsplit("::").next().unwrap();
let id = tokio::spawn(fut).id();
println!(" {} execute {short} -> task {id}", self.0);
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let start = Instant::now();
let ms = move || start.elapsed().as_millis();
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
println!("{:>4}ms server: connection task {}", ms(), tokio::task::id());
let svc = service_fn(move |req: Request<Incoming>| {
let path = req.uri().path().to_string();
println!("{:>4}ms server: call {path} in task {}", ms(), tokio::task::id());
async move {
println!("{:>4}ms server: future {path} polled in task {}", ms(), tokio::task::id());
tokio::time::sleep(Duration::from_millis(200)).await;
println!("{:>4}ms server: handler {path} done", ms());
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from(path))))
}
});
server::conn::http2::Builder::new(LogExec("server"))
.serve_connection(TokioIo::new(stream), svc)
.await
.unwrap();
});
let stream = TcpStream::connect(addr).await.unwrap();
let (sender, conn) = client::conn::http2::Builder::new(LogExec("client"))
.handshake(TokioIo::new(stream))
.await
.unwrap();
println!(" client: Connection future -> task {}", tokio::spawn(conn).id());
let mut tasks = Vec::new();
for path in ["/a", "/b", "/c"] {
let mut sender = sender.clone(); // SendRequest is Clone for HTTP/2
let task = tokio::spawn(async move {
let req = Request::get(format!("http://x{path}")).body(Empty::<Bytes>::new()).unwrap();
let res = sender.send_request(req).await.unwrap();
let body = res.into_body().collect().await.unwrap().to_bytes();
println!("{:>4}ms client: got {body:?}", ms());
});
println!(" client: request {path} -> task {}", task.id());
tasks.push(task);
}
for t in tasks {
t.await.unwrap();
}
}
M2 Max、rustc 1.99.0、--release,单线程运行时。连跑 3 次,除毫秒数差 1ms 以内外完全相同:
client execute H2ClientFuture -> task 2
client: Connection future -> task 3
client: request /a -> task 4
client: request /b -> task 5
client: request /c -> task 6
client execute H2ClientFuture -> task 7
client execute H2ClientFuture -> task 8
client execute H2ClientFuture -> task 9
1ms server: connection task 1
1ms server: call /a in task 1
server execute H2Stream -> task 10
1ms server: call /b in task 1
server execute H2Stream -> task 11
1ms server: call /c in task 1
server execute H2Stream -> task 12
1ms server: future /a polled in task 10
1ms server: future /b polled in task 11
1ms server: future /c polled in task 12
204ms server: handler /a done
204ms server: handler /b done
204ms server: handler /c done
204ms client: got b"/a"
205ms client: got b"/b"
205ms client: got b"/c"
服务端这一半:
- 连接 future 跑在任务 1。三个流的
call都在任务 1 里执行,每次call之后紧跟一次execute H2Stream,得到任务 10、11、12。 - 服务返回的 future 分别在任务 10、11、12 里第一次被 poll,三个
sleep同时开始,200ms 后同时结束。 - 三个请求一共约 200ms。HTTP/1 的同一个实验要 600ms:
/b要等/a的响应写完才被调用。
客户端那一半(任务 2 是握手时 spawn 的连接任务,任务 7、8、9 是每个请求一个的等响应任务)在 HTTP/2 客户端 里讲。
问: 每个流都有自己的任务了,那在服务里做一点同步的重活(解析大 JSON、算哈希)也不会影响别的请求吧?
答: 取决于活放在哪儿。service_fn 的闭包体、Service::call 的函数体,是在 poll_server 的循环里同步执行的,也就是在连接任务里:它跑多久,这条连接上的读、写、收新流就停多久,所有流一起等。只有 call 返回的 future 才在各自的 H2Stream 任务里被 poll。所以重活要放进 async 块里(或者交给 spawn_blocking),而 call 本身应该立即返回。tower 的服务经过 TowerToHyperService 适配后,call 里只有 clone 和构造 Oneshot(见 Service 与 Body),poll_ready 和 tower 服务自己的 call 都发生在 Oneshot 第一次被 poll 时,也就是在流任务里。
H2Stream:等响应,写响应头
H2Stream 是交给执行器的那个 future:
pub struct H2Stream<F, B, E>
where
B: Body,
{
reply: SendResponse<SendBuf<B::Data>>,
#[pin]
state: H2StreamState<F, B>,
date_header: bool,
exec: E,
}
state 先是 Service { fut, connect_parts },拿到响应后变成 Body { pipe }。poll2 的前一半:
let res = match h.poll(cx) {
Poll::Ready(Ok(r)) => r,
Poll::Pending => {
// Response is not yet ready, so we want to check if the client has sent a
// RST_STREAM frame which would cancel the current request.
if let Poll::Ready(reason) =
me.reply.poll_reset(cx).map_err(crate::Error::new_h2)?
{
debug!("stream received RST_STREAM: {:?}", reason);
return Poll::Ready(Err(crate::Error::new_h2(reason.into())));
}
return Poll::Pending;
}
Poll::Ready(Err(e)) => {
let err = crate::Error::new_user_service(e);
warn!("http2 service errored: {}", err);
me.reply.send_reset(err.h2_reason());
return Poll::Ready(Err(err));
}
};
let (head, body) = res.into_parts();
let mut res = ::http::Response::from_parts(head, ());
super::strip_connection_headers(
res.headers_mut(),
super::MessageKind::Response,
);
// set Date header if it isn't already set if instructed
if *me.date_header {
res.headers_mut()
.entry(::http::header::DATE)
.or_insert_with(date::update_and_header_value);
}
// ... CONNECT 的成功响应:交出升级后的流,见下文
if !body.is_end_stream() {
// automatically set Content-Length from body...
if let Some(len) = body.size_hint().exact() {
headers::set_content_length_if_missing(res.headers_mut(), len);
}
let body_tx = reply!(me, res, false);
H2StreamState::Body {
pipe: PipeToSendStream::new(body, body_tx),
}
} else {
reply!(me, res, true);
return Poll::Ready(Ok(()));
}
几处值得看:
- 服务 future 没好时,顺便盯着对端的
RST_STREAM。poll_reset注册了 waker,客户端取消请求时这个任务会被唤醒,返回Err,H2Stream结束,服务的 future 随之被 drop。服务里正在等的数据库查询、下游请求也就跟着被取消。客户端那边发生了什么、线上的帧长什么样,见 HTTP/2 客户端 的实验。 - 服务返回
Err,只重置这一个流。错误码是h2_reason:错误链里有h2::Error就用它的原因,否则是INTERNAL_ERROR。HTTP/1 里服务出错会让整条连接以错误结束(见 Dispatcher 的poll_write),HTTP/2 不会,连接继续服务别的流。H2Stream::poll把poll2的错误只记一条debug!就丢掉,它的Output是(),没人能收到这个错误。 - 连接级的头被删掉。
strip_connection_headers删keep-alive、proxy-connection、transfer-encoding、upgrade、响应里的te,以及connection头和它列出的头名,每删一个记一条warn!。HTTP/2 里这些信息由帧表达,RFC 9113 不允许消息里出现连接级的头。 - body 长度:
is_end_stream()为真就把 END_STREAM 标在 HEADERS 帧上,没有 DATA 帧;否则size_hint().exact()有值时补Content-Length(已经有就不动)。HTTP/2 没有 chunked,长度未知时什么也不加,靠 END_STREAM 结束。
实验:服务出错
为了看到线上的帧,后面几个实验都在客户端和服务端之间夹一个小代理 sniff.rs:原样转发字节,同时按 HTTP/2 帧头(3 字节长度、1 字节类型、1 字节标志、4 字节流 id)把指定类型的帧打出来,客户端方向先跳过 24 字节的连接前言。
// A one-connection TCP proxy that prints the HTTP/2 frame headers going
// through it. Only frame types listed in `show` are printed.
use std::{net::SocketAddr, sync::atomic::{AtomicUsize, Ordering}, time::Instant};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::{tcp::{OwnedReadHalf, OwnedWriteHalf}, TcpListener, TcpStream},
};
/// Number of PING frames (not ACKs) seen from the client to the server.
pub static CLIENT_PINGS: AtomicUsize = AtomicUsize::new(0);
const NAMES: [&str; 10] = [
"DATA", "HEADERS", "PRIORITY", "RST_STREAM", "SETTINGS",
"PUSH_PROMISE", "PING", "GOAWAY", "WINDOW_UPDATE", "CONTINUATION",
];
fn reason(code: u32) -> String {
match code {
0 => "NO_ERROR".into(),
2 => "INTERNAL_ERROR".into(),
7 => "REFUSED_STREAM".into(),
8 => "CANCEL".into(),
c => format!("code {c}"),
}
}
fn describe(kind: &str, flags: u8, payload: &[u8]) -> String {
let u32_at = |i: usize| u32::from_be_bytes(payload[i..i + 4].try_into().unwrap());
match kind {
"DATA" => format!("len={}{}", payload.len(), if flags & 1 != 0 { " END_STREAM" } else { "" }),
"HEADERS" => (if flags & 1 != 0 { "END_STREAM" } else { "" }).into(),
"RST_STREAM" => reason(u32_at(0)),
"GOAWAY" => format!("last_stream={} {}", u32_at(0) & 0x7fff_ffff, reason(u32_at(4))),
"PING" | "SETTINGS" if flags & 1 != 0 => "ACK".into(),
"SETTINGS" => payload
.chunks(6)
.map(|p| format!("{}={}", u16::from_be_bytes([p[0], p[1]]), u32::from_be_bytes([p[2], p[3], p[4], p[5]])))
.collect::<Vec<_>>()
.join(" "),
"WINDOW_UPDATE" => format!("+{}", u32_at(0)),
_ => String::new(),
}
}
async fn pump(mut from: OwnedReadHalf, mut to: OwnedWriteHalf, dir: &'static str, start: Instant, show: &'static [&'static str]) {
let mut buf = Vec::new();
let mut skip = if dir == "c->s" { 24 } else { 0 }; // client connection preface
let mut chunk = [0u8; 16384];
loop {
let n = match from.read(&mut chunk).await {
Ok(0) | Err(_) => {
let _ = to.shutdown().await;
if show.contains(&"EOF") {
println!("{:>5}ms {dir} EOF", start.elapsed().as_millis());
}
return;
}
Ok(n) => n,
};
if to.write_all(&chunk[..n]).await.is_err() {
return;
}
buf.extend_from_slice(&chunk[..n]);
if skip > 0 {
let k = skip.min(buf.len());
buf.drain(..k);
skip -= k;
}
while buf.len() >= 9 {
let len = u32::from_be_bytes([0, buf[0], buf[1], buf[2]]) as usize;
if buf.len() < 9 + len {
break;
}
let kind = NAMES.get(buf[3] as usize).copied().unwrap_or("UNKNOWN");
let flags = buf[4];
let stream = u32::from_be_bytes([buf[5], buf[6], buf[7], buf[8]]) & 0x7fff_ffff;
if kind == "PING" && flags & 1 == 0 && dir == "c->s" {
CLIENT_PINGS.fetch_add(1, Ordering::SeqCst);
}
if show.contains(&kind) {
let d = describe(kind, flags, &buf[9..9 + len]);
println!("{:>5}ms {dir} {kind} stream={stream} {d}", start.elapsed().as_millis());
}
buf.drain(..9 + len);
}
}
}
/// Listens on 127.0.0.1:0, forwards the first accepted connection to
/// `upstream`, and returns the proxy's address.
pub async fn proxy(upstream: SocketAddr, start: Instant, show: &'static [&'static str]) -> SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let (client, _) = listener.accept().await.unwrap();
let server = TcpStream::connect(upstream).await.unwrap();
let (cr, cw) = client.into_split();
let (sr, sw) = server.into_split();
tokio::spawn(pump(cr, sw, "c->s", start, show));
tokio::spawn(pump(sr, cw, "s->c", start, show));
});
addr
}
(CLIENT_PINGS 给 ping 那页的实验计数用。)依赖和上一个实验相同。服务对 /err 返回 Err,客户端先请求 /err 再请求 /ok:
use std::time::Instant;
use http_body_util::Empty;
use hyper::{body::{Bytes, Incoming}, client, server, service::service_fn, Request, Response};
use hyper_util::rt::{TokioExecutor, TokioIo};
use tokio::net::{TcpListener, TcpStream};
#[path = "../sniff.rs"]
mod sniff;
#[tokio::main(flavor = "current_thread")]
async fn main() {
let start = Instant::now();
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let proxy = sniff::proxy(listener.local_addr().unwrap(), start, &["HEADERS", "RST_STREAM", "GOAWAY"]).await;
tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let svc = service_fn(|req: Request<Incoming>| async move {
if req.uri().path() == "/err" {
return Err("handler failed");
}
Ok(Response::new(Empty::<Bytes>::new()))
});
let r = server::conn::http2::Builder::new(TokioExecutor::new())
.serve_connection(TokioIo::new(stream), svc)
.await;
println!("server conn: {r:?}");
});
let stream = TcpStream::connect(proxy).await.unwrap();
let (mut sender, conn) = client::conn::http2::handshake(TokioExecutor::new(), TokioIo::new(stream))
.await
.unwrap();
tokio::spawn(conn);
for path in ["/err", "/ok"] {
let req = Request::get(format!("http://x{path}")).body(Empty::<Bytes>::new()).unwrap();
match sender.send_request(req).await {
Ok(res) => println!("client: {path} -> {}", res.status()),
Err(e) => println!("client: {path} -> Err({e}: {})", std::error::Error::source(&e).unwrap()),
}
}
}
连跑 3 次,除毫秒数外相同:
1ms c->s HEADERS stream=1 END_STREAM
1ms s->c RST_STREAM stream=1 INTERNAL_ERROR
client: /err -> Err(http2 error: stream error received: unexpected internal error encountered)
2ms c->s HEADERS stream=3 END_STREAM
2ms s->c HEADERS stream=3 END_STREAM
client: /ok -> 200 OK
服务的错误在线上只是流 1 上的一个 RST_STREAM(INTERNAL_ERROR),错误内容(handler failed)不会发给客户端。流 3 照常得到 200,没有 GOAWAY,server conn 那行没有打印,因为程序结束时服务端连接还活着。/ok 的空 body 让响应只有一个带 END_STREAM 的 HEADERS 帧。
PipeToSendStream:按流控容量写 body
响应 body 不为空时,H2Stream 进入 Body 状态,后面的事全交给 PipeToSendStream。客户端发请求 body 用的也是它,所以它在 mod.rs 里而不在 server.rs 里。
pub(crate) struct PipeToSendStream<S>
where
S: Body,
{
body_tx: SendStream<SendBuf<S::Data>>,
data_done: bool,
// A data chunk that has been polled from the body but is still waiting
// for stream-level capacity before it can be shipped. Stored here so
// it survives across `Poll::Pending` returns from `poll_capacity`; if
// we left the chunk in a local, it would be dropped on every repoll.
buffered_data: Option<Peeked<S::Data>>,
#[pin]
stream: S,
}
HTTP/2 的发送受两层窗口限制:每个流一个,整条连接一个,都由对端用 WINDOW_UPDATE 放大。h2 把这个约束暴露成「容量」:reserve_capacity(n) 表示想发 n 字节,poll_capacity 等 h2 从窗口里分到容量,send_data 把数据交给 h2 排队。poll 每圈的顺序:
poll_reset:对端重置了这个流就停下,返回错误。等数据、等容量时都注册着这个 waker。- 手里有一块等容量的数据(
buffered_data):capacity()为 0 就poll_capacity,分到容量后send_data。 - 否则
poll_frame向 body 要下一帧:
match ready!(me.stream.as_mut().poll_frame(cx)) {
Some(Ok(frame)) => {
if frame.is_data() {
let chunk = frame.into_data().unwrap_or_else(|_| unreachable!());
let is_eos = me.stream.is_end_stream();
let len = chunk.remaining();
// ...
if len == 0 {
// ... 空数据帧不需要容量,直接 send_data
}
// ...
me.body_tx.reserve_capacity(1);
*me.buffered_data = Some(Peeked {
data: chunk,
is_eos,
});
} else if frame.is_trailers() {
// no more DATA, so give any capacity back
me.body_tx.reserve_capacity(0);
me.body_tx
.send_trailers(frame.into_trailers().unwrap_or_else(|_| unreachable!()))
.map_err(crate::Error::new_body_write)?;
return Poll::Ready(Ok(()));
} else {
trace!("discarding unknown frame");
// loop again
}
}
Some(Err(e)) => return Poll::Ready(Err(me.body_tx.on_user_err(e))),
None => {
// no more frames means we're done here
// but at this point, we haven't sent an EOS DATA, or
// any trailers, so send an empty EOS DATA.
return Poll::Ready(me.body_tx.send_eos_frame());
}
}
- 先拿到数据,再要容量,而且只要 1 字节。h2 在
send_data时会把请求的容量提高到实际缓冲的长度,所以对端最终看到的需求不变;下面的冷知识讲为什么是这个顺序、这个数。 - body 出错:
on_user_err用h2_reason给这个流发RST_STREAM,对端看到的是流被重置,不是一个截断的 body。 - body 结束但最后一帧没带结束标记:补一个空的、带 END_STREAM 的 DATA 帧。
背压来自两个方向。发送方向是这里:对端不放大窗口,poll_capacity 一直 Pending,PipeToSendStream 不再 poll_frame,响应 body 也就不再被拉取。接收方向在 Incoming 里:h2 变体每交出一块数据才对 h2 调一次 release_capacity,服务不读请求 body,窗口就不会被放大,对端最多发完一个窗口就得停(见 Incoming 与 body 通道)。HTTP/1 靠的是「不读 socket」,这里靠的是 HTTP/2 自己的流控,同一条连接上其他流不受影响,只要连接级窗口还有余量。
#4003(issue)报告生产环境里偶发死锁:一条 HTTP/2 连接发出「exactly 65534 bytes of body data」之后,第二个请求发不出去。原因是 PipeToSendStream「always keeps reserved one byte of flow control capacity while its waiting for more data from the body」:当时每圈一开始就 reserve_capacity(1),再去问 body 要数据。body 暂时没数据时,这 1 字节一直从连接级窗口里占着;对端如果只在窗口完全耗尽时才发 WINDOW_UPDATE(issue 里举的是 Bun 的 HTTP/2 服务端),它眼里窗口还剩 1 字节,永远不会放大,第二个流就一个字节也拿不到。
#4061(v1.10.0,「do not reserve capacity before body data is available」)把顺序倒过来:先 poll_frame,拿到数据块后再按数据块的长度预留,数据块存进新加的 buffered_data,「so it survives the poll_capacity wait across Poll::Pending returns」。
#4149(v1.11.1,「reserve minimal send capacity when piping request bodies」)又把预留量从整块数据改回 1 字节。PR 说按整块预留在一个压测环境里带来「a ~10% increase in CPU time per request and a ~12% drop in throughput」,只在连接上的流加起来要的容量超过对端窗口时出现;死锁的修复不受影响,因为「The reservation is still made only once the chunk is in hand」。上面代码里的 reserve_capacity(1) 和 buffered_data 就是这两次改动的结果。
CONNECT:流变成 IO
HTTP/2 没有 Upgrade 头,隧道用 CONNECT 方法在一个流上建立(RFC 8441 的扩展 CONNECT 要 enable_connect_protocol() 打开,:protocol 伪头以 hyper::ext::Protocol 出现在 extensions 里)。poll_server 对所有 CONNECT 请求都放一个 OnUpgrade,请求 body 给空的 Incoming,带非零 Content-Length 的 CONNECT 直接 RST_STREAM(INTERNAL_ERROR)。服务回 2xx 时,H2Stream 删掉 Content-Length、写响应头(不带 END_STREAM),然后把 h2 的 SendStream 和 RecvStream 拼成一个 IO(proto/h2/upgrade.rs),装进 Upgraded 交给等待者,同时 execute_upgrade 再 spawn 一个任务。HTTP/1 要显式 with_upgrades() 才会交出 IO(见 协议升级),HTTP/2 不用,因为交出去的只是一个流,连接还在 hyper 手里。
graceful_shutdown:两次 GOAWAY
Connection::graceful_shutdown 转到 Server::graceful_shutdown:
pub(crate) fn graceful_shutdown(&mut self) {
trace!("graceful_shutdown");
match &mut self.state {
State::Handshaking { .. } => {
self.close_pending = true;
}
State::Serving(srv) => {
if srv.closing.is_none() {
srv.conn.graceful_shutdown();
}
}
}
}
握手还没完成时只记一个 close_pending,等进入 Serving 后的第一次 poll 再真正开始(前面 poll 代码里那个分支)。这是 v1.5.1 的 #3729 改的,提交说明写的是之前「the connection would just be dropped instantly」,握手期间已经在路上的请求跟着丢了。
真正的关闭流程在 h2 里。h2 0.4.20 的 graceful_shutdown 照 RFC 9113 的建议,先发一个 last_stream_id = 2^31-1、NO_ERROR 的 GOAWAY,告诉客户端别再开新流;再发一个 PING,收到 PONG(即至少过了一个往返,路上的新流都已到达)后发第二个 GOAWAY,带上真实的最后一个流 id。之后 poll_accept 不再给出新流,已有的流照常跑完,全部结束后连接关闭,poll_accept 返回 None,Connection 以 Ok(()) 完成。和 HTTP/1 一样,graceful_shutdown 只改状态,之后必须继续 poll Connection。
实验:在途的请求和迟到的请求
服务每个请求睡 300ms。客户端在 1ms 发 /first;服务端在 100ms 调 graceful_shutdown;客户端在 200ms 用同一个 SendRequest 再发 /late。依赖同上。
use std::{convert::Infallible, pin::pin, time::{Duration, Instant}};
use http_body_util::{Empty, Full};
use hyper::{body::{Bytes, Incoming}, client, server, service::service_fn, Request, Response};
use hyper_util::rt::{TokioExecutor, TokioIo};
use tokio::net::{TcpListener, TcpStream};
#[path = "../sniff.rs"]
mod sniff;
#[tokio::main(flavor = "current_thread")]
async fn main() {
let start = Instant::now();
let ms = move || start.elapsed().as_millis();
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let show = &["HEADERS", "DATA", "RST_STREAM", "GOAWAY", "PING", "EOF"];
let proxy = sniff::proxy(listener.local_addr().unwrap(), start, show).await;
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let svc = service_fn(move |req: Request<Incoming>| async move {
let path = req.uri().path().to_string();
println!("{:>5}ms server: handler {path} start", ms());
tokio::time::sleep(Duration::from_millis(300)).await;
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from(path))))
});
let mut conn = pin!(server::conn::http2::Builder::new(TokioExecutor::new())
.serve_connection(TokioIo::new(stream), svc));
tokio::select! {
r = conn.as_mut() => return r,
_ = tokio::time::sleep(Duration::from_millis(100)) => {}
}
println!("{:>5}ms server: graceful_shutdown()", ms());
conn.as_mut().graceful_shutdown();
conn.await
});
let stream = TcpStream::connect(proxy).await.unwrap();
let (mut sender, conn) = client::conn::http2::handshake(TokioExecutor::new(), TokioIo::new(stream))
.await
.unwrap();
let client_conn = tokio::spawn(async move {
let r = conn.await;
println!("{:>5}ms client conn: {r:?}", ms());
});
let req = |p: &str| Request::get(format!("http://x{p}")).body(Empty::<Bytes>::new()).unwrap();
let mut s1 = sender.clone();
let first = tokio::spawn(async move { s1.send_request(req("/first")).await.map(|r| r.status()) });
tokio::time::sleep(Duration::from_millis(200)).await;
println!("{:>5}ms client: is_closed={}", ms(), sender.is_closed());
let late = sender.send_request(req("/late")).await;
println!("{:>5}ms client: /late -> {late:?}", ms());
let first = first.await.unwrap();
println!("{:>5}ms client: /first -> {first:?}", ms());
let server = server.await.unwrap();
println!("{:>5}ms server conn: {server:?}", ms());
client_conn.await.unwrap();
}
连跑 3 次,顺序相同,毫秒数差 2ms 以内:
1ms c->s HEADERS stream=1 END_STREAM
1ms server: handler /first start
104ms server: graceful_shutdown()
104ms s->c GOAWAY stream=0 last_stream=2147483647 NO_ERROR
104ms s->c PING stream=0
104ms c->s PING stream=0 ACK
104ms s->c GOAWAY stream=0 last_stream=1 NO_ERROR
202ms client: is_closed=false
202ms client conn: Ok(())
202ms client: /late -> Err(hyper::Error(Canceled, "connection closed"))
304ms s->c HEADERS stream=1
304ms s->c DATA stream=1 len=6 END_STREAM
304ms s->c EOF
304ms client: /first -> Ok(200)
304ms server conn: Ok(())
- 104ms 的四个帧就是上面说的两阶段:GOAWAY(2147483647)、PING、客户端的 PING ACK、GOAWAY(1)。本地回环上一个往返不到 1ms,两个 GOAWAY 几乎同时出现。
/first(流 1)不受影响,300ms 后照常拿到 200。最后一个流结束后服务端关掉连接(s->c EOF),Connection以Ok(())结束。/late根本没上线:线上没有流 3 的 HEADERS。客户端在 200ms 时is_closed()还是false,发出/late后它的连接 future 才发现连接已经不接受新流,以Ok(())结束,/late收到Canceled错误。这一段为什么是这样,见 HTTP/2 客户端 的「收到 GOAWAY 之后」。
hyper-util 的 GracefulShutdown 对 HTTP/2 连接调用的就是这个方法,「什么时候调、怎么等所有连接结束」在 优雅停机 里。keep-alive 超时走的是另一条路:hyper 调 h2 的 abrupt_shutdown(NO_ERROR),只发一个 GOAWAY 就关,见 ping。
小结
http2::Builder必须带执行器;serve_connection只组装proto::h2::Server,不 spawn。默认窗口 1 MiB、最大并发流 200,默认值不算稳定 API。Server先握手(IO 经Compat转成 tokio 的 IO trait 交给 h2),再在poll_server里循环poll_accept。这个 future 驱动整条连接的 IO,必须一直 poll。- 每个新流:
service.call在连接任务里同步执行,返回的 future 包成H2Stream交给执行器。实验里三个 200ms 的请求一共 200ms,HTTP/1 要 600ms。call本身要快,重活放进它返回的 future。 H2Stream等服务 future 时监听RST_STREAM;服务返回Err只给这个流发RST_STREAM(默认INTERNAL_ERROR),连接继续服务别的流。PipeToSendStream先从 body 拿到数据,再向 h2 要 1 字节容量、等分配、send_data;这个顺序是修死锁(#4003)的结果。graceful_shutdown交给 h2:GOAWAY(2^31-1) → PING → GOAWAY(最后流 id),在途的流跑完再关;握手中调用会推迟到握手完成。