Rust in Everything
基于 hyper 1.12.0

Incoming 与 body 通道

主线位置:两条主线都有。服务端主线上,Dispatcher 读到请求头后建一对通道,Request<Incoming> 交给服务,请求 body 的字节由连接任务解码后一块一块塞进通道。客户端主线上方向相反,Response<Incoming> 里的响应 body 也是这样送来的。

flowchart TB
  sock["socket 字节"] --> dec["Decoder 解码一步"]
  dec --> pr["Dispatcher::poll_read"]
  pr --> ready{"tx.poll_ready"}
  ready -- Ready --> slot["try_send_data 放进槽"]
  ready -- Pending --> stop["不读 socket,等唤醒"]
  ready -- "Err:接收端没了" --> drain["poll_drain_or_close_read"]
  slot --> rx["Incoming::poll_frame 取走"]
  rx --> wake["唤醒发送端"]
  wake --> pr
  drain --> kept{"一步读完了?"}
  kept -- 是 --> ka["连接照常复用"]
  kept -- 否 --> closed["close_read,响应后关连接"]

Incoming 是 hyper 自己唯一的 Body 实现。它在两个方向上都是「收到的 body」:服务端请求里的、客户端响应里的。HTTP/1 下它是一个通道的接收端,发送端在连接的 Dispatcher 手里;HTTP/2 下它直接包着 h2 的 RecvStream。这一页讲这个通道,以及接收端提前没了时连接怎么办。字节怎么从 socket 解码成块,见 body 的解码与编码;Body trait 本身见 Service 与 Body。

Incoming:一个类型,几种来源

Incoming 只有一个私有字段,所有区别都在 Kind 里:

pub struct Incoming {
    kind: Kind,
}

enum Kind {
    Empty,
    #[cfg(all(feature = "http1", any(feature = "client", feature = "server")))]
    Chan {
        content_length: DecodedLength,
        rx: chan::Receiver,
    },
    #[cfg(all(feature = "http2", any(feature = "client", feature = "server")))]
    H2 {
        content_length: DecodedLength,
        data_done: bool,
        ping: ping::Recorder,
        recv: h2::RecvStream,
    },
    #[cfg(feature = "ffi")]
    Ffi(crate::ffi::UserBody),
}
变体什么时候出现poll_frame 做什么
EmptyHTTP/1 算出的 body 长度是 0(poll_read_head 里 DecodedLength::ZERO => IncomingBody::empty())直接 Ready(None)
ChanHTTP/1 有 body从通道取一块;通道结束后再取 trailers
H2HTTP/2 的每个流poll_data 取数据,取完再 poll_trailers
FfiC API,要 --cfg hyper_unstable_ffi(见 源码地图)交给 UserBody

构造函数全是 pub(crate):new_channel、empty、h2。用户拿不到发送端,也造不出一个 Incoming,只能从 hyper 手里收到它。要自己造 body(响应 body、客户端请求 body)用 http-body-util 的类型,见 http-body-util。

Chan 和 H2 都带一个 content_length,poll_frame 每交出一块数据就减掉它的长度:

            Kind::Chan {
                content_length: len,
                rx,
            } => {
                if let Some(chunk) = ready!(rx.poll_next(cx)?) {
                    len.sub_if(chunk.len() as u64);
                    return Poll::Ready(Some(Ok(Frame::data(chunk))));
                }
                Poll::Ready(rx.take_trailers().map(Frame::trailers).map(Ok))
            }

所以 size_hint 给的是还剩多少,不是总长;is_end_stream 在 Chan 上就是「剩余长度等于 0」。本页「动脑筋」的输出里能看到 size_hint 随着每一帧变小。

trailers 不走数据槽。数据通道结束(poll_next 返回 None)后,take_trailers 去共享状态里拿,拿到就再交出一帧 trailers。HTTP/1 请求 trailers 怎么解码,见 body 的解码与编码 的 trailers 一节。

📝NOTE
冷知识:Incoming 原来叫 Body,中间还叫过 Recv

hyper 0.14 收到的 body 和用户自己造的 body 是同一个类型 hyper::Body,既能 Body::from("..."),也能 Body::channel()。1.0 把 trait 的名字 Body 留给 http_body::Body,这个结构体先被临时改名为 Recv,再在 issue #2971(「Determine name for Recv body type」)里讨论正式名字。issue 写明了它的用途:「an implementation of Body to represent bodies received from a remote」,并且「It is not meant to be constructed by users as a "default" implementation.」候选名里有 Recv、RecvStream、Streaming、Incoming、Remote、Wire。#3022(「rename Recv to Incoming」)定下了现在的名字,最早出现在 v1.0.0-rc.1。上面那些 pub(crate) 构造函数,就是这句「不是给用户构造的」的落实。

DecodedLength:一个 u64 装下长度和两种「不知道」

content_length 的类型是 DecodedLength,一个 u64 的新类型。最大的两个值被挪作他用:

#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) struct DecodedLength(u64);

// ...
const MAX_LEN: u64 = u64::MAX - 2;

impl DecodedLength {
    pub(crate) const CLOSE_DELIMITED: DecodedLength = DecodedLength(u64::MAX);
    pub(crate) const CHUNKED: DecodedLength = DecodedLength(u64::MAX - 1);
    pub(crate) const ZERO: DecodedLength = DecodedLength(0);
值意思谁产生它
0 到 MAX_LEN确切长度Content-Length,经 checked_new:超过 u64::MAX - 2 是解析错误 TooLarge
CHUNKED长度不知道,靠分块编码分界HTTP/1 的 Transfer-Encoding: chunked;HTTP/2 没有 Content-Length 时(From<Option<u64>> 把 None 也映射成它)
CLOSE_DELIMITED读到连接关闭为止只在客户端:响应既没有 Content-Length 也没有 chunked(Client::decoder)。请求 body 不可能这样分界,服务端缺省是 ZERO

ZERO 只是 0 的一个名字,不是特殊值。两个特殊值对外都表现为「不知道」:into_opt 把它们变成 None,size_hint 就是默认的 SizeHint(下限 0,没有上限);sub_if 遇到它们什么也不减。

用一个 u64 而不是 enum { Known(u64), Chunked, CloseDelimited },DecodedLength 是 8 字节、Copy,== DecodedLength::ZERO 一条比较就能判断「没有 body」。代价是 u64::MAX - 1 和 u64::MAX 这两个长度没法表示,checked_new 把它们挡在解析阶段。HTTP/2 那边还有一个小修正:Incoming::h2 发现流在头之后就已经结束(recv.is_end_stream()),就把「不知道」改成 ZERO。

chan.rs:只有一个槽的通道

HTTP/1 的 Chan 背后是 body/chan.rs,hyper 自己写的一个通道,两端共享一个 Shared:

struct Shared {
    state: Mutex<State>,
    sender_waker: AtomicWaker,
    receiver_waker: AtomicWaker,
}

struct State {
    item: Option<Result<Bytes, crate::Error>>,
    // An error must not displace data which was already accepted. The old
    // mpsc channel achieved this by sending the error from a cloned sender.
    pending_error: Option<crate::Error>,
    trailers: Option<HeaderMap>,
    sender_open: bool,
    receiver_open: bool,
    want: bool,
}

State 里的 item 就是那一个槽:最多放一块数据(或一个错误)。没有队列。

字段作用
item唯一的数据槽
pending_error槽里已经有数据时到来的错误,排在数据后面,不把数据挤掉
trailerstrailers 单独放,不占槽
sender_open / receiver_open两端各自是否还在,Drop 时置 false
want接收端是否已经要过数据;只对 Expect: 100-continue 有意义,见下

两把 AtomicWaker 各存一端的 waker,锁只护着 State,唤醒在放锁之后做。

发送端:poll_ready 与 try_send_data

Sender::poll_ready:

    pub(crate) fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<crate::Result<()>> {
        self.shared.sender_waker.register(cx.waker());
        let state = self.shared.state.lock().panic_if_poisoned();
        if !state.receiver_open {
            Poll::Ready(Err(crate::Error::new_closed()))
        } else if state.want && state.item.is_none() && state.pending_error.is_none() {
            Poll::Ready(Ok(()))
        } else {
            Poll::Pending
        }
    }

三种结果:接收端没了是 Err;接收端要数据、槽是空的才 Ready(Ok);否则 Pending。try_send_data 再检查一遍同样的条件,把块放进 item,然后唤醒接收端。条件不满足就把块原样退回(Err(chunk)),不会阻塞。

接收端:poll_next

Receiver::poll_next:

        self.shared.receiver_waker.register(cx.waker());
        let mut state = self.shared.state.lock().panic_if_poisoned();
        let wake_sender = if !state.want {
            state.want = true;
            true
        } else {
            false
        };
        if let Some(item) = state.item.take() {
            drop(state);
            self.shared.sender_waker.wake();
            return Poll::Ready(Some(item));
        }
        if let Some(err) = state.pending_error.take() {
            // ...
            return Poll::Ready(Some(Err(err)));
        }
        let sender_open = state.sender_open;
        drop(state);
        if wake_sender {
            self.shared.sender_waker.wake();
        }
        if sender_open {
            Poll::Pending
        } else {
            self.terminated = true;
            Poll::Ready(None)
        }

取走一块就唤醒发送端,告诉它槽空了。槽里没东西、发送端也已经 drop,就是 body 正常结束:Ready(None)。发送端 drop 前没有送错误,接收端看到的就是干净的结束,所以「没读完」必须显式地以错误送过来,下面「错误怎么送到 Incoming」一节讲这几处。

want:只给 Expect: 100-continue 用

channel 建通道时 want: !wanter。Dispatcher 只在请求带 Expect: 100-continue 时传 wanter = true(wants.contains(Wants::EXPECT)),其余情况 want 一开始就是 true,而 poll_next 只会把它从 false 改成 true,不会改回去。所以:

  • 普通的 body:want 永远是 true,poll_ready 只看槽空不空。处理函数还没开始读,Dispatcher 也会先读一块放进槽里。
  • Expect: 100-continue 的 body:want 从 false 开始,poll_ready 一直 Pending,Dispatcher 不去读 body,也就不发 100 Continue;处理函数第一次 poll body 时 poll_next 把 want 置真并唤醒发送端,Dispatcher 这才调 poll_read_body,100 Continue 在那里写出(见 Conn 的读写状态机 的「Expect: 100-continue」)。

所以 body 方向的背压靠的是「槽满了就不读」,不是 want。want 只是一个一次性的开关,把「客户端发不发 body」的决定推迟到处理函数第一次读 body 的时候。客户端主线上 Expect 只由服务端解析产生,响应 body 的通道总是 wanter = false。

💡TIP
打个比方:后厨和传菜口

后厨(Dispatcher)往传菜口(item)上放菜,传菜口只放得下一盘。上一盘没被服务员(读 body 的处理函数)端走,后厨就不做下一盘,也就不从冷库(socket)里取料。服务员端走一盘时按一下铃(sender_waker.wake()),后厨听到铃声再做下一盘。

Expect: 100-continue 是一种特别的单子:顾客说「你们真要做我再把料送来」。后厨先不开火(want = false),等服务员第一次来传菜口问(第一次 poll_next),才告诉顾客「送来吧」(100 Continue)。

服务员中途走了(Incoming 被 drop),后厨看一眼锅里:剩下的一步就能做完,就做完倒掉,接着接下一单(连接复用);做不完就关门(关连接)。

比方到这里为止:在服务端,后厨和服务员通常是同一个人。处理函数的 future 是在连接自己的任务里被 poll 的(Dispatcher::poll_write 里的 poll_msg),「按铃」唤醒的就是同一个任务,铃声要等这个人把手头的事做完、回到 poll_loop 的下一圈才会被听到。

Dispatcher 这一侧

发送端由 Dispatcher 持有,放在 body_tx 里(外面包着一层 SenderDropGuard)。poll_read 读 body 的那一支:

            } 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) {
                        Poll::Ready(Some(Ok(frame))) => {
                            if frame.is_data() {
                                let chunk = frame.into_data().unwrap_or_else(|_| unreachable!());
                                match body.try_send_data(chunk) {
                                    Ok(()) => {
                                        self.body_tx.set(body);
                                    }
                                    // ...
                                }
                            // ... trailers:try_send_trailers
                            }
                        }
                        Poll::Ready(None) => {
                            // just drop, the body will close automatically
                        }
                        Poll::Pending => {
                            self.body_tx.set(body);
                            return Poll::Pending;
                        }
                        Poll::Ready(Some(Err(e))) => {
                            body.send_error(crate::Error::new_body(e));
                        }
                    }
                } else {
                    // just drop, the body will close automatically
                }

顺序是「先问槽,再读 socket」:poll_ready 不是 Ready(Ok) 就不调 poll_read_body。poll_read_body 每次让 Decoder 解码一步:读缓冲里有字节就切走一段,没有就从 socket 读一次(见 body 的解码与编码)。所以从 socket 读出来的 body 字节,最多比处理函数多一块:槽里那一块。

body 读完时 Conn 的读端变成 KeepAlive,can_read_body() 为假,走最后的 else:发送端直接 drop。接收端取完槽里最后一块后看到 sender_open == false,返回 None。

Expect 之外,「poll_ready 返回 Pending」的唯一原因就是槽满了。注册的 waker 是连接任务的,处理函数取走一块时唤醒它,poll_loop 下一圈接着读。

📝NOTE
没有蠢问题

问: 一个槽、每块都要一次唤醒,上传大文件岂不是要来回切换成千上万次?

答: 块不是固定大小的。Length 解码器每次把读缓冲里的字节整段切走(split_to,不拷贝),而读缓冲每次从 socket 读多少由 ReadStrategy::Adaptive 决定:读满了下次就翻倍,上限是 max_buf_size(默认约 408 KiB,见 读写缓冲)。本页「动脑筋」里 256 KiB 的 body 只分成了 6 块:8140、16384、32768、65536、131072、8244 字节。块数大致是 body 大小除以几百 KiB,不是除以 8 KiB。

错误怎么送到 Incoming

接收端只有在读到错误时才知道 body 没读完,所以每条「没读完」的路都要主动送一个错误:

情况谁送送什么
解码出错(chunk 格式错、Content-Length 没读够连接就断了)poll_read 的 Ready(Some(Err(e))) 分支new_body(e),随后发送端 drop
连接出错poll_catchnew_body("connection error")
Dispatcher 被 drop(比如运行时关了)SenderDropGuard 的 Dropnew_incomplete()

send_error 在槽空时把错误放进槽,槽里已经有数据时放进 pending_error。poll_next 先交 item 再交 pending_error,接收端不会因为一个错误丢掉已经送达的数据。注释里说的「old mpsc channel」,是下面这条冷知识里的旧实现。

📝NOTE
冷知识:1.12.0 才换成这个通道

v1.11.1 的 Kind::Chan 有四个字段:content_length、want_tx: watch::Sender、data_rx: mpsc::Receiver<Result<Bytes, crate::Error>>、trailers_rx: oneshot::Receiver<HeaderMap>,数据用 futures-channel 的 mpsc::channel(0),trailers 一个 oneshot,want 是 hyper 自己的 common/watch.rs。#4168(「perf(body): simpler custom Incoming channel」,首次发布于 v1.12.0)把它们合成现在的 chan.rs,删掉了 common/watch.rs。PR 说明:「The previous channel variant for Incoming used several individual channels to handle the various mechanisms: sending data, signaling error/abort, sending trailers, waiting for optional expect-101 polling. Each individual channel would allocate its own internal shared state in an Arc.」改后「putting all the required shared pieces in a single Arc. This resulted in less allocations, and less places to potentially store wakers.」PR 给的微基准:建通道再 drop 从 400 ns 到 130–139 ns,一次数据交接从 229 ns 到 147–155 ns,Expect 等待从 492 ns 到 267–298 ns。同一版本里还有两条后续:#4185「remove unused futures-channel from http1 feature」,以及 #4225「skip waking a closed peer when a channel end drops」,就是现在两个 Drop 里「对端还开着才唤醒」的判断。

处理函数不读 body:一步读完,否则关连接

处理函数不读 body 就回响应,或者读了一半就放弃,Incoming 跟着 Request 一起被 drop,receiver_open 变成 false。Dispatcher 下一次 poll_ready 得到 Err,调 poll_drain_or_close_read:

    /// If the read side can be cheaply drained, do so. Otherwise, close.
    pub(super) fn poll_drain_or_close_read(&mut self, cx: &mut Context<'_>) {
        if let Reading::Continue(decoder) = &mut self.state.reading {
            // skip sending the 100-continue
            // just move forward to a read, in case a tiny body was included
            self.state.reading = Reading::Body(decoder.clone());
        }

        let _ = self.poll_read_body(cx);

        // If still in Reading::Body, just give up
        match self.state.reading {
            Reading::Init | Reading::KeepAlive => {
                trace!("body drained")
            }
            _ => self.close_read(),
        }
    }

「cheaply drained」就是只调一次 poll_read_body,解码一步,结果直接丢掉。这一步之后 body 读完了(读端到了 KeepAlive),连接照常复用;没读完,close_read 把读端设为 Closed、KA 设为 Disabled,响应写完后整条连接关闭(try_keep_alive 见 Conn 的读写状态机)。不循环、不设字节上限,「一步」有多大取决于解码器:

  • Length:读缓冲里有字节就切走一段(最多到 body 结尾),读缓冲空了就从 socket 读一次。
  • Chunked:最多交出一个 chunk 的数据。剩下的 chunk 不止一个,就算字节全在读缓冲里也读不完。
  • 这一步遇到 Pending(socket 上暂时没数据),读端还在 Body,同样关。
📝NOTE
冷知识:「一步」是给 chunked 的结尾留的

#2149(「fix(http1): try to drain connection buffer if user drops Body」,首次发布于 v0.13.4)之前,接收端一没,Dispatcher 直接 close_read 然后返回。这个提交把它换成 poll_drain_or_close_read,并在 tests/client.rs 里加了测试 client_keep_alive_eager_when_chunked,测试的注释说明了场景:「If a response body has been read to completion, with completion determined by some other factor, like decompression, and thus it is in't polled a final time to clear the final 0-len chunk, try to eagerly clear it so the connection can still be used.」也就是客户端读完了内容(比如解压器已经知道数据结束了),但没有再 poll 一次去吃掉 chunked 结尾的 0\r\n\r\n。这几个字节只需要解码一步,所以「一步」够用;后来 #3137(「prevent sending 100-continue if user drops request body」,v1.0.0-rc.3)加上开头处理 Expect 的那段,注释写的也是「in case a tiny body was included」。它从来不是「把 body 读完」的机制。

实验:小、中、大三种 body

服务端每条连接计数从 socket 读了多少字节。处理函数有两种:/ignore 直接回 ok,Incoming 在 future 结束时随 req 一起 drop;/drop-then-wait 先 drop(req),睡 50ms 再回。客户端用裸 TCP 发 Content-Length 分别为 10 B、12 KiB、1 MiB 的 POST,读完响应后在同一条连接上再发一个 GET /next:

hyper = { version = "=1.12.0", features = ["server", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio"] }
http-body-util = "=0.1.5"
tokio = { version = "=1.54.1", features = ["full"] }
use std::{
  convert::Infallible,
  io,
  pin::Pin,
  sync::{
    Arc,
    atomic::{AtomicUsize, Ordering::SeqCst},
  },
  task::{Context, Poll},
  time::Duration,
};
use http_body_util::Full;
use hyper::{
  Request, Response,
  body::{Bytes, Incoming},
  server::conn::http1,
  service::service_fn,
};
use hyper_util::rt::TokioIo;
use tokio::{
  io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf},
  net::{TcpListener, TcpStream},
  time::timeout,
};

// Counts the bytes the server pulls out of the socket.
struct Counting {
  inner: TcpStream,
  read: Arc<AtomicUsize>,
}
impl AsyncRead for Counting {
  fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<io::Result<()>> {
    let before = buf.filled().len();
    let r = Pin::new(&mut self.inner).poll_read(cx, buf);
    self.read.fetch_add(buf.filled().len() - before, SeqCst);
    r
  }
}
impl AsyncWrite for Counting {
  fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, b: &[u8]) -> Poll<io::Result<usize>> {
    Pin::new(&mut self.inner).poll_write(cx, b)
  }
  fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
    Pin::new(&mut self.inner).poll_flush(cx)
  }
  fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
    Pin::new(&mut self.inner).poll_shutdown(cx)
  }
}

async fn handler(req: Request<Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
  if req.uri().path() == "/drop-then-wait" {
    drop(req); // body receiver dropped before the response exists
    tokio::time::sleep(Duration::from_millis(50)).await;
  }
  // "/ignore": the body is dropped together with `req` when this future completes
  Ok(Response::new(Full::new(Bytes::from_static(b"ok"))))
}

async fn read_response(rd: &mut tokio::net::tcp::OwnedReadHalf) -> Result<String, String> {
  let mut got = Vec::new();
  let mut buf = [0u8; 4096];
  loop {
    if let Some(i) = got.windows(4).position(|w| w == b"\r\n\r\n") {
      if got.len() >= i + 4 + 2 {
        let head = String::from_utf8_lossy(&got[..i]).to_lowercase();
        let close = if head.contains("connection: close") { ", connection: close" } else { "" };
        return Ok(format!("{}{close}", head.lines().next().unwrap().to_uppercase()));
      }
    }
    match timeout(Duration::from_millis(500), rd.read(&mut buf)).await {
      Ok(Ok(0)) => return Err("EOF".into()),
      Ok(Ok(n)) => got.extend_from_slice(&buf[..n]),
      Ok(Err(e)) => return Err(format!("{:?}", e.kind())),
      Err(_) => return Err("no response in 500ms".into()),
    }
  }
}

#[tokio::main]
async fn main() {
  let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
  let addr = listener.local_addr().unwrap();
  tokio::spawn(async move {
    loop {
      let (stream, _) = listener.accept().await.unwrap();
      let read = Arc::new(AtomicUsize::new(0));
      let io = Counting { inner: stream, read: read.clone() };
      tokio::spawn(async move {
        let res = http1::Builder::new().serve_connection(TokioIo::new(io), service_fn(handler)).await;
        println!("  server: connection ended {res:?}, read {} bytes in total", read.load(SeqCst));
      });
    }
  });

  for path in ["/ignore", "/drop-then-wait"] {
    for size in [10usize, 12 * 1024, 1024 * 1024] {
      println!("== POST {path}, body {size} bytes");
      let tcp = TcpStream::connect(addr).await.unwrap();
      let (mut rd, mut wr) = tcp.into_split();
      let mut req = format!("POST {path} HTTP/1.1\r\nHost: x\r\nContent-Length: {size}\r\n\r\n").into_bytes();
      req.extend(std::iter::repeat_n(b'a', size));
      let total = req.len();
      let writer = tokio::spawn(async move {
        let r = wr.write_all(&req).await;
        (wr, r)
      });
      match read_response(&mut rd).await {
        Ok(status) => println!("  client: {status}"),
        Err(e) => println!("  client: response failed: {e}"),
      }
      let (mut wr, wrote) = match timeout(Duration::from_secs(2), writer).await {
        Ok(j) => j.unwrap(),
        Err(_) => panic!("writer stuck"),
      };
      match wrote {
        Ok(()) => println!("  client: wrote all {total} bytes of the request"),
        Err(e) => println!("  client: writing the request failed: {:?}", e.kind()),
      }
      let _ = wr.write_all(b"GET /next HTTP/1.1\r\nHost: x\r\n\r\n").await;
      match read_response(&mut rd).await {
        Ok(status) => println!("  client: second request on same connection -> {status}"),
        Err(e) => println!("  client: second request on same connection -> {e}"),
      }
      drop((rd, wr));
      tokio::time::sleep(Duration::from_millis(100)).await;
    }
  }
}

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

== POST /ignore, body 10 bytes
  client: HTTP/1.1 200 OK
  client: wrote all 64 bytes of the request
  client: second request on same connection -> HTTP/1.1 200 OK
  server: connection ended Ok(()), read 95 bytes in total
== POST /ignore, body 12288 bytes
  client: HTTP/1.1 200 OK
  client: wrote all 12345 bytes of the request
  client: second request on same connection -> HTTP/1.1 200 OK
  server: connection ended Ok(()), read 12376 bytes in total
== POST /ignore, body 1048576 bytes
  server: connection ended Ok(()), read 24576 bytes in total
  client: HTTP/1.1 200 OK
  client: writing the request failed: BrokenPipe
  client: second request on same connection -> ConnectionReset
== POST /drop-then-wait, body 10 bytes
  client: HTTP/1.1 200 OK
  client: wrote all 72 bytes of the request
  client: second request on same connection -> HTTP/1.1 200 OK
  server: connection ended Ok(()), read 103 bytes in total
== POST /drop-then-wait, body 12288 bytes
  client: HTTP/1.1 200 OK
  client: wrote all 12353 bytes of the request
  client: second request on same connection -> HTTP/1.1 200 OK
  server: connection ended Ok(()), read 12384 bytes in total
== POST /drop-then-wait, body 1048576 bytes
  server: connection ended Ok(()), read 24576 bytes in total
  client: HTTP/1.1 200 OK, connection: close
  client: wrote all 1048643 bytes of the request
  client: second request on same connection -> ConnectionReset

逐个看。第二个请求 GET /next HTTP/1.1\r\nHost: x\r\n\r\n 是 31 字节,服务端读到的总数减掉它就是第一个请求读了多少。

  • 10 B:64 + 31 = 95,全读了。整个请求在第一次读(8192 字节的空间)里就到齐了,请求头一解析完,body 的唯一一块就进了槽,读端随即到 KeepAlive,发送端 drop。处理函数 drop 的只是一个装着数据的槽,根本走不到 poll_drain_or_close_read。连接复用。
  • 12 KiB:12345 + 31 = 12376,也全读了。第一次读 8192 字节(头 57 字节 + body 8135 字节,这一块进槽);处理函数不要 body,poll_drain_or_close_read 的那一步从 socket 读一次,自适应读策略这时给的空间是 16384,剩下的 4153 字节一次读完,读端到 KeepAlive。连接复用。
  • 1 MiB:服务端一共只读了 24576 = 8192 + 16384 字节:第一次读进槽,drain 那一次读不完,close_read。响应照常写出,然后连接关闭。服务端关 socket 时接收缓冲里还有客户端发来、没读的数据,内核回的是 RST 而不是 FIN,所以客户端写剩下的请求得到 BrokenPipe,第二个请求读到 ConnectionReset。/drop-then-wait 那次客户端「wrote all」,是因为服务端晚 50ms 才关,写调用在这之前就已经把 1 MiB 全部交给了内核、返回成功;第二个请求同样被 reset。
  • connection: close 头:只有 /drop-then-wait 的 1 MiB 带。/ignore 的 Incoming 是在处理函数的 future 完成时 drop 的,而 Dispatcher 在同一次 poll_write 里拿到响应就编码响应头,那时 KA 还是 Busy;要到 poll_loop 下一圈的 poll_read 才发现接收端没了。/drop-then-wait 先 drop、再睡 50ms,poll_read 在响应头之前就做了 close_read,KA 已经是 Disabled,编码时加上了 connection: close。Expect: 100-continue 请求被拒时也是前一种情况,见 Conn 的读写状态机。

所以服务端「不读 body」的后果取决于剩下多少:剩下的一步能解码完,连接保住;否则连接在这个响应之后结束,客户端要重新建连。客户端主线用的是同一个 poll_read:响应 body 没读完就 drop,连接同样走这一步,读不完就关,回不到连接池(见 连接池)。

📝NOTE
没有蠢问题

问: 处理函数确实不需要 body(比如鉴权失败要回 401),但又想让连接复用,该怎么办?

答: hyper 不会替你读,只能自己读完再回:req.into_body().collect().await,或者循环 frame() 把数据丢掉。代价是为一个要拒绝的请求接收全部字节,而 body 的大小由对方决定,所以通常要配一个上限,比如 Limited(见 http-body-util),超过上限就放弃读、接受关连接。反过来,拒绝一个很大的上传时直接 drop 往往是对的:关连接比替对方收完 1 GiB 便宜。

HTTP/2 的 Incoming

HTTP/2 不需要这个通道:每个流由 h2 交给 hyper 一个 RecvStream,数据已经按流分开了。Kind::H2 的 poll_frame 直接 poll_data,每交出一块就 release_capacity(bytes.len()),把这块的大小还给 h2 的流控窗口,对方才能接着发。背压在这里靠的是 HTTP/2 自己的流量控制:处理函数不读,窗口不释放,对方被窗口挡住,不需要一个「槽」。同一处还调 ping.record_data,给 BDP 估算采样(见 ping:保活与 BDP)。

另一个区别是 RST_STREAM(NO_ERROR):源码注释引 RFC 7540 第 8.1 节,说它「indicates an early response」,即对方已经给出了完整的响应、叫停剩下的传输。poll_frame 把它当作 body 正常结束(Ready(None)),不当错误。HTTP/2 服务端怎么建流、调服务,见 HTTP/2 服务端。

❗IMPORTANT
动脑筋:慢慢读一个 256 KiB 的 body

客户端一次写完一个 Content-Length: 262144 的 POST。处理函数先睡 100ms,然后一帧一帧读,每帧之后睡 20ms。服务端用上面实验里的 Counting 记录从 socket 读了多少字节(Counting 的定义同上,这里省略):

hyper = { version = "=1.12.0", features = ["server", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio"] }
http-body-util = "=0.1.5"
tokio = { version = "=1.54.1", features = ["full"] }
const SIZE: usize = 256 * 1024;

#[tokio::main]
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 read = Arc::new(AtomicUsize::new(0));
    let io = Counting { inner: stream, read: read.clone() };
    let svc = service_fn(move |req: Request<Incoming>| {
      let read = read.clone();
      async move {
        let mut body = req.into_body();
        // the client has long finished writing when we start
        tokio::time::sleep(Duration::from_millis(100)).await;
        println!("before first frame: socket read {:>6}, size_hint {:?}", read.load(SeqCst), body.size_hint().exact());
        let mut consumed = 0;
        loop {
          let before = read.load(SeqCst);
          let Some(frame) = body.frame().await else { break };
          let data = frame.unwrap().into_data().unwrap();
          consumed += data.len();
          println!(
            "frame {:>6} B: socket read before {before:>6}, after {:>6}; consumed {consumed:>6}, size_hint {:?}",
            data.len(),
            read.load(SeqCst),
            body.size_hint().exact()
          );
          tokio::time::sleep(Duration::from_millis(20)).await;
        }
        Ok::<_, Infallible>(Response::new(Full::new(Bytes::from_static(b"ok"))))
      }
    });
    http1::Builder::new().serve_connection(TokioIo::new(io), svc).await.unwrap();
  });

  let mut tcp = TcpStream::connect(addr).await.unwrap();
  let mut req = format!("POST / HTTP/1.1\r\nHost: x\r\nContent-Length: {SIZE}\r\n\r\n").into_bytes();
  req.extend(std::iter::repeat_n(b'a', SIZE));
  tcp.write_all(&req).await.unwrap();
  println!("client: wrote {} bytes", req.len());
  let mut buf = [0u8; 1024];
  let n = tcp.read(&mut buf).await.unwrap();
  println!("client: got {:?}", String::from_utf8_lossy(&buf[..n]).lines().next().unwrap());
}

(use 部分在上面实验的基础上加 http_body_util::BodyExt 和 hyper::body::Body,去掉 tokio::time::timeout。)处理函数睡完 100ms、开始读的时候,服务端从 socket 读了多少字节?每一帧前后,「socket read」的两个数会不会不同?答案在本页末尾。

动脑筋答案

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

client: wrote 262196 bytes
before first frame: socket read   8192, size_hint Some(262144)
frame   8140 B: socket read before   8192, after   8192; consumed   8140, size_hint Some(254004)
frame  16384 B: socket read before  24576, after  24576; consumed  24524, size_hint Some(237620)
frame  32768 B: socket read before  57344, after  57344; consumed  57292, size_hint Some(204852)
frame  65536 B: socket read before 122880, after 122880; consumed 122828, size_hint Some(139316)
frame 131072 B: socket read before 253952, after 253952; consumed 253900, size_hint Some(8244)
frame   8244 B: socket read before 262196, after 262196; consumed 262144, size_hint Some(0)
client: got "HTTP/1.1 200 OK"
  • 开始读之前只读了 8192 字节。客户端早就把 262196 字节都写进了内核,但 hyper 只读了第一次:头 52 字节 + body 8140 字节,这 8140 字节在处理函数还没 poll body 时就已经进了槽(普通 body 的 want 一开始就是真)。槽满了,Dispatcher 不再读,100ms 里一个字节也没多读。
  • 「before」总是等于已消费的量加 52。取第 k 帧之前,socket 读到的正好是前 k 帧的总和加请求头:服务端只比处理函数多读一块,就是槽里那一块。
  • 「before」和「after」相同。取走一块时 poll_next 唤醒了发送端,但发送端(Dispatcher)和处理函数在同一个任务里:处理函数打印时还在这次 poll 里,Dispatcher 还没机会读。处理函数 sleep 返回 Pending 后,poll_loop 走到下一圈的 poll_read,这时才读下一块放进槽。
  • 块的大小翻倍:8192(含头)、16384、32768、65536、131072,每块正好是一次 socket 读,大小由自适应读策略决定;最后一块是剩下的 8244 字节。
  • size_hint 是剩余量:从 262144 开始,每帧减掉这一帧的长度,最后是 0。

小结

  • Incoming 是 hyper 唯一的 Body 实现,只能从 hyper 收到,不能自己构造。Kind 有 Empty、Chan(HTTP/1)、H2、Ffi 四种;size_hint 给的是剩余长度。
  • DecodedLength 是一个 u64,u64::MAX 表示读到关闭为止,u64::MAX - 1 表示 chunked 或长度未知,确切长度最大 u64::MAX - 2。
  • HTTP/1 的通道(body/chan.rs,1.12.0 起)只有一个数据槽 item。Dispatcher 先 poll_ready 再读 socket,槽满了就不读,所以服务端读到的 body 最多比处理函数多一块。
  • want 只对 Expect: 100-continue 起作用:处理函数第一次 poll body 前不读 body、不发 100 Continue。普通 body 的 want 从一开始就是真。
  • 解码错误、连接错误、Dispatcher 被 drop 都会往通道里送一个错误;错误排在已送达的数据后面。
  • 处理函数 drop 掉没读完的 body,poll_drain_or_close_read 只再解码一步:读完了连接复用,读不完就 close_read,响应后关连接。如果发现 drop 时响应头已经编码,响应里没有 connection: close。
  • HTTP/2 的 Incoming 不用这个通道,背压靠 release_capacity 和 h2 的流控窗口。

💬 关联讨论

发起讨论
加载中...