Dispatcher
主线位置:两条主线共用。服务端主线上,
Connectionfuture 每次被 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 槽
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 的读写状态机 |
dispatch | Dispatch 的实现:服务端是 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:服务端和客户端只差在这里
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_msg | poll in_flight,响应好了拆成头和 body,清空 in_flight | 从请求通道 rx 取下一个请求和回调 |
poll_ready | in_flight 有东西就 Pending | 回调的接收方还在就 Ready(Ok),已经放弃就 Ready(Err) |
should_poll | in_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 的计数观察见本页的「动脑筋」。
wants_write_again 和后面那段二次检查不是一次写成的:
- #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时继续转圈。 - #3977(v1.8.1)把它回滚了,原因是 #3976 报告 1.8.0 下「concurrent downloading of large files exhausts all CPU resources」。
- #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那段。 - #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 服务端。
卡住下一个请求的是哪一道闸
服务端串行有两道闸:
Dispatch::poll_ready:in_flight有东西就Pending,poll_read_head不去读下一个头。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)才被调用。真正起作用的是第二道闸。
问: 既然 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 就以这个错误结束。
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,等外层交出去);服务端的错误从Connectionfuture 返回。