HTTP/2 客户端
主线位置:客户端主线的 HTTP/2 版本。「请求 → 字节」还是先进一个通道,但通道另一头不是
Dispatcher,而是一个把请求转交给 h2 的循环;「字节 → 响应」由 h2 在它自己的连接任务里完成,再由每个请求单独的任务把响应交回调用方。
flowchart TB user["send_request"] --> chan["无界通道"] chan --> ct["ClientTask:你 spawn 的 Connection"] ct --> h2tx["h2 SendRequest:开流"] ct --> sendt["Send 任务:等响应头"] ct --> pipet["Pipe 任务:body 没发完时"] h2tx --> conn["ConnTask:握手时 spawn"] conn --> io["socket 读写"] sendt --> cb["回调:交回 Response"]
SendRequest:可以克隆,永远「就绪」
SendRequest 里只有一个 dispatch::UnboundedSender,Clone 就是克隆这个发送端。HTTP/1 的 SendRequest 不能克隆:一条 HTTP/1 连接同一时刻只能有一个请求在路上,want 信号让发送端在连接空闲时才 ready,最多先排一个(见 连接级客户端)。HTTP/2 不需要这道闸,UnboundedSender 的注释说得直白:
/// An unbounded version.
///
/// Cannot poll the Giver, but can still use it to determine if the Receiver
/// has been dropped. However, this version can be cloned.
#[cfg(feature = "http2")]
pub(crate) struct UnboundedSender<T, U> {
/// Only used for `is_closed`, since `mpsc::UnboundedSender` cannot be checked.
giver: want::SharedGiver,
inner: mpsc::UnboundedSender<Envelope<T, U>>,
}
want 在这里只剩一个用途:知道接收端(连接)还在不在。于是 poll_ready 只看连接有没有关:
pub fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<crate::Result<()>> {
if self.is_closed() {
Poll::Ready(Err(crate::Error::new_closed()))
} else {
Poll::Ready(Ok(()))
}
}
永远不会 Pending。并发多少个请求由 h2 和对端的 SETTINGS_MAX_CONCURRENT_STREAMS 决定,见下面的「没有蠢问题」。
send_request 把请求连同一个 oneshot 回调放进通道,返回的 future 只是等那个 oneshot。文档里的「Cancel safety」一节:
/// Dropping the returned future is the supported way to cancel an
/// in-flight HTTP/2 request. The stream is reset with `RST_STREAM`
/// (`CANCEL` error code); the shared connection remains usable for
/// other in-flight and future requests. The peer is notified
/// immediately rather than continuing to send a response body that
/// would be discarded.
本页后半部分就是验证这几句话。
握手:h2 连接在另一个任务里
Builder::handshake 建通道,调 proto::h2::client::handshake:
let (h2_tx, mut conn) = new_builder(config)
.handshake::<_, SendBuf<B::Data>>(Compat::new(io))
.await
.map_err(crate::Error::new_h2)?;
// An mpsc channel is used entirely to detect when the
// 'Client' has been dropped. This is to get around a bug
// in h2 where dropping all SendRequests won't notify a
// parked Connection.
let (conn_drop_ref, conn_drop_rx) = mpsc::channel(1);
let (cancel_tx, conn_eof) = oneshot::channel();
let ping_config = new_ping_config(config);
let (conn, ping) = if ping_config.is_enabled() {
let pp = conn.ping_pong().expect("conn.ping_pong");
let (recorder, ponger) = ping::channel(pp, ping_config, timer);
let conn: Conn<_, B> = Conn::new(ponger, conn);
(Either::left(conn), recorder)
} else {
(Either::right(conn), ping::disabled())
};
// ...
exec.execute_h2_future(H2ClientFuture::Task {
task: ConnTask::new(conn, conn_drop_rx, cancel_tx),
});
Ok(ClientTask {
ping,
conn_drop_ref,
conn_eof,
executor: exec,
h2_tx,
req_rx,
fut_ctx: None,
marker: PhantomData,
})
h2 的客户端握手给出两样东西:h2_tx(h2 自己的 SendRequest,用来开流)和 conn(h2 的 Connection,驱动 socket)。hyper 把 conn(开了 ping 时外面再包一层 Conn,先 poll Ponger 再 poll 连接,见 ping)包成 ConnTask,在握手里就交给执行器。返回给你的 Connection 里装的是 ClientTask,它不碰 socket,只负责从通道里取请求、交给 h2_tx。
所以「客户端连接」在 HTTP/2 下其实是两个任务:
| 任务 | 是什么 | 谁 spawn |
|---|---|---|
ConnTask | h2 的 Connection:读写 socket、收发所有帧 | hyper,握手时 |
ClientTask | Connection future:通道 → h2_tx.send_request | 你 |
两个 channel 把它们的生命周期连起来:
conn_drop_ref是一个容量为 1、从不发送数据的mpsc。ClientTask和每个发 body 的任务各持一个发送端;全部 drop 之后ConnTask里的drop_rx结束,ConnTask::poll随即 dropcancel_tx,继续 poll 连接让它自己收尾。注释说这是为了绕开 h2 的一个问题:drop 掉所有 h2SendRequest不会唤醒停着的Connection。conn_eof是反方向的 oneshot:ConnTask结束(连接断了、keep-alive 超时),cancel_tx随任务一起被 drop,ClientTask在通道里没有请求时会看到这个信号,以Ok(())结束。
握手文档里那句「if Connection is not await-ed, SendRequest will do nothing」也由此而来:h2 连接在跑,但没人把请求从通道里拿出来交给它。
ClientTask:把请求交给 h2
loop {
match ready!(self.h2_tx.poll_ready(cx)) {
Ok(()) => (),
Err(err) => {
self.ping.ensure_not_timed_out()?;
return if err.reason() == Some(::h2::Reason::NO_ERROR) {
trace!("connection gracefully shutdown");
Poll::Ready(Ok(Dispatched::Shutdown))
} else {
Poll::Ready(Err(crate::Error::new_h2(err)))
};
}
}
// If we were waiting on pending open
// continue where we left off.
if let Some(f) = self.fut_ctx.take() {
self.poll_pipe(f, cx);
continue;
}
match self.req_rx.poll_recv(cx) {
Poll::Ready(Some((req, cb))) => {
// check that future hasn't been canceled already
if cb.is_canceled() {
trace!("request callback is canceled");
continue;
}
let (head, body) = req.into_parts();
let mut req = ::http::Request::from_parts(head, ());
super::strip_connection_headers(req.headers_mut(), super::MessageKind::Request);
if let Some(len) = body.size_hint().exact() {
if len != 0 || headers::method_has_defined_payload_semantics(req.method()) {
headers::set_content_length_if_missing(req.headers_mut(), len);
}
}
let is_connect = req.method() == Method::CONNECT;
let eos = body.is_end_stream();
// ... CONNECT 带非零 Content-Length:回调直接给错误
let (fut, body_tx) = match self.h2_tx.send_request(req, !is_connect && eos) {
Ok(ok) => ok,
Err(err) => {
// ... 回调给错误,continue
}
};
let f = FutCtx {
is_connect,
eos,
fut,
body_tx,
body,
cb,
};
// Check poll_ready() again.
// If the call to send_request() resulted in the new stream being pending open
// we have to wait for the open to complete before accepting new requests.
match self.h2_tx.poll_ready(cx) {
Poll::Pending => {
// Save Context
self.fut_ctx = Some(f);
return Poll::Pending;
}
// ...
}
self.poll_pipe(f, cx);
}
Poll::Ready(None) => {
trace!("client::dispatch::Sender dropped");
return Poll::Ready(Ok(Dispatched::Shutdown));
}
Poll::Pending => match ready!(Pin::new(&mut self.conn_eof).poll(cx)) {
// ...
Err(_conn_is_eof) => {
trace!("connection task is closed, closing dispatch task");
return Poll::Ready(Ok(Dispatched::Shutdown));
}
},
}
}
- 取请求之前先
h2_tx.poll_ready,h2 说连接已经不能开新流时,NO_ERROR(对端 GOAWAY)算正常结束,别的原因算错误。 - 调用方已经放弃的请求(回调被取消)直接跳过,连流都不开。
- 删连接级头(和服务端是同一个
strip_connection_headers,请求里允许te: trailers),按size_hint补Content-Length。 h2_tx.send_request(req, end_of_stream)开流,拿回ResponseFuture和 body 的SendStream。body 已经结束就把 END_STREAM 标在 HEADERS 上。- 再问一次
poll_ready。Pending说明这个流超过了对端允许的并发数,在 h2 里排着等开;ClientTask把它存进fut_ctx,在它开成之前不再从通道取新请求。 - 通道两头:所有
SendRequest都 drop 了(None),或者连接任务结束了(conn_eof),都以Shutdown结束,Connection返回Ok(())。
问: poll_ready 永远是 Ready、通道又是无界的,同时发一万个请求会怎样?
答: 一万个请求都会立刻进通道,但不会一次全上线。对端在 SETTINGS 里声明 MAX_CONCURRENT_STREAMS(hyper 服务端默认 200),收到之前 h2 按 initial_max_send_streams(hyper 客户端默认 100,源码注释说这是规范建议的最小值,用来减少被对端 REFUSED_STREAM)算。超过上限的那个流在 h2 里「pending open」,ClientTask 第二次 poll_ready 得到 Pending,把它存进 fut_ctx 后返回,剩下的请求留在通道里,等有流结束再一个个取出来。所以请求不会丢,也不会超过对端的上限,只是在客户端排队;排队的时间算在每个请求 future 的耗时里,调用方的超时要把它算进去。
poll_pipe:每个请求一到两个任务
let send_stream = if !f.is_connect {
if !f.eos {
let mut pipe = PipeToSendStream::new(f.body, f.body_tx);
// eagerly see if the body pipe is ready and
// can thus skip allocating in the executor
match Pin::new(&mut pipe).poll(cx) {
Poll::Ready(_) => (),
Poll::Pending => {
let conn_drop_ref = self.conn_drop_ref.clone();
// keep the ping recorder's knowledge of an
// "open stream" alive while this body is
// still sending...
let ping = ping.clone();
let pipe = PipeMap {
pipe,
conn_drop_ref: Some(conn_drop_ref),
ping: Some(ping),
cancel_rx: Some(cancel_rx),
};
// Clear send task
self.executor
.execute_h2_future(H2ClientFuture::Pipe { pipe });
}
}
}
None
} else {
Some(f.body_tx)
};
self.executor.execute_h2_future(H2ClientFuture::Send {
send_when: SendWhen {
when: ResponseFutMap {
fut: f.fut,
ping: Some(ping),
send_stream: Some(send_stream),
exec: self.executor.clone(),
cancel_tx: Some(cancel_tx),
},
call_back: Some(f.cb),
},
});
- body 没结束:先在
ClientTask里就地 poll 一次PipeToSendStream(和服务端写响应 body 的是同一个类型,见 HTTP/2 服务端)。一次就发完了,就不必再分配一个任务;没发完,包成PipeMapspawn 出去,这是 Pipe 任务。 - 每个请求都有:一个 Send 任务,
SendWhen包着ResponseFutMap:等 h2 的ResponseFuture给出响应头,把响应 body 包成Incoming,通过回调交给send_request返回的那个 future。 - 三种任务在类型上是同一个枚举
H2ClientFuture的三个变体Pipe、Send、Task,执行器只需要接受一种 future 类型。
回到 HTTP/2 服务端 的并发实验,客户端那几行:
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
任务 2 是握手时的 ConnTask,任务 3 是我们 spawn 的 Connection(ClientTask),4 到 6 是我们为三个请求各 spawn 的任务,7 到 9 是三个 Send 任务。三个请求都是 GET、body 为空,没有 Pipe 任务。客户端一共 8 个任务,其中 4 个是 hyper 通过执行器建的。三个响应在 204~205ms 先后到达:三个流在同一条连接上同时在路上。
取消:drop 请求 future
调用方 drop send_request 返回的 future,丢掉的是 oneshot 的接收端。真正在等响应的是 Send 任务,它在 SendWhen::poll 里发现这件事:
match Pin::new(&mut this.when).poll(cx) {
Poll::Ready(Ok(res)) => {
call_back.send(Ok(res));
Poll::Ready(())
}
Poll::Pending => {
// check if the callback is canceled
match call_back.poll_canceled(cx) {
Poll::Ready(v) => v,
Poll::Pending => {
// Move call_back back to struct before return
this.call_back.set(Some(call_back));
return Poll::Pending;
}
}
trace!("send_when canceled");
// Tell pipe_task to reset the h2 stream so that
// RST_STREAM is sent and flow-control capacity freed.
this.when.as_mut().cancel();
Poll::Ready(())
}
// ...
}
响应还没好时,它用 poll_canceled 注册在回调上,接收端一 drop 就被唤醒。之后分两种情况:
- 没有 Pipe 任务(body 已经发完):Send 任务结束,
ResponseFuture被 drop,这个流在 hyper 这边再没有任何句柄。流还没结束,h2 自己给它发RST_STREAM(CANCEL)。 - 有 Pipe 任务(body 还在发):Pipe 任务手里还握着这个流的
SendStream,光 dropResponseFuture不够。cancel通过一个 oneshot 通知 Pipe 任务,PipeMap::poll收到后调send_reset(h2::Reason::CANCEL)并结束。
实验:两种取消,然后接着用
服务端:/slow 睡 1 秒;/upload 一块一块读请求 body;每个处理函数里放一个 Guard,它的 future 被 drop(正常结束或被取消)时打印一行。客户端在同一个 SendRequest 上:GET /slow 只等 100ms;POST /upload,body 先给一块 part1,之后既不给数据也不结束,同样只等 100ms;最后 GET /ok。中间夹着 HTTP/2 服务端 那页的 sniff.rs。
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"] }
futures = "0.3"
use std::{convert::Infallible, time::{Duration, Instant}};
use futures::channel::mpsc;
use http_body_util::{combinators::BoxBody, BodyExt, Empty, StreamBody};
use hyper::{body::{Bytes, Frame, Incoming}, client, server, service::service_fn, Request, Response};
use hyper_util::rt::{TokioExecutor, TokioIo};
use tokio::net::{TcpListener, TcpStream};
#[path = "../sniff.rs"]
mod sniff;
// Prints when the handler's future is dropped.
struct Guard(String, Instant);
impl Drop for Guard {
fn drop(&mut self) {
println!("{:>5}ms server: handler {} future dropped", self.1.elapsed().as_millis(), self.0);
}
}
type Body = BoxBody<Bytes, Infallible>;
#[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"];
let proxy = sniff::proxy(listener.local_addr().unwrap(), start, show).await;
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();
let _guard = Guard(path.clone(), start);
if path == "/upload" {
let mut body = req.into_body();
while let Some(frame) = body.frame().await {
match frame {
Ok(f) => println!("{:>5}ms server: /upload got {:?}", ms(), f.into_data().unwrap()),
Err(e) => {
let source = std::error::Error::source(&e).unwrap();
println!("{:>5}ms server: /upload body error: {e}: {source}", ms());
break;
}
}
}
} else if path == "/slow" {
tokio::time::sleep(Duration::from_secs(1)).await;
}
Ok::<_, Infallible>(Response::new(Empty::<Bytes>::new()))
});
let r = server::conn::http2::Builder::new(TokioExecutor::new())
.serve_connection(TokioIo::new(stream), svc)
.await;
println!("{:>5}ms server conn: {r:?}", ms());
});
let stream = TcpStream::connect(proxy).await.unwrap();
let (mut sender, conn) = client::conn::http2::handshake::<_, _, Body>(TokioExecutor::new(), TokioIo::new(stream))
.await
.unwrap();
let client_conn = tokio::spawn(conn);
let timeout = Duration::from_millis(100);
// 1: GET whose response never comes in time
let req = Request::get("http://x/slow").body(Empty::new().boxed()).unwrap();
let r = tokio::time::timeout(timeout, sender.send_request(req)).await;
println!("{:>5}ms client: /slow timed out = {}", ms(), r.is_err());
tokio::time::sleep(Duration::from_millis(50)).await;
// 2: POST whose body sends one chunk and then never ends
let (mut tx, rx) = mpsc::channel::<Result<Frame<Bytes>, Infallible>>(1);
tx.try_send(Ok(Frame::data(Bytes::from_static(b"part1")))).unwrap();
let req = Request::post("http://x/upload").body(StreamBody::new(rx).boxed()).unwrap();
let r = tokio::time::timeout(timeout, sender.send_request(req)).await;
println!("{:>5}ms client: /upload timed out = {}", ms(), r.is_err());
tokio::time::sleep(Duration::from_millis(50)).await;
// 3: the same connection is still usable
let req = Request::get("http://x/ok").body(Empty::new().boxed()).unwrap();
let res = sender.send_request(req).await;
println!("{:>5}ms client: /ok -> {:?}", ms(), res.map(|r| r.status()));
println!("{:>5}ms client: conn finished = {}", ms(), client_conn.is_finished());
drop(tx);
}
M2 Max、rustc 1.99.0、--release,连跑 3 次,除毫秒数差 1ms 以内外相同:
1ms c->s HEADERS stream=1 END_STREAM
104ms client: /slow timed out = true
104ms c->s RST_STREAM stream=1 CANCEL
104ms server: handler /slow future dropped
157ms c->s HEADERS stream=3
157ms c->s DATA stream=3 len=5
157ms server: /upload got b"part1"
258ms client: /upload timed out = true
258ms c->s RST_STREAM stream=3 CANCEL
258ms server: /upload body error: error reading a body from connection: stream error received: stream no longer needed
258ms server: handler /upload future dropped
311ms c->s HEADERS stream=5 END_STREAM
311ms server: handler /ok future dropped
311ms s->c HEADERS stream=5 END_STREAM
311ms client: /ok -> Ok(200)
311ms client: conn finished = false
- 流 1(GET):超时的同一毫秒,客户端发出
RST_STREAM(CANCEL)。服务端的H2Stream在poll_reset上被唤醒,返回,/slow的 future 被 drop,它的 1 秒sleep没睡完。没有 Pipe 任务,这个 RST 是 h2 在流的句柄全部 drop 后发的。 - 流 3(POST):HEADERS 不带 END_STREAM,
part1作为一个 5 字节的 DATA 帧发出,之后 body 卡住,Pipe 任务在等它。超时后 Send 任务通知 Pipe 任务,Pipe 任务发RST_STREAM(CANCEL)。服务端正在读 body 的frame()得到错误,最底下是 h2 对CANCEL的描述stream no longer needed。 - 流 5:同一个
SendRequest、同一条连接,照常 200(/ok的 Guard 在正常完成时也会打印)。客户端的Connection没有结束,线上没有 GOAWAY。
对比 HTTP/1:同样的超时会让客户端关掉整条连接,之后的 ready() 返回错误,见 hyper 解决什么问题 的「drop 一个请求 future 会怎样」。HTTP/2 每个请求有自己的流 id,取消一个流不影响别的流,这正是 lib.rs 文档里那两条的区别。
#4040(issue,标题「Dropping HTTP/2 response future doesn't cancel request body send and does not send RST_STREAM」)描述了实验里流 3 的情形:Send 任务发现取消后结束,drop 了 ResponseFuture,「but it does not signal pipe_task in any way. Since pipe_task still holds the SendStream, the h2 crate considers the stream alive and does not send RST_STREAM」。后果是服务端不知道请求已经被放弃,而这个流还占着(或继续等待)流控窗口,拖慢同一条连接上的其他流。
#4042(v1.9.0,「cancel sending client request body on response future drop」)的修法是「Add a oneshot channel between send_task and pipe_task」:Send 任务通过它通知 Pipe 任务,Pipe 任务调「send_reset(CANCEL) on the h2 SendStream, which sends RST_STREAM to the server and frees flow-control capacity」。就是上面 poll_pipe 里的 cancel_rx、ResponseFutMap 的 cancel_tx,以及 PipeMap::poll 开头那段检查。
收到 GOAWAY 之后
HTTP/2 服务端 的优雅停机实验里,客户端那几行是:
202ms client: is_closed=false
202ms client conn: Ok(())
202ms client: /late -> Err(hyper::Error(Canceled, "connection closed"))
GOAWAY 在 104ms 就到了,h2 的 ConnTask 那时就知道不能再开新流。但 ClientTask 没有因此被唤醒:它只在通道里来了新请求、或者 conn_eof 触发时才会被 poll,而服务端还有一个流没跑完,连接任务没结束。所以 200ms 时 is_closed() 还是 false。
/late 进入通道,唤醒 ClientTask,循环第一步 h2_tx.poll_ready 返回原因为 NO_ERROR 的错误,ClientTask 以 Dispatched::Shutdown 结束,Connection 返回 Ok(())。/late 还在通道里,ClientTask 一结束,通道的接收端被 drop,里面的每个 Envelope 在 drop 时都给回调送一个 Canceled 错误,说明文字是 connection closed。Envelope 送出的 TrySendError 里还带着原请求(message: Some(val)):这个请求从没上线,换一条连接重发是安全的。用 try_send_request 发请求的调用方能拿回它,hyper-util 的 legacy Client 就是这么做的,见 legacy Client。在途的 /first 由 ConnTask 和它的 Send 任务继续处理,不受 ClientTask 结束的影响,照常拿到 200。
把客户端的执行器换成一个只计数的 CountExec,在同一个 SendRequest 上依次发四个 POST(每个都等响应读完再发下一个):Empty、Full("hello")、一个先给 abc、50ms 后给 def 再结束的流式 body、一个一帧也不给就结束的流式 body。依赖同上一个实验。
use std::{
convert::Infallible,
future::Future,
sync::atomic::{AtomicUsize, Ordering::SeqCst},
time::Duration,
};
use futures::stream;
use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full, StreamBody};
use hyper::{body::{Bytes, Frame, Incoming}, client, rt::Executor, server, service::service_fn, Request, Response};
use hyper_util::rt::{TokioExecutor, TokioIo};
use tokio::net::{TcpListener, TcpStream};
static EXECUTED: AtomicUsize = AtomicUsize::new(0);
// Counts how many futures the client side hands to the executor.
#[derive(Clone)]
struct CountExec;
impl<F> Executor<F> for CountExec
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
fn execute(&self, fut: F) {
EXECUTED.fetch_add(1, SeqCst);
tokio::spawn(fut);
}
}
type Body = BoxBody<Bytes, Infallible>;
#[tokio::main(flavor = "current_thread")]
async fn main() {
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();
let svc = service_fn(|req: Request<Incoming>| async move {
let n = req.into_body().collect().await.unwrap().to_bytes().len();
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from(n.to_string()))))
});
server::conn::http2::Builder::new(TokioExecutor::new())
.serve_connection(TokioIo::new(stream), svc)
.await
.unwrap();
});
let stream = TcpStream::connect(addr).await.unwrap();
let (mut sender, conn) = client::conn::http2::handshake::<_, _, Body>(CountExec, TokioIo::new(stream))
.await
.unwrap();
tokio::spawn(conn);
println!("handshake: {}", EXECUTED.load(SeqCst));
// a body whose second chunk arrives 50ms later
let slow = || {
StreamBody::new(stream::unfold(0, |i| async move {
match i {
0 => Some((Ok(Frame::data(Bytes::from_static(b"abc"))), 1)),
1 => {
tokio::time::sleep(Duration::from_millis(50)).await;
Some((Ok(Frame::data(Bytes::from_static(b"def"))), 2))
}
_ => None,
}
}))
};
let reqs: Vec<(&str, Body)> = vec![
("POST empty ", Empty::new().boxed()),
("POST full ", Full::new(Bytes::from_static(b"hello")).boxed()),
("POST stream", slow().boxed()),
("POST nodata", StreamBody::new(stream::empty()).boxed()),
];
for (name, body) in reqs {
let before = EXECUTED.load(SeqCst);
let res = sender.send_request(Request::post("http://x/").body(body).unwrap()).await.unwrap();
let got = res.into_body().collect().await.unwrap().to_bytes();
println!("{name}: +{} (server read {got:?} bytes)", EXECUTED.load(SeqCst) - before);
}
}
握手之后计数是几?四个请求各让计数加几?提示:Full 是一次就能交出全部数据的 body。答案在本页末尾。
动脑筋答案
M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:
handshake: 1
POST empty : +1 (server read b"0" bytes)
POST full : +2 (server read b"5" bytes)
POST stream: +2 (server read b"6" bytes)
POST nodata: +1 (server read b"0" bytes)
handshake: 1:握手里 spawn 的ConnTask。POST empty:Empty的is_end_stream()为真,END_STREAM 标在 HEADERS 上,没有PipeToSendStream,只有 Send 任务。POST full:+2,不是 +1。Full的数据一次就能拿到,但就地 poll 的那一次里,PipeToSendStream拿到hello、预留 1 字节容量之后,poll_capacity返回了Pending(这一刻 h2 还没给这个流分到发送容量),于是 Pipe 任务也被 spawn。「一次 poll 发完就省一个任务」的捷径对带数据的 body 在这里没有生效。POST stream:数据要等,Pipe 任务 + Send 任务。POST nodata:StreamBody的is_end_stream()是默认的false,所以要走PipeToSendStream;但第一次poll_frame就返回None,它直接补一个空的 END_STREAM DATA 帧,就地完成,不需要 Pipe 任务。
小结
- HTTP/2 的
SendRequest可以Clone,后面是无界通道;poll_ready只在连接关闭时返回错误,永不Pending。并发上限来自对端的MAX_CONCURRENT_STREAMS,超出的流在 h2 里等,ClientTask暂停取新请求。 - 握手时 hyper 自己 spawn
ConnTask驱动 h2 连接和 socket;你 spawn 的Connection是ClientTask,只把请求交给 h2。两者之间靠conn_drop_ref(mpsc)和conn_eof(oneshot)互相感知结束。 - 每个请求一个 Send 任务等响应头;body 就地 poll 一次没发完,再加一个 Pipe 任务。三者都是
H2ClientFuture的变体。 - drop 请求 future:Send 任务经
poll_canceled发现,没有 Pipe 任务时 h2 发RST_STREAM(CANCEL),有 Pipe 任务时由它send_reset(CANCEL)(v1.9.0 起);服务端的处理函数随之被 drop,连接继续可用。 - 收到 GOAWAY 后,
ClientTask要等下一次被唤醒才结束;那时还在通道里的请求得到Canceled「connection closed」,它们没有上线。