Rust in Everything
基于 hyper 1.12.0

conn::http1 客户端

主线位置:客户端主线的中段。legacy::Client 拿到一条连接后,真正发请求的是这里的 SendRequest:请求和一个回调放进通道,后台 Connection 任务里的 Dispatcher 把它写出去、读回响应、通过回调交回来。本页讲通道两端怎么商量「现在能不能发」,以及请求在各个环节失败时调用方拿到什么。

flowchart TB
  caller["调用方 send_request"] --> sender["dispatch::Sender"]
  sender -->|"Envelope:请求和 Callback"| mpsc["无界 mpsc"]
  mpsc --> recv["dispatch::Receiver"]
  recv --> dc["dispatch::Client"]
  dc --> disp["Dispatcher:写请求、读响应"]
  disp -->|"Callback.send"| shot["oneshot 回到调用方"]
  recv -.->|"Taker.want:连接空闲"| flag["want 标志"]
  flag -.->|"Giver.give / poll_want"| sender

一次请求从头到尾经过哪些任务、哪些函数,一个请求的一生已经跑过一遍;Dispatcher 的循环见 Dispatcher。这一页只看客户端特有的三样东西:请求通道、dispatch::Client 这个 Dispatch 实现、回调。

handshake:拿到两个半边

handshake 是 Builder::new().handshake(io) 的简写。Builder::handshake 建一条请求通道,发送端放进 SendRequest,接收端交给 dispatch::Client,再和 Conn 一起装进 Dispatcher,成为 Connection。HTTP/1 没有握手报文,这个 future 一个字节也不发(代码见一个请求的一生)。

Connection 是一个 future,要有人 poll。通常 tokio::spawn(conn);不 poll 它,SendRequest 什么也做不了。HTTP/1 的 Builder 没有 Executor 参数:一条连接一个 future,hyper 自己不需要再 spawn 任何东西。v1.0.0-rc.3 的 CHANGELOG 记着这一点:http1::Builder::executor()「is removed, since it did nothing」(#3108)。HTTP/2 不同,它的 Builder 要一个执行器,见 HTTP/2 客户端。

Connection 自己的文档写了 drop 的后果:「Dropping the Connection will close the underlying IO resource. Any in-flight requests that have not received a response will be interrupted.」在途请求被打断时报什么错,见本页后面的 dispatch task is gone。

Connection::poll 收到 Dispatched::Upgrade 时调 pending.manual(),等升级的一方会收到错误;要把 IO 交出去得用 with_upgrades(),见协议升级。without_shutdown / into_parts 让连接结束时不关 IO,把 IO 和读缓冲里多出来的字节还给你。

SendRequest 的方法都落在通道的发送端上:

方法背后是
poll_ready / readyGiver::poll_want:标志是 Want 才 Ready(Ok);接收端没了是 Err(ChannelClosed)
is_readyGiver::is_wanting,只看一眼,不注册 waker
is_closedGiver::is_canceled
send_requestSender::send,回调是 NoRetry
try_send_requestSender::try_send,回调是 Retry,失败时可能把请求还回来

请求通道:无界 mpsc 加一个 want 标志

channel 把 tokio 的无界 mpsc 和 want crate(0.3.2)的一对 Giver / Taker 绑在一起。Sender:

pub(crate) struct Sender<T, U> {
    /// One message is always allowed, even if the Receiver hasn't asked
    /// for it yet. This boolean keeps track of whether we've sent one
    /// without notice.
    #[cfg(feature = "http1")]
    buffered_once: bool,
    /// The Giver helps watch that the Receiver side has been polled
    /// when the queue is empty. This helps us know when a request and
    /// response have been fully processed, and a connection is ready
    /// for more.
    giver: want::Giver,
    /// Actually bounded by the Giver, plus `buffered_once`.
    inner: mpsc::UnboundedSender<Envelope<T, U>>,
}

能不能发,由 can_send 决定:

    fn can_send(&mut self) -> bool {
        if self.giver.give() || !self.buffered_once {
            // If the receiver is ready *now*, then of course we can send.
            //
            // If the receiver isn't ready yet, but we don't have anything
            // in the channel yet, then allow one message.
            self.buffered_once = true;
            true
        } else {
            false
        }
    }

send 先问 can_send,不行就把请求原样还回去(Err(val)),send_request 把它变成 Canceled, "connection was not ready";行就新建一个 oneshot,把请求和回调装进 Envelope 塞进 mpsc。

want 的标志只有四个值,谁改它:

状态谁设的含义
Idle初始值;Giver::give 从 Want 换成 Idle;Taker::unwant连接没在要请求
WantTaker::want,在 Receiver::poll_recv 看到队列为空时连接空闲,要下一个
GiveGiver::poll_want 在不是 Want 时登记 waker发送端在等,want() 时要叫醒它
ClosedTaker::cancel,Receiver 被 drop 或 close 时连接没了

give() 是一次比较交换:Want 才换成 Idle 并返回真。所以每说一次「要」,只放进来一个请求。

buffered_once 有一个容易看漏的地方:它被设成 true 以后再也不会变回 false。「不等连接开口先放一个」这个特权在整个通道的一生里只有一次,就是连接任务第一次被 poll 之前的那一个请求(handshake 后马上 send_request,Connection 还没 spawn)。之后每个请求都要等连接说「要」。hyper 自己的单元测试 sender_checks_for_want_on_send 写的就是这个:「Even though 1 has been popped, only 1 could be buffered for the lifetime of the channel.」

💡TIP
打个比方:窗口上方的「请下一位」灯

柜台(连接任务)一次只办一个人。顾客(send_request)看灯:灯亮(Want)就把单子递进去,递的同时把灯按灭(give());灯不亮就被打发走(connection was not ready)。柜台办完一个、低头看见托盘空了,才把灯按亮(Taker::want)。顾客也可以站在旁边等灯亮(poll_ready 登记 waker,状态 Give)。柜台撤了,灯换成「暂停服务」(Closed)。开门前的特例:柜台还没开张时可以先往托盘里放一张单子(buffered_once),只此一次。

比方到这里为止:真实的托盘(mpsc)没有容量上限,限制全在那盏灯上;而且灯是「柜台看见托盘空」时亮的,不是「上一个人走了」时亮的,两者之间的差别正是下面 #4208 修的那个 bug。

下面的程序把这几条都跑一遍:连接还没被 poll 时发两个;连接跑起来、/1 在途时问一次就绪再发一个;/1 完成后连发两个。服务端每个请求睡 200ms。

hyper = { version = "=1.12.0", features = ["client", "server", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio"] }
http-body-util = "=0.1.5"
bytes = "1"
tokio = { version = "=1.54.1", features = ["macros", "rt", "net", "time"] }
use std::convert::Infallible;
use std::future::poll_fn;
use std::task::Poll;
use std::time::{Duration, Instant};

use bytes::Bytes;
use http_body_util::{Empty, Full};
use hyper::client::conn::http1::SendRequest;
use hyper::{Request, Response};
use hyper_util::rt::TokioIo;
use tokio::net::{TcpListener, TcpStream};

// 只 poll 一次 SendRequest::poll_ready,把结果打印出来
async fn probe(tag: &str, start: Instant, sender: &mut SendRequest<Empty<Bytes>>) {
  let r = poll_fn(|cx| Poll::Ready(sender.poll_ready(cx))).await;
  let shown = match r {
    Poll::Pending => "Pending".to_string(),
    Poll::Ready(Ok(())) => "Ready(Ok)".to_string(),
    Poll::Ready(Err(e)) => format!("Ready(Err({e}))"),
  };
  println!("{:>4}ms {tag:<26} is_ready={:<5} poll_ready={shown}", start.elapsed().as_millis(), sender.is_ready());
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
  let start = Instant::now();
  let listener = TcpListener::bind("127.0.0.1:0").await?;
  let addr = listener.local_addr()?;

  // 服务端:每个请求睡 200ms 再回 "ok"
  tokio::spawn(async move {
    let (stream, _) = listener.accept().await.unwrap();
    let svc = hyper::service::service_fn(|_req| async {
      tokio::time::sleep(Duration::from_millis(200)).await;
      Ok::<_, Infallible>(Response::new(Full::new(Bytes::from_static(b"ok"))))
    });
    let _ = hyper::server::conn::http1::Builder::new()
      .serve_connection(TokioIo::new(stream), svc)
      .await;
  });

  let stream = TcpStream::connect(addr).await?;
  let (mut sender, conn) = hyper::client::conn::http1::handshake(TokioIo::new(stream)).await?;
  let req = |p: &str| Request::get(p).header("host", "x").body(Empty::<Bytes>::new()).unwrap();

  // 1. Connection 还没被 poll 过
  probe("before conn polled", start, &mut sender).await;
  let f1 = sender.send_request(req("/1"));
  let f2 = sender.send_request(req("/2"));
  println!("{:>4}ms send /2 -> {:?}", start.elapsed().as_millis(), f2.await.err());

  // 2. 让 Connection 跑起来;/1 是先前排进去的那一个
  tokio::spawn(conn);
  let t1 = tokio::spawn(async move { f1.await.map(|r| r.status()) });
  tokio::time::sleep(Duration::from_millis(100)).await;
  probe("/1 in flight", start, &mut sender).await;
  let f3 = sender.send_request(req("/3"));
  println!("{:>4}ms send /3 -> {:?}", start.elapsed().as_millis(), f3.await.err());

  let r1 = t1.await?;
  println!("{:>4}ms /1 -> {:?}", start.elapsed().as_millis(), r1);
  probe("after /1 done", start, &mut sender).await;

  // 3. 连接在要请求:连着发两个,第二个被拒
  let f4 = sender.send_request(req("/4"));
  let f5 = sender.send_request(req("/5"));
  println!("{:>4}ms send /5 -> {:?}", start.elapsed().as_millis(), f5.await.err());
  let r4 = f4.await?.status();
  println!("{:>4}ms /4 -> {}", start.elapsed().as_millis(), r4);
  Ok(())
}

平台 M2 Max、rustc 1.99.0,跑 3 次,各行顺序和结果相同,毫秒数差 4ms 以内。其中一次:

   0ms before conn polled         is_ready=false poll_ready=Pending
   0ms send /2 -> Some(hyper::Error(Canceled, "connection was not ready"))
 104ms /1 in flight               is_ready=false poll_ready=Pending
 104ms send /3 -> Some(hyper::Error(Canceled, "connection was not ready"))
 204ms /1 -> Ok(200)
 204ms after /1 done              is_ready=true  poll_ready=Ready(Ok)
 204ms send /5 -> Some(hyper::Error(Canceled, "connection was not ready"))
 407ms /4 -> 200 OK
  • 第 1 行:连接一次都没被 poll,标志是初始的 Idle,poll_ready 是 Pending。但 /1 照样放进去了(buffered_once),/2 被拒。
  • 第 3、4 行:/1 在途,连接任务不去 poll 通道,没人说「要」。poll_ready 是 Pending,/3 被拒。/3 的错误不是等出来的:send_request 调用的那一刻 can_send 就失败了,返回的 future 第一次 poll 就是这个错误。
  • 第 6 行:/1 完成,连接回到空闲,再次 poll 空队列,标志变 Want。
  • 第 7、8 行:标志是 Want 时连发两个,/4 的 give() 把它换成 Idle,/5 被拒,/4 正常完成。所以就算连接空闲,同一时刻也只能放进一个。

send_request 被拒时请求就没了(错误里不带请求);换成 try_send_request,被拒时的 TrySendError 带着原来的请求,可以拿去别的连接上发。hyper-util 的连接池就是这么用的,见 legacy Client。

📝NOTE
没有蠢问题

问: 既然要限制成「一次一个」,为什么不直接用容量为 1 的有界 mpsc?

答: 有界通道的「有空位」发生在连接任务取走请求的那一刻,可那时请求才刚开始写,连接要一直忙到响应读完。SendRequest 的就绪要表达的是「连接空闲」,这件事只有连接任务知道,而且只能用「我又来 poll 空队列了」来表达:Dispatcher 只在上一次请求和响应都走完、Conn 回到 Init(can_write_head)、手里没有回调(should_poll)时才 poll 通道。Sender 字段上的注释说的就是这个:Giver 用来知道「a request and response have been fully processed, and a connection is ready for more」。连接池拿 is_ready 判断一条连接能不能借给下一个请求,靠的也是这一点。

Receiver 这一边:什么时候说「要」

Receiver::poll_recv:

    pub(crate) fn poll_recv(&mut self, cx: &mut Context<'_>) -> Poll<Option<(T, Callback<T, U>)>> {
        match self.inner.poll_recv(cx) {
            Poll::Ready(item) => {
                // A want signaled after finding the queue empty can land after
                // the Sender's `give()` for the message taken here, as can one
                // signaled on a coop-budget Pending with a message queued.
                // Withdraw it, or the Sender reports this connection as ready
                // while it is still serving this message.
                self.taker.unwant();
                Poll::Ready(item.map(|mut env| env.0.take().expect("envelope not dropped")))
            }
            Poll::Pending => {
                self.taker.want();
                Poll::Pending
            }
        }
    }

队列空(Pending)就 want();取到东西就 unwant(),把可能残留的 Want 撤回去。这个函数只在 Dispatcher::poll_write 里经 Dispatch::poll_msg 调用,而那里的前提是 body_rx 为空、can_write_head()、should_poll()(手里没有回调),三个条件合起来就是「上一个请求写完了、响应也读完了」。

Receiver 被 drop 时先 taker.cancel(),再 drop 里面的 mpsc 接收端(Drop for Receiver)。顺序是故意的:先让 Giver 看到 Closed,is_closed() 和 poll_ready 就不会把一条正在消失的连接报成可用。

📝NOTE
冷知识:unwant() 是 v1.12.0 才加的

issue #4207 的标题是「HTTP/1 client: SendRequest::is_ready() can stay true while a request is in flight」。#4208(随 v1.12.0 发布)的提交说明给了两种走法:连接任务「finds its request queue empty」之后、调 want() 之前,另一个线程上的 send_request 先 give() 了;或者「tokio's coop budget can make the queue report Pending with a request already queued」。两种情况下请求被取走时标志仍是 Want,而请求在途期间没人再 poll 通道,is_ready() 一直是真,直到响应结束。后果在 hyper-util:「hyper-util's legacy pool returns connections on is_ready, so it hands a busy connection to the next request, which then waits behind the whole response.」

修法就是上面代码里取到请求时那句 self.taker.unwant(),一次从 Want 到 Idle 的比较交换。Taker::unwant 是 want 为此新加的方法,所以 hyper 1.12.0 的 Cargo.toml 把 want 提到了 0.3.2。PR 也解释了为什么不改发送端的顺序(先入队再 give()):「The task's late want() can still land after the give(), and the coop path doesn't depend on the order at all.」本站锁的 hyper 1.10.1 没有这一句。

dispatch::Client:取请求、交响应

Dispatch 的客户端实现 Client 只有三个字段:callback(在途请求的回调)、rx(通道接收端)、rx_closed。

poll_msg 从通道取一个请求,先看调用方是否已经放弃:

            match this.rx.poll_recv(cx) {
                Poll::Ready(Some((req, mut cb))) => {
                    // check that future hasn't been canceled already
                    match cb.poll_canceled(cx) {
                        Poll::Ready(()) => {
                            trace!("request canceled");
                            Poll::Ready(None)
                        }
                        Poll::Pending => {
                            // ... 拆成 RequestHead 和 body
                            this.callback = Some(cb);
                            Poll::Ready(Some(Ok((head, body))))
                        }
                    }
                }
                Poll::Ready(None) => {
                    // user has dropped sender handle
                    trace!("client tx closed");
                    this.rx_closed = true;
                    Poll::Ready(None)
                }
                Poll::Pending => Poll::Pending,
            }

两种 None 在 Dispatcher::poll_write 里的处理一样:self.close(),读写都关。也就是说,请求还在通道里没写出去、调用方就 drop 了 future,连接同样会关,而不是跳过这个请求接着用。下面的程序验证这一点:请求放进通道后,连接任务一次都还没跑,就把 send_request 返回的 future 扔掉(依赖同上一段程序)。

use std::convert::Infallible;

use bytes::Bytes;
use http_body_util::Empty;
use hyper::{Request, Response};
use hyper_util::rt::TokioIo;
use tokio::net::{TcpListener, TcpStream};

#[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();
  let server = tokio::spawn(async move {
    let (stream, _) = listener.accept().await.unwrap();
    let svc = hyper::service::service_fn(|req: Request<hyper::body::Incoming>| async move {
      println!("server: got {}", req.uri());
      Ok::<_, Infallible>(Response::new(Empty::<Bytes>::new()))
    });
    hyper::server::conn::http1::Builder::new().serve_connection(TokioIo::new(stream), svc).await
  });
  let stream = TcpStream::connect(addr).await.unwrap();
  let (mut tx, conn) = hyper::client::conn::http1::handshake::<_, Empty<Bytes>>(TokioIo::new(stream)).await.unwrap();
  let conn = tokio::spawn(conn);
  let req = |p: &str| Request::get(p).header("host", "x").body(Empty::new()).unwrap();
  // 请求进了通道,连接任务还没跑,调用方就把 future 扔了
  drop(tx.send_request(req("/never")));
  println!("ready:  {:?}", tx.ready().await);
  println!("client conn task: {:?}", conn.await.unwrap());
  println!("server conn task: {:?}", server.await.unwrap());
}

跑 3 次,输出相同:

ready:  Err(hyper::Error(ChannelClosed))
client conn task: Ok(())
server conn task: Ok(())

服务端没有打印 server: got,请求一个字节也没写;客户端连接任务正常结束,SendRequest 再问就绪得到 ChannelClosed;服务端看到的是一条空闲时被关掉的连接,Ok(())。请求已经写出去之后再 drop 的情况,hyper 解决什么问题里跑过:同样关连接,服务端那边是 IncompleteMessage。负责发现「已经写出去的请求没人要了」的是 poll_ready:回调的接收端没了就返回 Err,poll_read_head 随即关闭连接。

recv_msg 收到读出来的东西,分三支:

            match msg {
                Ok((msg, body)) => {
                    if let Some(cb) = self.callback.take() {
                        let res = msg.into_response(body);
                        cb.send(Ok(res));
                        Ok(())
                    } else {
                        // ...
                        Err(crate::Error::new_unexpected_message())
                    }
                }
                Err(err) => {
                    if let Some(cb) = self.callback.take() {
                        cb.send(Err(TrySendError {
                            error: err,
                            message: None,
                        }));
                        Ok(())
                    } else if !self.rx_closed {
                        self.rx.close();
                        if let Some((req, cb)) = self.rx.try_recv() {
                            trace!("canceling queued request with connection error: {}", err);
                            // in this case, the message was never even started, so it's safe to tell
                            // the user that the request was completely canceled
                            cb.send(Err(TrySendError {
                                error: crate::Error::new_canceled().with(err),
                                message: Some(req),
                            }));
                            Ok(())
                        } else {
                            Err(err)
                        }
                    } else {
                        Err(err)
                    }
                }
            }
  • 读到响应头:从 callback 里取出回调,把 Response<Incoming> 交回去。回调在这里就被取走了,should_poll() 变真,但下一个请求还要等 Conn 回到 Init 才会被取(body 读完)。
  • 连接出错、有在途请求:错误交给这个请求,message: None,请求已经写出去(至少开始写了),不能还。
  • 连接出错、没有在途请求:关掉通道,看看里面有没有排着的请求。有的话它从没开始写,用 Canceled 包住原错误、连同请求一起还回去。这是「可以安全换一条连接重发」的一个来源。

两种回调,以及每种失败报什么

Callback 有两个变体,区别只在 oneshot 里装什么:

pub(crate) enum Callback<T, U> {
    #[allow(unused)]
    Retry(Option<oneshot::Sender<Result<U, TrySendError<T>>>>),
    NoRetry(Option<oneshot::Sender<Result<U, crate::Error>>>),
}

try_send 用 Retry,错误是 TrySendError,可能带着请求;send 用 NoRetry,Callback::send 发送前把 TrySendError 拆成里面的 hyper::Error,请求就扔了。连接任务内部不区分两者,一律按 TrySendError 构造错误。TrySendError 的文档说明了带不带请求的分界:「If an error occurs after the message has been serialized onto the connection, it will not be available here.」

把请求可能失败的地方排一遍:

情形调用方拿到的错误try_send_request 能拿回请求吗
发的时候连接没在要Canceled,"connection was not ready"能
排在通道里,连接任务结束或被 dropCanceled,"connection closed"(Envelope 的 Drop)能
排在通道里,连接空闲时出错Canceled,原因是那个错误(recv_msg 第三支)能
已在途,连接出错(对端关闭、IO 错误、解析失败)那个错误本身,比如 IncompleteMessage不能
已在途,连接任务被 dropUser(DispatchGone),见下一节不能
连接已经没了,再问就绪poll_ready 返回 ChannelClosed不涉及

Envelope 是通道里装的那一项,它的 Drop 在请求还没被取走时触发:

impl<T, U> Drop for Envelope<T, U> {
    fn drop(&mut self) {
        if let Some((val, cb)) = self.0.take() {
            cb.send(Err(TrySendError {
                error: crate::Error::new_canceled().with("connection closed"),
                message: Some(val),
            }));
        }
    }
}

dispatch task is gone

回调被 drop 时如果还没发过结果,Drop for Callback 补发一个错误,错误由 dispatch_gone 构造:

#[cold]
fn dispatch_gone() -> crate::Error {
    // ...
    crate::Error::new_user_dispatch_gone().with(if std::thread::panicking() {
        "user code panicked"
    } else {
        "runtime dropped the dispatch task"
    })
}

正常路径上回调总是通过 send 交出结果的,走到 Drop 只有一种情况:在途请求的回调还在 dispatch::Client 里,整个 Dispatcher 被 drop 了。也就是 Connection 这个 future 没跑完就没了:任务被 abort、所在的运行时被关、或者任务里 panic。Display 只打印 dispatch task is gone,后半句在 source() 里;判断它用 is_user(),没有专门的 is_* 方法。原因只分两种:drop 发生在 panic 展开过程中是「user code panicked」,其他情况都写「runtime dropped the dispatch task」,哪怕是你自己 abort 的。

请求还没被取走时 Dispatcher 被 drop,回调还在通道里的 Envelope 里,走的是上一节的 Envelope::drop,报的是 Canceled,不是 DispatchGone。

下面的程序把四种情况各做一次。服务端每个请求睡 200ms。

hyper = { version = "=1.12.0", features = ["client", "server", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio"] }
http-body-util = "=0.1.5"
bytes = "1"
tokio = { version = "=1.54.1", features = ["macros", "rt", "rt-multi-thread", "net", "time"] }
use std::convert::Infallible;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;

use bytes::Bytes;
use http_body_util::Empty;
use hyper::body::{Body, Frame};
use hyper::{Request, Response};
use hyper_util::rt::TokioIo;
use tokio::net::{TcpListener, TcpStream};

// 一个 poll 就 panic 的请求 body
struct PanicBody;
impl Body for PanicBody {
  type Data = Bytes;
  type Error = Infallible;
  fn poll_frame(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Result<Frame<Bytes>, Infallible>>> {
    panic!("body exploded");
  }
}

async fn connect<B>(addr: std::net::SocketAddr) -> (hyper::client::conn::http1::SendRequest<B>, hyper::client::conn::http1::Connection<TokioIo<TcpStream>, B>)
where
  B: Body + 'static + Send,
  B::Data: Send,
  B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
{
  let stream = TcpStream::connect(addr).await.unwrap();
  hyper::client::conn::http1::handshake(TokioIo::new(stream)).await.unwrap()
}

fn get() -> Request<Empty<Bytes>> {
  Request::get("/").header("host", "x").body(Empty::new()).unwrap()
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  std::panic::set_hook(Box::new(|_| {})); // 不打印 panic 信息
  let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
  let addr = listener.local_addr().unwrap();
  // 服务端:每个请求睡 200ms 再回
  tokio::spawn(async move {
    loop {
      let (stream, _) = listener.accept().await.unwrap();
      tokio::spawn(async move {
        let svc = hyper::service::service_fn(|_req| async {
          tokio::time::sleep(Duration::from_millis(200)).await;
          Ok::<_, Infallible>(Response::new(Empty::<Bytes>::new()))
        });
        let _ = hyper::server::conn::http1::Builder::new().serve_connection(TokioIo::new(stream), svc).await;
      });
    }
  });

  // A: 请求已写出、在等响应时,abort 连接任务
  let (mut tx, conn) = connect::<Empty<Bytes>>(addr).await;
  let task = tokio::spawn(conn);
  let fut = tx.send_request(get());
  let waiter = tokio::spawn(fut);
  tokio::time::sleep(Duration::from_millis(50)).await;
  task.abort();
  println!("A abort in flight:   {:?}", waiter.await.unwrap().err());

  // B: 连接任务跑在另一个运行时上,请求在途时那个运行时被关掉
  let other = tokio::runtime::Builder::new_multi_thread().worker_threads(1).enable_all().build().unwrap();
  let (mut tx, conn) = connect::<Empty<Bytes>>(addr).await;
  other.spawn(conn);
  let waiter = tokio::spawn(tx.send_request(get()));
  tokio::time::sleep(Duration::from_millis(50)).await;
  other.shutdown_background();
  println!("B runtime shut down: {:?}", waiter.await.unwrap().err());

  // C: 请求 body 在连接任务里 panic
  let (mut tx, conn) = connect::<PanicBody>(addr).await;
  let task = tokio::spawn(conn);
  let res = tx.send_request(Request::post("/").header("host", "x").body(PanicBody).unwrap()).await;
  println!("C body panicked:     {:?}", res.err());
  println!("  conn task:         panic = {}", task.await.unwrap_err().is_panic());

  // D: 请求还在通道里(连接任务一次都没跑过)就 abort
  let (mut tx, conn) = connect::<Empty<Bytes>>(addr).await;
  let task = tokio::spawn(conn);
  let fut = tx.send_request(get());
  task.abort();
  println!("D abort before run:  {:?}", fut.await.err());
}
❗IMPORTANT
动脑筋:A、B、C、D 四行各打印什么错误?

平台 M2 Max、rustc 1.99.0。A 在请求写出后 50ms abort 连接任务;B 的连接任务在另一个运行时上,那个运行时在请求在途时被关掉;C 的请求 body 一被 poll 就 panic;D 在连接任务第一次运行之前就 abort 了它。哪几行是 DispatchGone?它们的原因字符串分别是什么?

hyper-util 的 legacy::Client 把每条连接的 Connection 交给你给它的执行器(TokioExecutor 就是 tokio::spawn),连接任务跑在建立连接时所在的运行时上。一个 static 的 Client 被多个 #[tokio::test] 共用时,池里的连接可能属于一个已经结束的测试的运行时,在途请求就会碰到 B 这种情况。这个坑放到常见坑一章复现。

📝NOTE
冷知识:这个错误以前是一个 panic

issue #2649(「Consider removing "dispatch dropped without returning error"」)写道:「Hyper panics with "dispatch dropped without returning error" if the background dispatch task goes away, either because the runtime was dropped or some user-provided code such as a HttpBody implementation panicked」,希望「return a proper error instead」。修复提交「fix(client): send an error back to client when dispatch misbehaves」给 Callback 里的发送端套上 Option,加了上面那个 Drop 和 User::DispatchGone,panic 和运行时 drop 两种原因就是那时定下的字符串。它在 0.14 线上随 v0.14.21 发布,1.x 线上首见于 v1.0.0-rc.2 的 CHANGELOG。send_request 里那句 unreachable!("dispatch dropped without returning error") 留到了今天,按注释,现在走到它就是 hyper 的 bug。

连接什么时候结束

Dispatcher 对客户端的 is_done 只看读端:读端关了就结束(见 Dispatcher)。客户端连接结束的几条路:

起因走到哪里
所有 SendRequest 都 drop 了poll_msg 收到 None,close()(一个请求的一生的收尾一节)
调用方放弃了请求排队中:poll_msg 里 poll_canceled 为 Ready;在途:poll_ready 返回 Err。两者都 close()
请求带 Connection: closeencode_head 在写请求头时就把 KA 设成 Disabled,响应走完后关闭(#4110,v1.11.1)
服务端关了空闲连接poll_read_keep_alive 走 require_empty_read,读到 EOF 就 close_read,正常结束
空闲时读到多余字节require_empty_read 返回 unexpected_message 错误,经 recv_msg 的第三支处理排队中的请求
在途时读到 EOFIncompleteMessage,交给在途请求

Connection: close 那一行的背景是 #4110 的说明:「hyper's client derives connection reuse from the response alone」,后端如果不理会请求里的 close、响应里也不带,连接就会留在池里被复用;现在写请求头时就禁止复用。头里的 close 怎么合进去,见 Conn 的读写状态机。

「服务端关了空闲连接」那一行有一个时间窗:客户端要等反应器把 EOF 事件送到、连接任务再跑一次,才知道连接已经没了。在这之前它的 is_ready() 还是真,请求还会被放进来。放进来以后是在发现 EOF 之前还是之后写出去,决定了请求能不能被安全重发。这件事放在 legacy Client 里用实验看。

动脑筋答案

A abort in flight:   Some(hyper::Error(User(DispatchGone), "runtime dropped the dispatch task"))
B runtime shut down: Some(hyper::Error(User(DispatchGone), "runtime dropped the dispatch task"))
C body panicked:     Some(hyper::Error(User(DispatchGone), "user code panicked"))
  conn task:         panic = true
D abort before run:  Some(hyper::Error(Canceled, "connection closed"))

跑了 3 次,输出相同。

  • A:请求已经被取走,回调在 dispatch::Client::callback 里。abort 让 tokio 在下一次调度时 drop 这个任务的 future(见 JoinHandle、取消与 JoinError),回调随之 drop。不是 panic,所以是「runtime dropped the dispatch task」,虽然动手的是我们自己。
  • B:shutdown_background 关闭运行时,运行时逐个 drop 还活着的任务(见 OwnedTasks 与关闭),结果和 A 一样。这是这句报文字面上说的情形。
  • C:poll_frame 在连接任务里 panic,展开时 drop Dispatcher,std::thread::panicking() 为真,于是是「user code panicked」。panic 被 tokio 接住,JoinError::is_panic() 为真,调用方的线程没有崩。
  • D:连接任务一次都没跑,请求还躺在通道里。drop 的是 Envelope,报 Canceled, "connection closed",这种错误里带着请求,try_send_request 能拿回来。

小结

  • handshake 只是把通道、Conn、Dispatcher 装起来,不发字节;HTTP/1 不需要执行器,但 Connection 必须有人 poll。
  • 请求通道是无界 mpsc 加 want 标志。连接任务只有在上一次请求和响应都走完、再去 poll 空队列时才 want();poll_ready 就是等这个标志,在途期间一直 Pending。
  • 每次「要」只放进一个请求;不等「要」先放的只有连接第一次被 poll 之前的那一个,buffered_once 置位后不复原。被拒的请求得到 Canceled, "connection was not ready"。
  • 取到请求时 unwant() 撤回残留的 Want,否则 is_ready() 会在请求在途时为真,连接池会把忙着的连接借出去(#4208,v1.12.0)。
  • 调用方放弃请求,无论请求写没写出去,连接都会关。
  • 请求没被取走时失败报 Canceled,try_send_request 能拿回请求;在途时失败报真实错误,请求拿不回来;连接任务被 drop 时在途请求报 dispatch task is gone,原因只分「user code panicked」和「runtime dropped the dispatch task」。

💬 关联讨论

发起讨论
加载中...