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 做什么 |
|---|---|---|
Empty | HTTP/1 算出的 body 长度是 0(poll_read_head 里 DecodedLength::ZERO => IncomingBody::empty()) | 直接 Ready(None) |
Chan | HTTP/1 有 body | 从通道取一块;通道结束后再取 trailers |
H2 | HTTP/2 的每个流 | poll_data 取数据,取完再 poll_trailers |
Ffi | C 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 一节。
Incoming 原来叫 Body,中间还叫过 Recvhyper 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 | 槽里已经有数据时到来的错误,排在数据后面,不把数据挤掉 |
trailers | trailers 单独放,不占槽 |
sender_open / receiver_open | 两端各自是否还在,Drop 时置 false |
want | 接收端是否已经要过数据;只对 Expect: 100-continue 有意义,见下 |
两把 AtomicWaker 各存一端的 waker,锁只护着 State,唤醒在放锁之后做。
发送端:poll_ready 与 try_send_data
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
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。
后厨(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 下一圈接着读。
问: 一个槽、每块都要一次唤醒,上传大文件岂不是要来回切换成千上万次?
答: 块不是固定大小的。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_catch | new_body("connection error") |
Dispatcher 被 drop(比如运行时关了) | SenderDropGuard 的 Drop | new_incomplete() |
send_error 在槽空时把错误放进槽,槽里已经有数据时放进 pending_error。poll_next 先交 item 再交 pending_error,接收端不会因为一个错误丢掉已经送达的数据。注释里说的「old mpsc channel」,是下面这条冷知识里的旧实现。
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,同样关。
#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,连接同样走这一步,读不完就关,回不到连接池(见 连接池)。
问: 处理函数确实不需要 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 服务端。
客户端一次写完一个 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 的流控窗口。