Executor 与 Timer
主线位置:两条主线的旁路。HTTP/1 服务端主线上只有一处用到运行时:读请求头的超时要一个
Timer。HTTP/2 的两条主线都要Executor,每个流、客户端的连接任务都是 spawn 出去的;HTTP/2 的 keep-alive ping 要Timer。
flowchart TB
need["hyper 需要计时"] --> which{"Builder 上有 timer?"}
which -->|有| ok["Timer::sleep 等"]
which -->|没有| empty["Time::Empty"]
empty -->|"now()"| inst["退回 Instant::now"]
empty -->|"默认的超时"| off["静默不生效"]
empty -->|"显式配置的超时"| p1["serve_connection 时 panic"]
empty -->|"sleep / reset"| p2["You must supply a timer."]
hyper 没有自己的运行时,也不假定你用 tokio。它把「起一个后台任务」和「睡一段时间」都写成 trait,由使用者在 Builder 上交进来。交不交是可选的,所以 hyper 必须决定「没交的时候怎么办」,这一页的后半部分就是这个决定。
Executor:只有一个方法
pub trait Executor<Fut> {
/// Place the future into the executor to be run.
fn execute(&self, fut: Fut);
}
没有返回值,没有 JoinHandle,hyper 拿不到任务的句柄,也就不能通过执行器等它结束或取消它。泛型参数在 trait 上而不在方法上,所以一个执行器可以只对某几种 future 实现,比如只接受 Send 的。hyper-util 的 TokioExecutor 就是这么做的,实现要求 Send + 'static:
impl<Fut> Executor<Fut> for TokioExecutor
where
Fut: Future + Send + 'static,
Fut::Output: Send + 'static,
{
fn execute(&self, fut: Fut) {
#[cfg(feature = "rt-tracing-exec-force")]
tokio::spawn(fut.in_current_span());
#[cfg(not(feature = "rt-tracing-exec-force"))]
tokio::spawn(fut);
}
}
就是一个 tokio::spawn(spawn 之后发生什么见 spawn 全路径)。rt-tracing-exec-force 是一个临时特性,让 spawn 出去的任务继承当前的 tracing span,不在 full 里。0.1.21 之前不需要它:0.1.11 到 0.1.20,只要开了 tracing feature(full 里就有),TokioExecutor 就这样做;0.1.21 把它改成了可选的包装执行器,见 rt::tracing。
hyper 里调用 execute 的地方全在 proto/h2:
| 位置 | spawn 的 future | 什么时候 |
|---|---|---|
服务端 poll_server 里的 exec.execute_h2stream(fut) | H2Stream:调用 service.call(req) 得到的 future,加上回写响应 | 每收到一个新流 |
客户端 handshake 里的 exec.execute_h2_future | H2ClientFuture::Task:驱动整条 h2 连接 | 握手完成时,一次 |
客户端 poll_pipe 里的 execute_h2_future | H2ClientFuture::Pipe:把请求 body 灌进流 | 请求 body 没有一次发完时 |
同一个函数末尾的 execute_h2_future | H2ClientFuture::Send:等响应头,交回调用方 | 每个请求 |
| 两端的 CONNECT 升级 | UpgradedSendStreamTask | 升级后的流 |
HTTP/1 一处都没有。http1::Builder 连放执行器的字段都没有:一条 HTTP/1 连接的所有请求都在 Connection 这一个 future 里处理,由你自己 spawn 或 .await,见 服务端 Builder 与 Connection。HTTP/2 每个流一个任务,这是它能在一条连接上并发处理请求的原因,代价是这些 future 要满足执行器的约束,用 TokioExecutor 就得是 Send。HTTP/2 这一侧的细节见 HTTP/2 服务端 和 HTTP/2 客户端。
rt::bounds:用户写不出的约束
问题在于 H2Stream 这些类型是 hyper 的私有类型(proto 是私有模块),用户写不出 E: Executor<H2Stream<F, B, E>>。可是 hyper-util 这类库要写泛型代码转发执行器,必须能表达「这个执行器能跑 hyper 的 HTTP/2 任务」。rt::bounds 给了一个能写出名字的别名:
pub trait Http2ServerConnExec<F, B: Body>:
super::Http2UpgradedExec<B::Data> + sealed::Sealed<(F, B)> + Clone
{
#[doc(hidden)]
fn execute_h2stream(&mut self, fut: H2Stream<F, B, Self>);
}
#[doc(hidden)]
impl<E, F, B> Http2ServerConnExec<F, B> for E
where
E: Clone,
E: Executor<H2Stream<F, B, E>>,
E: super::Http2UpgradedExec<B::Data>,
H2Stream<F, B, E>: Future<Output = ()>,
B: Body,
{
fn execute_h2stream(&mut self, fut: H2Stream<F, B, E>) {
self.execute(fut);
}
}
三点:
- 只能靠 blanket impl 得到。超 trait 里有一个私有模块里的
Sealed,外部 crate 实现不了它,也就没法手写Http2ServerConnExec。你只实现Executor,别名自动成立。 - 参数是用户能写的类型。
F是服务的 future,B是响应 body,http2::Builder::serve_connection的约束就写成E: Http2ServerConnExec<S::Future, Bd>。hyper-util 的graceful.rs、auto里转发的也是这个。 - 要求
Clone。每个H2Stream里要带一份执行器(升级时还要再 spawn),所以执行器得能复制。
客户端对应的是 Http2ClientConnExec,结构一样。
Timer 与 Sleep
pub trait Timer {
/// Return a future that resolves in `duration` time.
fn sleep(&self, duration: Duration) -> Pin<Box<dyn Sleep>>;
/// Return a future that resolves at `deadline`.
fn sleep_until(&self, deadline: Instant) -> Pin<Box<dyn Sleep>>;
/// Return an `Instant` representing the current time.
///
/// The default implementation returns [`Instant::now()`].
fn now(&self) -> Instant {
Instant::now()
}
/// Reset a future to resolve at `new_deadline` instead.
fn reset(&self, sleep: &mut Pin<Box<dyn Sleep>>, new_deadline: Instant) {
*sleep = self.sleep_until(new_deadline);
}
}
返回的都是 Pin<Box<dyn Sleep>>:hyper 在 Builder 里把 timer 存成 Arc<dyn Timer + Send + Sync>,trait 对象的方法没法返回 impl Future,只能装箱。now 是 1.8.0 加的(#3965),起因是 #3950:tokio 开了暂停时间(test-util 的 pause)以后,std::time::Instant 和 tokio 的时钟会分叉。默认实现还是 Instant::now(),TokioTimer::now 则返回 tokio::time::Instant::now()。reset 的默认实现是新建一个 Sleep 覆盖旧的,能用但浪费:tokio 的 Sleep 本来可以原地改截止时间(见 Sleep 的一生)。
要原地改,reset 拿到 dyn Sleep 时必须先确认它真是自己造的那种 Sleep。Sleep 为此带了一个类型 id:
pub trait Sleep: Send + Sync + Future<Output = ()> {
#[doc(hidden)]
/// This method is private and can not be implemented by downstream crates.
fn __type_id(&self, _: private::Sealed) -> TypeId
where
Self: 'static,
{
TypeId::of::<Self>()
}
}
参数类型 private::Sealed 外部拿不到,所以外部实现只能用默认的方法体,返回的一定是自己真实的 TypeId,没法撒谎。impl dyn Sleep 上的 is::<T>() 和 downcast_mut_pin::<T>() 就是照着 std::any::Any 重写的一份:比较 TypeId,相同就把 Pin<&mut dyn Sleep> 转成 Pin<&mut T>。
TokioTimer::reset 用的就是它:
fn reset(&self, sleep: &mut Pin<Box<dyn Sleep>>, new_deadline: Instant) {
if let Some(sleep) = sleep.as_mut().downcast_mut_pin::<TokioSleep>() {
sleep.reset(new_deadline)
}
}
TokioSleep 是包着 tokio::time::Sleep 的一层 pin_project,下转型成功就调 tokio 的 Sleep::reset 原地改。不是 TokioSleep 时什么也不做,不报错。
Sleep 的类型 id 是为了让 reset 能安全地优化#3027(issue,标题「Timer::reset can't soundly have an optimized implementation」)指出,1.0.0-rc.1 的 Timer::reset 默认实现「simply creates a new Sleep and writes it over the old one」,而重写它的实现拿到的是 dyn Sleep,「Sleep has no downcasting APIs, there is no safe way to confirm that the Sleep instance is one created by this Timer implementation」,结论是「the current default implementation is the only sound way of implementing the method」。维护者在 issue 里回复「we can't just say trait Sleep: Any」,提出的第一个方案就是把 Any 的方法抄进 Sleep。
#3125(「This pr adds a downcast for the Sleep trait」)照这个思路加上了 __type_id 和 downcast_mut_pin,第一个包含它的发布是 v1.0.0-rc.4。之后 v1.2.0 又修了一次(提交 7206fe30,「Sleep::downcast_mut_pin() no longer extend lifetime」)。上面 TokioTimer::reset 里的 downcast_mut_pin::<TokioSleep>() 就是这次改动的用户。
没有 timer 时:Time::Empty
Builder 里存 timer 的字段不是 Option,而是 crate 私有的 Time:
pub(crate) enum Time {
Timer(Arc<dyn Timer + Send + Sync>),
Empty,
}
http1::Builder::new()、http2::Builder::new() 都从 Time::Empty 开始,调了 .timer(...) 才变成 Time::Timer。hyper 内部所有计时都经过 Time 的方法,每个方法对 Empty 的处理不同:
| 方法 | Time::Empty 时 | 谁会调用 |
|---|---|---|
now | 退回 Instant::now() | HTTP/1 算读头截止时间、HTTP/2 ping 记时间 |
sleep | panic!("You must supply a timer.") | HTTP/2 keep-alive(ping::channel 里 timer.sleep(interval)) |
sleep_until | 同上 | HTTP/1 服务端的读头超时 |
reset | 同上 | 两者重置计时 |
check | 见下文 | http1::Builder::serve_connection |
所以 HTTP/2 会不会 panic,取决于有没有走到 sleep。ping::channel 只在开了 keep-alive 或自适应窗口(BDP)时才建,而 BDP 只用 now(),不 sleep。下面的程序把四种情况各跑一遍:
hyper = { version = "=1.12.0", features = ["server", "client", "http2"] }
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 std::sync::Mutex;
use std::time::Duration;
use bytes::Bytes;
use http_body_util::{Empty, Full};
use hyper::{client, server, service::service_fn, Request, Response};
use hyper_util::rt::{TokioExecutor, TokioIo, TokioTimer};
use tokio::net::{TcpListener, TcpStream};
static PANIC_MSG: Mutex<Option<String>> = Mutex::new(None);
async fn hello(_req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
Ok(Response::new(Full::new(Bytes::from_static(b"ok"))))
}
fn report<T: std::fmt::Debug, E: std::fmt::Display>(r: Result<Result<T, String>, E>, is_panic: bool) -> String {
match r {
Ok(Ok(v)) => format!("Ok({v:?})"),
Ok(Err(e)) => format!("Err({e})"),
Err(e) if is_panic => format!("panic: {}", PANIC_MSG.lock().unwrap().take().unwrap_or(e.to_string())),
Err(e) => format!("join error: {e}"),
}
}
async fn run_case(name: &str, srv: server::conn::http2::Builder<TokioExecutor>, cli: client::conn::http2::Builder<TokioExecutor>) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
srv.serve_connection(TokioIo::new(stream), service_fn(hello)).await.map_err(|e| e.to_string())
});
let client = tokio::spawn(async move {
let stream = TcpStream::connect(addr).await.unwrap();
let (mut tx, conn) = cli.handshake(TokioIo::new(stream)).await.map_err(|e| e.to_string())?;
let conn_task = tokio::spawn(conn);
let req = Request::get("http://x/").body(Empty::<Bytes>::new()).unwrap();
let res = tx.send_request(req).await.map_err(|e| e.to_string())?;
drop(tx);
let _ = conn_task.await;
Ok::<_, String>(res.status())
});
let c = client.await;
let c_panic = c.as_ref().err().is_some_and(|e| e.is_panic());
let c = report(c, c_panic);
let s = tokio::time::timeout(Duration::from_secs(3), server).await;
let s = match s {
Ok(r) => {
let p = r.as_ref().err().is_some_and(|e| e.is_panic());
report(r, p)
}
Err(_) => "still running after 3s".to_string(),
};
println!("{name}\n client: {c}\n server: {s}");
}
#[tokio::main]
async fn main() {
std::panic::set_hook(Box::new(|info| {
let loc = info.location().map(|l| {
let file = l.file();
let short = file.find("hyper-1.12.0/").map_or(file, |i| &file[i..]);
format!("{short}:{}", l.line())
});
let msg = info
.payload()
.downcast_ref::<String>()
.cloned()
.or_else(|| info.payload().downcast_ref::<&str>().map(|s| s.to_string()))
.unwrap_or_default();
*PANIC_MSG.lock().unwrap() = Some(format!("{msg} at {}", loc.unwrap_or_default()));
}));
let exec = TokioExecutor::new();
let plain_cli = || client::conn::http2::Builder::new(TokioExecutor::new());
let plain_srv = || server::conn::http2::Builder::new(TokioExecutor::new());
let mut s = plain_srv();
s.keep_alive_interval(Duration::from_secs(1));
run_case("1 server keep_alive_interval, no timer", s, plain_cli()).await;
let mut s = plain_srv();
s.adaptive_window(true);
run_case("2 server adaptive_window, no timer", s, plain_cli()).await;
let mut c = client::conn::http2::Builder::new(exec.clone());
c.keep_alive_interval(Duration::from_secs(1));
run_case("3 client keep_alive_interval, no timer", plain_srv(), c).await;
let mut s = plain_srv();
s.keep_alive_interval(Duration::from_secs(1)).timer(TokioTimer::new());
run_case("4 server keep_alive_interval + TokioTimer", s, plain_cli()).await;
}
M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:
1 server keep_alive_interval, no timer
client: Err(connection error)
server: panic: You must supply a timer. at hyper-1.12.0/src/common/time.rs:38
2 server adaptive_window, no timer
client: Ok(200)
server: Ok(())
3 client keep_alive_interval, no timer
client: panic: You must supply a timer. at hyper-1.12.0/src/common/time.rs:38
server: Ok(())
4 server keep_alive_interval + TokioTimer
client: Ok(200)
server: Ok(())
- 1:服务端握手完成、建 ping 通道时
sleep在time.rs第 38 行 panic。panic 发生在 pollConnection的任务里,这个任务死了,socket 随之关闭,客户端看到的只是connection error。 - 2:只开自适应窗口,没有
sleep,正常完成。 - 3:客户端的
handshake同样建 ping 通道,panic 在调用handshake().await的任务里。 - 4:给了
TokioTimer,一切正常。
HTTP/1:默认值和显式配置分开处理
HTTP/1 服务端只有 header_read_timeout 一个超时。它的值不是 Option<Duration>,而是带来源的 Dur:
pub(crate) enum Dur {
Default(Option<Duration>),
Configured(Option<Duration>),
}
Builder::new() 里是 Dur::Default(Some(Duration::from_secs(30))),调了 header_read_timeout(...) 就变成 Dur::Configured(...)。serve_connection 把它交给 Time::check:
pub(crate) fn check(&self, dur: Dur, name: &'static str) -> Option<Duration> {
match dur {
Dur::Default(Some(dur)) => match self {
Time::Empty => {
warn!("timeout `{}` has default, but no timer set", name,);
None
}
Time::Timer(..) => Some(dur),
},
Dur::Configured(Some(dur)) => match self {
#[allow(clippy::panic)]
Time::Empty => panic!("timeout `{name}` set, but no timer set"),
Time::Timer(..) => Some(dur),
},
Dur::Default(None) | Dur::Configured(None) => None,
}
}
返回 None 时 serve_connection 就不给连接设超时,后面的 sleep_until 永远不会被调用,所以 HTTP/1 不会出现 You must supply a timer.。四种组合:
| 超时来源 | 有 timer | 没 timer |
|---|---|---|
| 默认(30 秒) | 生效 | 不生效,只有一条 warn! |
显式 Some(d) | 生效 | serve_connection 时 panic:timeout header_read_timeout set, but no timer set |
显式 None | 关闭 | 关闭 |
warn! 是 hyper 自己的宏(src/trace.rs),只有开了 tracing 特性才展开成 tracing::warn!,而 tracing 特性又要求 --cfg hyper_unstable_tracing。普通构建里这条警告不存在,默认超时就这样无声无息地没了。hyper-util 的 auto::Builder::new 也只是 http1::Builder::new(),同样不设 timer;要读头超时,得自己在 Builder 上调 .timer(TokioTimer::new())。这在实际部署里是什么样子,见常见坑。
#3393(issue,标题「Decide what to do about a missing Timer」,正文为空)在 2023-11-06 提出,同一天的提交 f3308c04「feat(lib): missing Timer will warn or panic」关掉了它,提交信息是:「If a default timeout is set, and no Timer, a warning will be emitted. If a timeout is configured by the user, and no Timer is set, it will panic.」上面的 Dur 枚举和 check 就是这次加的。
在那之前,header_read_timeout 是一个普通的 Option<Duration>,默认 None;设了值却没给 timer,要等到第一次读请求头,Time::sleep_until 才在连接任务里 panic,信息正是 You must supply a timer.。改动之后 panic 提前到 serve_connection,信息里带上了选项名。
几个小时后的提交 8bf26d1e「feat(server): default http1 header_read_timeout to 30 seconds」把默认值改成 Dur::Default(Some(30 秒))。两个提交都第一次出现在 v1.0.0。顺序很重要:如果先改默认值,所有没设 timer 的用户在升级后都会 panic;有了「默认值缺 timer 只警告」,30 秒才能安全地成为默认。
超时在连接上从什么时候开始算、到期后连接怎么关,属于 Conn 的读状态,见 Conn 的读写状态机。
服务端用 http1::Builder 的六种配置,客户端各发一段永远不结束的请求头(E 除外,它发一个完整请求,然后什么也不发),最多等 35 秒。
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 std::sync::Mutex;
use std::time::{Duration, Instant};
use bytes::Bytes;
use http_body_util::Full;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response};
use hyper_util::rt::{TokioIo, TokioTimer};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
static PANIC_MSG: Mutex<Option<String>> = Mutex::new(None);
async fn hello(_req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
Ok(Response::new(Full::new(Bytes::from_static(b"ok"))))
}
// One server connection + one raw TCP client. Returns a one-line report.
async fn run_case(name: &str, builder: http1::Builder, send: &'static [u8]) -> String {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let conn = builder.serve_connection(TokioIo::new(stream), service_fn(hello));
conn.await.map_err(|e| format!("{e} (is_timeout={})", e.is_timeout()))
});
let mut client = TcpStream::connect(addr).await.unwrap();
let start = Instant::now();
client.write_all(send).await.unwrap();
let mut got = Vec::new();
let read = tokio::time::timeout(Duration::from_secs(35), client.read_to_end(&mut got)).await;
let elapsed = start.elapsed().as_secs_f64();
let client_side = match read {
Ok(Ok(_)) => format!("closed after {elapsed:.1}s, got {} bytes", got.len()),
Ok(Err(e)) => format!("read error after {elapsed:.1}s: {e}"),
Err(_) => format!("still open after {elapsed:.1}s, got {} bytes", got.len()),
};
drop(client);
let server_side = match server.await {
Ok(Ok(())) => "Ok(())".to_string(),
Ok(Err(e)) => format!("Err({e})"),
Err(e) if e.is_panic() => format!("panic: {}", PANIC_MSG.lock().unwrap().take().unwrap_or_default()),
Err(e) => format!("join error: {e}"),
};
format!("{name:<26} client: {client_side}\n{:<26} server: {server_side}", "")
}
#[tokio::main]
async fn main() {
std::panic::set_hook(Box::new(|info| {
let loc = info.location().map(|l| {
let file = l.file();
let short = file.find("hyper-1.12.0/").map_or(file, |i| &file[i..]);
format!("{short}:{}", l.line())
});
let msg = info
.payload()
.downcast_ref::<String>()
.cloned()
.or_else(|| info.payload().downcast_ref::<&str>().map(|s| s.to_string()))
.unwrap_or_default();
*PANIC_MSG.lock().unwrap() = Some(format!("{msg} at {}", loc.unwrap_or_default()));
}));
const PARTIAL: &[u8] = b"GET / HTTP/1.1\r\nHost: x\r\n"; // head never finishes
const FULL: &[u8] = b"GET / HTTP/1.1\r\nHost: x\r\n\r\n"; // one request, then idle
let a = http1::Builder::new();
let mut b = http1::Builder::new();
b.timer(TokioTimer::new());
let mut c = http1::Builder::new();
c.header_read_timeout(Duration::from_secs(1));
let mut d = http1::Builder::new();
d.timer(TokioTimer::new()).header_read_timeout(Duration::from_secs(1));
let mut e = http1::Builder::new();
e.header_read_timeout(None);
let results = tokio::join!(
run_case("A default, no timer", a, PARTIAL),
run_case("B default, timer", b, PARTIAL),
run_case("C 1s, no timer", c, PARTIAL),
run_case("D 1s, timer, partial", d.clone(), PARTIAL),
run_case("E 1s, timer, idle", d, FULL),
run_case("F None, no timer", e, PARTIAL),
);
for r in [results.0, results.1, results.2, results.3, results.4, results.5] {
println!("{r}");
}
}
动脑筋答案
M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:
A default, no timer client: still open after 35.0s, got 0 bytes
server: Err(connection closed before message completed (is_timeout=false))
B default, timer client: closed after 30.0s, got 0 bytes
server: Err(read header from client timeout (is_timeout=true))
C 1s, no timer client: read error after 0.0s: Connection reset by peer (os error 54)
server: panic: timeout `header_read_timeout` set, but no timer set at hyper-1.12.0/src/common/time.rs:84
D 1s, timer, partial client: closed after 1.0s, got 0 bytes
server: Err(read header from client timeout (is_timeout=true))
E 1s, timer, idle client: closed after 1.0s, got 77 bytes
server: Err(read header from client timeout (is_timeout=true))
F None, no timer client: still open after 35.0s, got 0 bytes
server: Err(connection closed before message completed (is_timeout=false))
- A:默认 30 秒,但没有 timer,
check返回None,超时根本没设。35 秒后客户端自己断开,服务端这时才收到 EOF,报的是「请求没读完连接就断了」(IncompleteMessage),不是超时。 - B:同样的默认值,有了 timer 就在 30.0 秒准时断开。服务端不回任何字节(
got 0 bytes),Connection以read header from client timeout结束,is_timeout()为true。读头超时不在on_error的自动回复列表里,所以没有 408。 - C:显式配置却没 timer,
serve_connection一调用就在time.rs第 84 行 panic。服务端任务死掉,socket 被丢弃时客户端发来的字节还没读,内核回了 RST,客户端看到Connection reset by peer。 - D:1 秒超时生效,1.0 秒断开。
- E:客户端收到了完整的 77 字节响应,然后连接空闲,1 秒后也被关掉,错误同样是读头超时。原因是响应写完后连接回到「等下一个请求头」的状态,读头计时从这时重新开始。所以
header_read_timeout同时也是 keep-alive 连接的空闲上限。 - F:显式
None关掉了超时,没有 timer 也不 panic,表现和 A 一样。
小结
Executor<Fut>只有execute,hyper 只在 HTTP/2 里用它:服务端每个流一个H2Stream,客户端的连接任务、每个请求的发送、请求 body 的灌送、升级后的流各一个。HTTP/1 不用执行器。rt::bounds的Http2ServerConnExec/Http2ClientConnExec是封闭的约束别名:用户写不出私有的H2Stream,只能通过实现Executor得到它们。Timer返回Pin<Box<dyn Sleep>>;Sleep带一个外部无法伪造的__type_id,让TokioTimer::reset能下转型后原地改截止时间(#3125)。- 没有 timer 时:
now()退回Instant::now();HTTP/2 keep-alive 走到sleep就 panicYou must supply a timer.(服务端和客户端都是),只开 BDP 不 panic。 - HTTP/1 的
header_read_timeout默认 30 秒,没 timer 时静默失效(warn!只在 unstable tracing 下存在);显式配置却没 timer 在serve_connection时 panic;显式None关闭。超时到期不回响应,直接关连接,而且它也管 keep-alive 连接的空闲时间。