Rust in Everything
基于 hyper 1.12.0

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
ConnTaskh2 的 Connection:读写 socket、收发所有帧hyper,握手时
ClientTaskConnection future:通道 → h2_tx.send_request你

两个 channel 把它们的生命周期连起来:

  • conn_drop_ref 是一个容量为 1、从不发送数据的 mpsc。ClientTask 和每个发 body 的任务各持一个发送端;全部 drop 之后 ConnTask 里的 drop_rx 结束,ConnTask::poll 随即 drop cancel_tx,继续 poll 连接让它自己收尾。注释说这是为了绕开 h2 的一个问题:drop 掉所有 h2 SendRequest 不会唤醒停着的 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

ClientTask::poll:

        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(())。
📝NOTE
没有蠢问题

问: 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:每个请求一到两个任务

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 服务端)。一次就发完了,就不必再分配一个任务;没发完,包成 PipeMap spawn 出去,这是 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,光 drop ResponseFuture 不够。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 文档里那两条的区别。

📝NOTE
冷知识:v1.9.0 之前,取消一个还在发 body 的请求不会发 RST_STREAM

#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。

❗IMPORTANT
动脑筋:四种 body,执行器各多跑几个任务?

把客户端的执行器换成一个只计数的 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」,它们没有上线。

💬 关联讨论

发起讨论
加载中...