Rust in Everything
基于 hyper 1.12.0

Dispatcher

主线位置:两条主线共用。服务端主线上,Connection future 每次被 poll,都是 Dispatcher 在转圈:读字节、解析成请求、调用服务、等响应、编码写回、刷新。客户端主线上,同一个 Dispatcher 换了一个 Dispatch 实现,变成「从通道取请求、写出去、读响应、交回回调」。

flowchart TB
  pollnode["Future::poll"] --> catchnode[poll_catch]
  catchnode --> inner[poll_inner]
  inner --> loopnode["poll_loop:最多 16 圈"]
  loopnode --> rd["poll_read:读头或 body"]
  rd --> wr["poll_write:等响应,写头和 body"]
  wr --> fl["poll_flush:刷到 socket"]
  fl --> again{"还要再读或再写?"}
  again -- 是 --> rd
  again -- 否 --> done{is_done}
  loopnode -- 转满 16 圈 --> yieldnode["yield_now:唤醒自己,Pending"]
  done -- 否 --> pend[Pending]
  done -- 是 --> result["Shutdown 或 Upgrade"]

Dispatcher 不碰字节格式。解析和编码在 Conn 里(再往下是 role 和缓冲区),调用服务和交付响应在 Dispatch 的实现里。Dispatcher 只决定「这一刻该推动哪一边」,以及什么时候让出、什么时候结束。

结构:一个 Conn,一个 Dispatch,两个 body 槽

Dispatcher:

pub(crate) struct Dispatcher<D, Bs: Body, I, T> {
    conn: Conn<I, Bs::Data, T>,
    dispatch: D,
    body_tx: SenderDropGuard,
    body_rx: Pin<Box<Option<Bs>>>,
    is_closing: bool,
}
字段是什么
conn连接的读写状态机加读写缓冲,I 是 IO,T 是 role::Server 或 role::Client,见 Conn 的读写状态机
dispatchDispatch 的实现:服务端是 Server<S, B>,客户端是 Client<B>
body_tx正在读进来的 body 的发送端。读到的数据块从这里送给 Incoming
body_rx正在写出去的 body(服务端是响应 body,客户端是请求 body)
is_closing置位后读写两边都不再推进,is_done 直接为真

两个 body 槽都只有一个位置:一条 HTTP/1 连接同一时刻最多读一个 body、写一个 body。

服务端用到的具体类型在 server/conn/http1.rs 里写成别名 Http1Dispatcher,Dispatch 填 dispatch::Server,T 填 ServerTransaction(即 role::Server)。serve_connection 把配置写进 Conn,再 Dispatcher::new(sd, conn),包进 Connection 返回,不 spawn 任何任务。Connection 和 Builder 的选项见 服务端 Builder 与 Connection。

body_tx 外面那层 SenderDropGuard 只做一件事:Dispatcher 被 drop 时如果还握着 body 发送端,就往 body 里送一个 IncompleteMessage 错误,让读 body 的一方看到「没读完」,而不是一个干净的结束。

Dispatch trait:服务端和客户端只差在这里

Dispatch:

pub(crate) trait Dispatch {
    type PollItem;
    type PollBody;
    type PollError;
    type RecvItem;
    fn poll_msg(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
    ) -> Poll<Option<Result<(Self::PollItem, Self::PollBody), Self::PollError>>>;
    fn recv_msg(&mut self, msg: crate::Result<(Self::RecvItem, IncomingBody)>)
        -> crate::Result<()>;
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), ()>>;
    fn should_poll(&self) -> bool;
}

poll_msg 给出「下一条要写出去的消息」,recv_msg 收下「刚读进来的消息」。两种实现对照:

方法Server(服务端)Client(客户端)
recv_msg拼出 Request,调 service.call(req),future 放进 in_flight读到的是响应:交给 callback
poll_msgpoll in_flight,响应好了拆成头和 body,清空 in_flight从请求通道 rx 取下一个请求和回调
poll_readyin_flight 有东西就 Pending回调的接收方还在就 Ready(Ok),已经放弃就 Ready(Err)
should_pollin_flight.is_some()callback.is_none()

服务端的 recv_msg 和 poll_ready:

        fn recv_msg(&mut self, msg: crate::Result<(Self::RecvItem, IncomingBody)>) -> crate::Result<()> {
            let (msg, body) = msg?;
            let mut req = Request::new(body);
            *req.method_mut() = msg.subject.0;
            *req.uri_mut() = msg.subject.1;
            *req.headers_mut() = msg.headers;
            *req.version_mut() = msg.version;
            *req.extensions_mut() = msg.extensions;
            let fut = self.service.call(req);
            self.in_flight.set(Some(fut));
            Ok(())
        }

        fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), ()>> {
            if self.in_flight.is_some() {
                Poll::Pending
            } else {
                Poll::Ready(Ok(()))
            }
        }

service.call 是同步调用,返回的 future 直接存在 Server 里,由 Dispatcher 在连接自己的任务里 poll。hyper 的 Service 没有 poll_ready,call(&self) 随时可调,见 Service 与 Body。recv_msg 收到 Err 时第一行的 msg? 原样把错误返回,所以服务端的连接错误最终从 Connection future 冒出来,而不是交给服务。

客户端那一列的细节(want、回调、请求通道)见 conn::http1 客户端。

poll_loop:一次 poll 最多转 16 圈

Connection::poll 调 Dispatcher::poll,再到 poll_catch 和 poll_inner。poll_inner 先 T::update_date()(服务端刷新缓存的 Date 头,见 解析与编码),然后进 poll_loop:

    fn poll_loop(&mut self, cx: &mut Context<'_>) -> Poll<crate::Result<()>> {
        // Limit the looping on this connection, in case it is ready far too
        // often, so that other futures don't starve.
        //
        // 16 was chosen arbitrarily, as that is number of pipelined requests
        // benchmarks often use. Perhaps it should be a config option instead.
        for _ in 0..16 {
            let _ = self.poll_read(cx)?;
            let write_ready = self.poll_write(cx)?.is_ready();
            let flush_ready = self.poll_flush(cx)?.is_ready();

            // If we can write more body and the connection is ready, we should
            // write again. If we return `Ready(Ok(())` here, we will yield
            // without a guaranteed wake-up from the write side of the connection.
            // This would lead to a deadlock if we also don't expect reads.
            let wants_write_again = self.can_write_again() && (write_ready || flush_ready);

            // ...
            let wants_read_again = self.conn.wants_read_again();

            // If we cannot write or read again, we yield and rely on the
            // wake-up from the connection futures.
            if !(wants_write_again || wants_read_again) {
                return Poll::Ready(Ok(()));
            }

            // ...
            if !wants_read_again && wants_write_again {
                // ...
                if self.poll_write(cx)?.is_pending() {
                    // ...
                    if self.conn.has_buffered_write() {
                        let _ = self.poll_flush(cx)?;
                    }
                    return Poll::Ready(Ok(()));
                }
            }
        }
        trace!("poll_loop yielding (self = {:p})", self);
        task::yield_now(cx).map(|never| match never {})
    }

每圈固定三步:poll_read、poll_write、poll_flush。三步的返回值大多被丢掉了(let _ =、.is_ready()),只有 ? 传出来的错误会中断循环。一圈结束后问两个问题:

  • 还要再写吗(wants_write_again):还有响应 body 没写完(can_write_again:body_rx 有东西且 Conn 处在 Writing::Body),并且这一圈写或刷新是 Ready。写端 Ready 说明 socket 当下可写,此时返回,没有任何东西保证会再唤醒这个任务,所以要接着转。
  • 还要再读吗(wants_read_again):Conn 在一个请求处理完、读写状态回到 Init 时,如果读缓冲里还有字节,会置一个 notify_read 标志(maybe_notify,见 Conn 的读写状态机)。读缓冲里的字节不会触发 socket 的可读事件,不再转一圈就没人来读它们。

两个都不需要,返回 Ready(Ok(())),由 poll_inner 判断连接是否结束;没结束就返回 Pending,等 IO、服务的 future、body 通道这些地方注册的 waker。

转满 16 圈还停不下来,就调 yield_now:

pub(crate) fn yield_now(cx: &mut Context<'_>) -> Poll<std::convert::Infallible> {
    cx.waker().wake_by_ref();
    Poll::Pending
}

先唤醒自己,再返回 Pending。在 tokio 上,任务在 poll 期间被唤醒,poll 返回后会被放回运行队列的尾部(见 poll 与唤醒),排在其他已经就绪的任务后面。返回类型是 Infallible,调用处 match never {} 把它变成任意类型,表示这里不可能返回 Ready。

什么情况下会一直有活干?读缓冲里一直有下一个请求。客户端把很多请求一次写过来(pipelining),每个请求的响应又是立即就绪的,那么每一圈都能处理完一个请求、notify_read 又置位,循环停不下来。注释说 16 是「随便选的」,取自基准测试常用的 pipelining 深度。这个上限是 2019 年的提交 8316f96d(v0.12.30,标题「force always-ready connections to yield after a few spins」)加上的,在那之前这里是一个不设上限的 loop。

hyper 不依赖运行时,所以要自己数圈。tokio 也有类似的保护:每个任务有协作预算,tokio 的 IO 资源用完预算就返回 Pending,见 协作式预算。两者互不知道对方,hyper 这 16 圈换了别的运行时也成立。

poll_loop 的计数观察见本页的「动脑筋」。

📝NOTE
冷知识:「还要再写吗」这段改了四次

wants_write_again 和后面那段二次检查不是一次写成的:

  1. #3952(v1.8.0)。PR 描述:一个 poll 时总是立即就绪、从不注册 waker 的 IO 会让连接卡住,「if the poll_write is demonstrating readiness to write and the connection has readiness to write, we should do that or else hyper may stall」。修法是写端 Ready 时继续转圈。
  2. #3977(v1.8.1)把它回滚了,原因是 #3976 报告 1.8.0 下「concurrent downloading of large files exhausts all CPU resources」。
  3. #3988(v1.10.0)重新提交。PR 说第一版没考虑「poll_flush always returns Poll::Ready」的无缓冲 IO:刷新总是 Ready,第一版就会一直转。第二版只在「因为写端看起来可写才继续」时再 poll_write 一次,这次还是 Pending 就退出,因为「the writer's waker will schedule us」。也就是上面代码里 !wants_read_again && wants_write_again 那段。
  4. #4143(v1.11.1)补了一个洞:第二次 poll_write 可能先把 body 的结尾(比如 chunked 的终止块)写进缓冲区,再在下一条消息上 Pending,直接返回会把这几个字节留在缓冲区里,等待的唤醒都是读方向的,没人去刷。提交说明里写的现象是对端「receives the body but never the terminator」,大约每 60 万条连接出现一次。修法就是返回前那句 if self.conn.has_buffered_write() 再刷一次。

poll_read:读头、读 body,或者盯着连接

poll_read 是一个循环,按状态走三条路:

        loop {
            if self.is_closing {
                return Poll::Ready(Ok(()));
            } else if self.conn.can_read_head() {
                ready!(self.poll_read_head(cx))?;
            } else if let Some(mut body) = self.body_tx.take() {
                if self.conn.can_read_body() {
                    match body.poll_ready(cx) {
                        Poll::Ready(Ok(())) => (),
                        Poll::Pending => {
                            self.body_tx.set(body);
                            return Poll::Pending;
                        }
                        Poll::Ready(Err(_canceled)) => {
                            // user doesn't care about the body
                            // so we should stop reading
                            trace!("body receiver dropped before eof, draining or closing");
                            self.conn.poll_drain_or_close_read(cx);
                            continue;
                        }
                    }
                    match self.conn.poll_read_body(cx) {
                        // ... 数据块 try_send_data,trailers try_send_trailers
                    }
                } else {
                    // just drop, the body will close automatically
                }
            } else {
                return self.conn.poll_read_keep_alive(cx);
            }
        }
  • 读头:Conn 处在 Reading::Init 时走 poll_read_head。
  • 读 body:手里有 body 发送端,先 body.poll_ready。通道里只有一个槽,上一块还没被服务取走就是 Pending,不去 socket 上读下一块,这是 body 方向的背压。Expect: 100-continue 的请求例外地从「不要数据」开始,服务第一次 poll body 才变成要(Wants::EXPECT 就是传给通道的这个开关)。通道的细节见 Incoming 与 body 通道。接收方已经 drop,走 poll_drain_or_close_read:能便宜地读完就读完,否则关掉读端。
  • 都不是:当前请求的头和 body 都读完了,Conn::poll_read_keep_alive 盯着连接,主要是发现对端提前关闭。服务端在这里会遇到「半关闭」,见 Conn 的读写状态机。

poll_read_head 的开头先问 Dispatch 能不能收:

    fn poll_read_head(&mut self, cx: &mut Context<'_>) -> Poll<crate::Result<()>> {
        // can dispatch receive, or does it still care about other incoming message?
        if let Ok(()) = ready!(self.dispatch.poll_ready(cx)) {
        } else {
            trace!("dispatch no longer receiving messages");
            self.close();
            return Poll::Ready(Ok(()));
        }

        // dispatch is ready for a message, try to read one
        match ready!(self.conn.poll_read_head(cx)) {
            Some(Ok((mut head, body_len, wants))) => {
                let body = match body_len {
                    DecodedLength::ZERO => IncomingBody::empty(),
                    other => {
                        let (tx, rx) =
                            IncomingBody::new_channel(other, wants.contains(Wants::EXPECT));
                        self.body_tx.set(tx);
                        rx
                    }
                };
                if wants.contains(Wants::UPGRADE) {
                    let upgrade = self.conn.on_upgrade();
                    // ...
                    head.extensions.insert(upgrade);
                }
                self.dispatch.recv_msg(Ok((head, body)))?;
                Poll::Ready(Ok(()))
            }
            // ...
        }
    }

读到一个头,按 Conn 算出的 body 长度建 body:长度为 0 给一个空的 Incoming,否则建一对通道,发送端留在 body_tx。Wants::EXPECT 让通道从「不要数据」开始,Wants::UPGRADE 让请求的 extensions 里多一个 OnUpgrade(见 协议升级)。然后 recv_msg,服务被调用。

poll_write:等响应,写头,写 body

poll_write 也是一个循环,分支的条件就是「现在能写什么」:

            } else if self.body_rx.is_none()
                && self.conn.can_write_head()
                && self.dispatch.should_poll()
            {
                if let Some(msg) = ready!(Pin::new(&mut self.dispatch).poll_msg(cx)) {
                    let (head, body) = msg.map_err(crate::Error::new_user_service)?;

                    let body_type = if body.is_end_stream() {
                        self.body_rx.set(None);
                        None
                    } else {
                        let btype = body
                            .size_hint()
                            .exact()
                            .map(BodyLength::Known)
                            .or(Some(BodyLength::Unknown));
                        self.body_rx.set(Some(body));
                        btype
                    };
                    self.conn.write_head(head, body_type);
                } else {
                    self.close();
                    return Poll::Ready(Ok(()));
                }
            } else if !self.conn.can_buffer_body() {
                ready!(self.poll_flush(cx))?;
            } else {
                // ... 从 body_rx poll_frame,数据块 write_body / write_body_and_end,
                // trailers write_trailers,结束 end_body
            }
  • 写头:没有正在写的 body、Conn 处在 Writing::Init、服务端有 in_flight,就 poll 服务的 future。服务的错误在这里包成 new_user_service 往外传,连接随之结束。拿到响应后,is_end_stream() 为真就当作没有 body;否则 size_hint().exact() 有值就是 BodyLength::Known(写 Content-Length),没有就是 Unknown(写 chunked)。头怎么编码见 解析与编码,body 怎么编码见 body 的解码与编码。
  • 缓冲区满了:先 poll_flush,Pending 就返回。写缓冲的策略见 读写缓冲。
  • 写 body:poll_frame 一次取一帧。数据帧之后 is_end_stream() 为真,就 write_body_and_end 把最后一块和结尾一起编码,省掉一次单独的结束。空数据块直接丢掉。

poll_write 只往缓冲区里写,真正写到 socket 是 poll_flush,它调 Conn::poll_flush。刷新成功后 Conn 会检查这条消息是否已经读写完毕,读写都完了才把状态拨回 Init,下一个请求才能开始读。下一节的实验就是这句话的后果。

同一条连接上的请求严格串行

客户端一次写三个请求(pipelining),服务每个请求睡 200ms:

hyper = { version = "=1.12.0", features = ["server", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio"] }
http-body-util = "0.1"
tokio = { version = "=1.54.1", features = ["full"] }
use std::{convert::Infallible, time::{Duration, Instant}};
use http_body_util::Full;
use hyper::{body::{Bytes, Incoming}, server::conn::http1, service::service_fn, Request, Response};
use hyper_util::rt::TokioIo;
use tokio::{io::{AsyncReadExt, AsyncWriteExt}, net::{TcpListener, TcpStream}};

#[tokio::main]
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();
    let svc = service_fn(move |req: Request<Incoming>| async move {
      let path = req.uri().path().to_string();
      println!("{:>4}ms server: handler {path} start", ms());
      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))))
    });
    http1::Builder::new().serve_connection(TokioIo::new(stream), svc).await.unwrap();
  });

  let mut client = TcpStream::connect(addr).await.unwrap();
  // three requests in a single write: pipelining
  client
    .write_all(b"GET /a HTTP/1.1\r\nHost: x\r\n\r\nGET /b HTTP/1.1\r\nHost: x\r\n\r\nGET /c HTTP/1.1\r\nHost: x\r\n\r\n")
    .await
    .unwrap();
  println!("{:>4}ms client: wrote 3 requests", ms());
  let mut buf = [0u8; 4096];
  let mut seen = 0;
  while seen < 3 {
    let n = client.read(&mut buf).await.unwrap();
    let text = String::from_utf8_lossy(&buf[..n]);
    let bodies: Vec<&str> = text.split("\r\n\r\n").skip(1).map(|s| s.split("HTTP/").next().unwrap()).collect();
    seen += bodies.len();
    println!("{:>4}ms client: read {n} bytes, bodies {bodies:?}", ms());
  }
}

M2 Max、rustc 1.99.0、--release。连跑 4 次,顺序和间隔相同,毫秒数在几毫秒内浮动:

   0ms client: wrote 3 requests
   0ms server: handler /a start
 203ms server: handler /a done
 203ms server: handler /b start
 203ms client: read 77 bytes, bodies ["/a"]
 407ms server: handler /b done
 407ms server: handler /c start
 407ms client: read 77 bytes, bodies ["/b"]
 610ms server: handler /c done
 611ms client: read 77 bytes, bodies ["/c"]

三个请求在 0ms 就都到了服务端的读缓冲区,但服务 /b 要等 /a 的响应写完才被调用,总共 600ms。pipelining 在 hyper 的服务端只省掉了客户端等响应再发下一个请求的往返,请求本身仍然一个一个处理。一个慢请求会拖住同一条连接上后面所有的请求,这条在「常见坑」里还会出现。要在一条连接上并发,得用 HTTP/2,每个流一个任务,见 HTTP/2 服务端。

卡住下一个请求的是哪一道闸

服务端串行有两道闸:

  1. Dispatch::poll_ready:in_flight 有东西就 Pending,poll_read_head 不去读下一个头。
  2. Conn::can_read_head:只有 Reading::Init 时才读头,而读端回到 Init 要等读写两端都走到 KeepAlive,也就是响应连同 body 都写完、刷出去(见 Conn 的读写状态机)。

in_flight 在 poll_msg 拿到响应头时就清空了,那时 body 可能还没开始写。如果只有第一道闸,一个流式响应写到一半,下一个请求就会被调用。下面让 /a 的 body 分两块,第二块晚 300ms:

hyper = { version = "=1.12.0", features = ["server", "http1"] }
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::stream;
use http_body_util::StreamBody;
use hyper::{body::{Bytes, Frame, Incoming}, server::conn::http1, service::service_fn, Request, Response};
use hyper_util::rt::TokioIo;
use tokio::{io::{AsyncReadExt, AsyncWriteExt}, net::{TcpListener, TcpStream}};

#[tokio::main]
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();
    let svc = service_fn(move |req: Request<Incoming>| async move {
      let path = req.uri().path().to_string();
      println!("{:>4}ms server: handler {path} called", ms());
      // /a: two chunks, the second one 300ms later; /b: one chunk
      let n = if path == "/a" { 2 } else { 1 };
      let body = stream::unfold(0, move |i| async move {
        if i == n {
          return None;
        }
        if i > 0 {
          tokio::time::sleep(Duration::from_millis(300)).await;
        }
        println!("{:>4}ms server: body chunk {i}", ms());
        Some((Ok::<_, Infallible>(Frame::data(Bytes::from(format!("{i};")))), i + 1))
      });
      Ok::<_, Infallible>(Response::new(StreamBody::new(body)))
    });
    http1::Builder::new().serve_connection(TokioIo::new(stream), svc).await.unwrap();
  });

  let mut client = TcpStream::connect(addr).await.unwrap();
  client
    .write_all(b"GET /a HTTP/1.1\r\nHost: x\r\n\r\nGET /b HTTP/1.1\r\nHost: x\r\n\r\n")
    .await
    .unwrap();
  let mut buf = [0u8; 4096];
  let mut all = String::new();
  while !all.ends_with("0;\r\n0\r\n\r\n") || all.matches("HTTP/1.1").count() < 2 {
    let n = client.read(&mut buf).await.unwrap();
    all.push_str(&String::from_utf8_lossy(&buf[..n]));
  }
  println!("{:>4}ms client: both responses complete", ms());
}

连跑 4 次,顺序相同,毫秒数差 1ms 以内:

   0ms server: handler /a called
   0ms server: body chunk 0
 303ms server: body chunk 1
 304ms server: handler /b called
 304ms server: body chunk 0
 304ms client: both responses complete

/a 的响应头和第一块在 0ms 就写出去了,in_flight 也在那时清空,但 /b 直到 /a 的 body 结束(303ms)才被调用。真正起作用的是第二道闸。

📝NOTE
没有蠢问题

问: 既然 Conn 的状态已经保证了串行,服务端的 poll_ready 检查 in_flight 还有什么用?

答: 对服务端来说它是一道保险,正常路径上碰不到。源码里能走到 can_read_head() 为真的时刻只有两个:连接刚建立,或者 State::idle 刚把读写拨回 Init。前者 in_flight 还没放过东西;后者要求写端到过 KeepAlive,而写端离开 Init 要先经过 write_head,write_head 之前 poll_msg 已经清空了 in_flight。这个方法真正有分量的是客户端的实现:它检查发请求的一方是否已经放弃(回调的接收端被 drop),放弃了就返回 Err,poll_read_head 随即关闭连接,不再读一个没人要的响应。

结束:is_done、Shutdown 与 Upgrade

每次 poll_loop 返回 Ready,poll_inner 都问一次 is_done:

    fn is_done(&self) -> bool {
        if self.is_closing {
            return true;
        }

        let read_done = self.conn.is_read_closed();

        if !T::should_read_first() && read_done {
            // a client that cannot read may was well be done.
            true
        } else {
            let write_done = self.conn.is_write_closed()
                || (!self.dispatch.should_poll() && self.body_rx.is_none());
            read_done && write_done
        }
    }

服务端要读写两端都关了才算结束;写端「关了」也包括「没有在途请求、也没有在写的 body」。客户端只要读端关了就结束。

结束后 poll_inner 分两条路:

        if self.is_done() {
            if let Some(pending) = self.conn.pending_upgrade() {
                self.conn.take_error()?;
                return Poll::Ready(Ok(Dispatched::Upgrade(pending)));
            } else if should_shutdown {
                ready!(self.conn.poll_shutdown(cx)).map_err(crate::Error::new_shutdown)?;
            }
            self.conn.take_error()?;
            Poll::Ready(Ok(Dispatched::Shutdown))
        } else {
            Poll::Pending
        }

Dispatched 只有两个变体。Shutdown 之前先对 IO 调 poll_shutdown(TCP 上就是发 FIN)。Conn 里还挂着一个升级请求时返回 Upgrade(pending),不关 IO,因为 IO 马上要交给升级后的协议。拿到 Upgrade 后怎么处理由外层决定:普通的 Connection 调 pending.manual() 让等待升级的一方收到错误,with_upgrades() 返回的 UpgradeableConnection 才会把 IO 交出去,见 协议升级。poll_without_shutdown 走同一个 poll_catch,只是 should_shutdown 为 false。

出错时 poll_catch 先给正在读的 body 送一个「connection error」,再把错误交给 dispatch.recv_msg(Err(e)):客户端能把它交给某个在等的请求,就以 Ok(Shutdown) 结束;服务端的 recv_msg 原样返回错误,Connection 就以这个错误结束。

❗IMPORTANT
动脑筋:一次 poll 处理几个请求?

40 个请求在服务端开始读之前就已经全部在 socket 里,客户端随后关闭写方向。服务立即返回空响应。把 Connection 包进一个 Probe,每次被 poll 后打印这一次服务被调用了几次、future 有没有在 poll 里唤醒自己:

hyper = { version = "=1.12.0", features = ["server", "http1"] }
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,
  pin::Pin,
  sync::{atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, Arc},
  task::{Context, Poll, Wake, Waker},
};
use http_body_util::Empty;
use hyper::{body::{Bytes, Incoming}, server::conn::http1, service::service_fn, Request, Response};
use hyper_util::rt::TokioIo;
use tokio::{io::{AsyncReadExt, AsyncWriteExt}, net::{TcpListener, TcpStream}};

// A waker that records whether it was woken, then forwards to the real one.
struct Flag(AtomicBool, Waker);
impl Wake for Flag {
  fn wake(self: Arc<Self>) {
    self.wake_by_ref();
  }
  fn wake_by_ref(self: &Arc<Self>) {
    self.0.store(true, SeqCst);
    self.1.wake_by_ref();
  }
}

// Wraps the Connection future and reports, for every poll, how many
// requests the service saw and whether the future woke itself.
struct Probe<F> {
  inner: Pin<Box<F>>,
  calls: Arc<AtomicUsize>,
  polls: usize,
}
impl<F: Future> Future for Probe<F> {
  type Output = F::Output;
  fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<F::Output> {
    let before = self.calls.load(SeqCst);
    let flag = Arc::new(Flag(AtomicBool::new(false), cx.waker().clone()));
    let waker = Waker::from(flag.clone());
    let r = self.inner.as_mut().poll(&mut Context::from_waker(&waker));
    self.polls += 1;
    let handled = self.calls.load(SeqCst) - before;
    if handled > 0 || r.is_ready() {
      println!(
        "poll #{}: {handled} requests, woke itself: {}, {}",
        self.polls,
        flag.0.load(SeqCst),
        if r.is_ready() { "Ready" } else { "Pending" }
      );
    }
    r
  }
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
  let mut client = TcpStream::connect(listener.local_addr().unwrap()).await.unwrap();
  // 40 pipelined requests are already in the socket before the server reads
  let mut reqs = Vec::new();
  for i in 0..40 {
    reqs.extend_from_slice(format!("GET /{i} HTTP/1.1\r\nHost: x\r\n\r\n").as_bytes());
  }
  client.write_all(&reqs).await.unwrap();
  client.shutdown().await.unwrap(); // done sending; lets the connection finish

  let (stream, _) = listener.accept().await.unwrap();
  let calls = Arc::new(AtomicUsize::new(0));
  let c = calls.clone();
  let svc = service_fn(move |_: Request<Incoming>| {
    c.fetch_add(1, SeqCst);
    async { Ok::<_, Infallible>(Response::new(Empty::<Bytes>::new())) }
  });
  let conn = http1::Builder::new()
    .half_close(true) // see the conn-state page for why
    .serve_connection(TokioIo::new(stream), svc);
  let reader = tokio::spawn(async move {
    let mut out = Vec::new();
    client.read_to_end(&mut out).await.unwrap();
    String::from_utf8_lossy(&out).matches("HTTP/1.1 200").count()
  });
  let res = Probe { inner: Box::pin(conn), calls, polls: 0 }.await;
  println!("connection: {res:?}, client got {} responses", reader.await.unwrap());
}

会打印几行 poll #…,每行的请求数是多少?哪几次 poll 唤醒了自己?答案在本页末尾。(half_close(true) 为什么必须开,见 Conn 的读写状态机 的「对端半关闭」。)

动脑筋答案

M2 Max、rustc 1.99.0、--release,连跑 5 次输出相同:

poll #1: 16 requests, woke itself: true, Pending
poll #2: 16 requests, woke itself: true, Pending
poll #3: 8 requests, woke itself: false, Ready
connection: Ok(()), client got 40 responses
  • 第 1、2 次 poll:每圈处理一个请求。读头、调服务、响应立即就绪、写头、刷新;刷新后读写回到 Init,读缓冲里还有下一个请求,notify_read 置位,wants_read_again 为真,接着转。转满 16 圈走到 yield_now,wake_by_ref 叫醒了自己(woke itself: true),返回 Pending。
  • 第 3 次 poll:剩下的 8 个请求处理完,读缓冲空了。Conn 去 socket 读,读到 EOF,连接是空闲的,直接关闭。poll_loop 正常返回,is_done 为真,Connection 完成。这次没有走到 yield_now,所以 woke itself: false。

Probe 只在「这次处理了请求或者完成了」时打印,但 poll # 是每次 poll 都加一的,编号连续,说明中间没有别的 poll:三次 poll 处理完 40 个请求,两次让出都是 hyper 自己发起的,不是在等 IO。

小结

  • Dispatcher 持有一个 Conn、一个 Dispatch 实现和两个 body 槽;服务端和客户端共用它,差别只在 Dispatch(Server 调服务、存 in_flight;Client 从通道取请求、把响应交给回调)和 T。
  • poll_loop 每圈 poll_read → poll_write → poll_flush;还能写(body 没写完且写端就绪)或还要读(读缓冲里有下一个请求)就继续,否则返回等唤醒。最多 16 圈,转满就 wake_by_ref 再 Pending,把线程让给别的任务。
  • poll_read 读头时先问 Dispatch::poll_ready,读 body 时先问 body 通道要不要数据;poll_write 等服务的 future,按 is_end_stream 和 size_hint 决定 body 长度,再逐帧写。
  • 服务端一条连接上的请求严格串行:下一个请求要等上一个响应连 body 一起写完、刷出,Conn 回到 Init 才会被读。pipelining 只省了往返,不带来并发。
  • is_done 后返回 Dispatched::Shutdown(先关 IO)或 Dispatched::Upgrade(不关 IO,等外层交出去);服务端的错误从 Connection future 返回。

💬 关联讨论

发起讨论
加载中...