Conn 的读写状态机
主线位置:两条主线共用。服务端主线上,「请求处理完、回到
Init,进入下一个 keep-alive 请求」这一步就发生在这里;客户端主线上,连接能不能回到连接池,看的也是这几个状态。
stateDiagram-v2 state "Init 等下一个头" as RInit state "Continue 未发 100" as RCont state "Body 读 body" as RBody state "KeepAlive 读完" as RKA state "Closed" as RClosed [*] --> RInit RInit --> RKA: 没有 body RInit --> RCont: 有 body 且 Expect RInit --> RBody: 有 body RCont --> RBody: 开始读 body RBody --> RKA: body 读完 RBody --> RClosed: 解码出错 RKA --> RInit: 两端完成且 Busy RKA --> RClosed: 写端关或非 Busy RClosed --> [*]
上图是读端(Reading)。写端是一张同样形状的图:Init → Body(还有 body 要写)→ KeepAlive(这条消息写完、连接还能用)或 Closed(写完就关)。两张图只在一处汇合:try_keep_alive。
Dispatcher 决定这一刻推哪一边,Conn 决定哪一边能推:能不能读头、能不能读 body、能不能写头、写完以后连接还能不能用。
Conn 和 State
Conn 只有两个字段:
pub(crate) struct Conn<I, B, T> {
io: Buffered<I, EncodedBuf<B>>,
state: State,
_marker: PhantomData<fn(T)>,
}
io 是带读写缓冲的 IO,见 读写缓冲。T 是 role::Server 或 role::Client,只在类型上区分,所以是 PhantomData。其余全在 State 里。和状态机有关的字段:
| 字段 | 作用 |
|---|---|
reading | 读端走到哪一步 |
writing | 写端走到哪一步 |
keep_alive | 这条连接在这条消息之后还能不能复用 |
version | 对端说的版本,初始假定 HTTP/1.1,读到头之后更新 |
notify_read | 回到 Init 时读缓冲里还有字节,告诉 Dispatcher 再读一次 |
allow_half_close | 对端关了写方向时是否继续处理 |
upgrade | 等待中的协议升级,见 协议升级 |
error | 没法当场返回的错误,Dispatcher 结束时取走 |
Conn::new 的初值是 reading: Reading::Init、writing: Writing::Init、keep_alive: KA::Busy、version: Version::HTTP_11。
三个枚举
#[derive(Debug)]
enum Reading {
Init,
Continue(Decoder),
Body(Decoder),
KeepAlive,
Closed,
}
enum Writing {
Init,
Body(Encoder),
KeepAlive,
Closed,
}
#[derive(Clone, Copy, Debug, Default)]
enum KA {
Idle,
#[default]
Busy,
Disabled,
}
| 状态 | 含义 |
|---|---|
Reading::Init | 等下一条消息的头。服务端只有在这个状态才读头 |
Reading::Continue(d) | 请求带 Expect: 100-continue,body 还没开始读,d 是它的解码器 |
Reading::Body(d) | 正在读 body,d 是 Length、Chunked 或 Eof 解码器,见 body 的解码与编码 |
Reading::KeepAlive | 这条消息读完了,等写端 |
Reading::Closed | 读端关了,不再读 |
Writing::Init | 还没写这条消息的头 |
Writing::Body(e) | 头写了,body 还没写完 |
Writing::KeepAlive | 这条消息写完了,等读端 |
Writing::Closed | 写端关了 |
KA::Busy | 有消息在处理,结束后可以复用 |
KA::Idle | 空闲,读写都在 Init |
KA::Disabled | 这条消息结束后关连接 |
Decoder 和 Encoder 装在状态里面,所以「还剩多少字节没读 / 没写」和「现在在读 / 在写 body」是同一件事,不会出现状态说在读 body、解码器却不存在的情况。
读端怎么走
poll_read_head 解析出一个头以后(解析本身见 解析与编码),用解析结果更新状态:
self.state.busy();
self.state.keep_alive &= msg.keep_alive;
self.state.version = msg.head.version;
let mut wants = if msg.wants_upgrade {
Wants::UPGRADE
} else {
Wants::EMPTY
};
if msg.decode == DecodedLength::ZERO {
if msg.expect_continue {
debug!("ignoring expect-continue since body is empty");
}
self.state.reading = Reading::KeepAlive;
if !T::should_read_first() {
self.try_keep_alive(cx);
}
} else if msg.expect_continue && msg.head.version.gt(&Version::HTTP_10) {
let h1_max_header_size = self.state.h1_max_header_size;
self.state.reading = Reading::Continue(Decoder::new(
msg.decode,
self.state.h1_max_headers,
h1_max_header_size,
));
wants = wants.add(Wants::EXPECT);
} else {
let h1_max_header_size = self.state.h1_max_header_size;
self.state.reading = Reading::Body(Decoder::new(
msg.decode,
self.state.h1_max_headers,
h1_max_header_size,
));
}
busy()把KA设成Busy,但State::busy遇到Disabled什么都不做:关连接的决定一旦做出,就不会被下一条消息撤销。keep_alive &= msg.keep_alive用的是KA上手写的BitAndAssign<bool>:右边是false就变成Disabled,是true什么都不变。对端说「这条之后关」,这里只能往Disabled方向走。- 没有 body,读端直接到
KeepAlive;有 body,按Expect进Continue或Body。Expect: 100-continue只在 HTTP/1.1 及以上才认,body 为空时忽略。
msg.keep_alive 是 role::Server::parse 算出来的:HTTP/1.1 默认 true,HTTP/1.0 默认 false,然后看 Connection 头:带 close 就是 false,HTTP/1.0 带 keep-alive 就是 true。
#4201(v1.12.0)之前,parse 处理每一行 Connection 头时,用 keep_alive 的当前值来判断版本:
// keep_alive was previously set to default for Version
if keep_alive {
// HTTP/1.1
keep_alive = !headers::connection_close(&value);
} else {
// HTTP/1.0
keep_alive = headers::connection_keep_alive(&value);
}
提交说明解释了问题:一行 close 把 keep_alive 设成 false 之后,下一行就走进了 HTTP/1.0 的分支,被 keep-alive 改回 true,「Connection: close followed by Connection: keep-alive therefore parsed as keep-alive, while the reverse order parsed correctly」。修法是加一个 saw_connection_close,只要有一行带 close,之后的行都不能再把它改回来,服务端和客户端的 parse 都改了。下面实验的最后一个场景就是这种请求,1.12.0 回的是 connection: close 并关闭了连接。
body 读完在 poll_read_body 里:解码器报告 is_eof(),或者读到 trailers,读端到 KeepAlive;解码出错到 Closed。无论哪种,最后都调一次 try_keep_alive。
写端怎么走
pub(crate) fn write_head(&mut self, head: MessageHead<T::Outgoing>, body: Option<BodyLength>) {
if let Some(encoder) = self.encode_head(head, body) {
self.state.writing = if !encoder.is_eof() {
Writing::Body(encoder)
} else if encoder.is_last() {
Writing::Closed
} else {
Writing::KeepAlive
};
}
}
编码器还有 body 要写就进 Body;没有 body 的话,看编码器的 is_last:这是最后一条消息,写端直接 Closed,否则 KeepAlive。write_body、end_body 在 body 写完时做同样的判断。
is_last 是 role::Server::encode 决定的(见 解析与编码),和状态机有关的只有三条:
encode里is_last的默认值是!msg.keep_alive,而msg.keep_alive就是State::wants_keep_alive(),即KA不是Disabled。- 响应头里有
Connection: close(服务自己加的),编码时把is_last设成true。 101 Switching Protocols和CONNECT的 2xx 响应总是is_last,见 协议升级。
编码之前,enforce_version 按对端的版本修正响应头:
fn enforce_version(&mut self, head: &mut MessageHead<T::Outgoing>) {
match self.state.version {
Version::HTTP_10 => {
// Fixes response or connection when keep-alive header is not present
self.fix_keep_alive(head);
// If the remote only knows HTTP/1.0, we should force ourselves
// to do only speak HTTP/1.0 as well.
head.version = Version::HTTP_10;
}
Version::HTTP_11 => {
if let KA::Disabled = self.state.keep_alive.status() {
match head.headers.entry(CONNECTION) {
Entry::Occupied(mut entry) => {
if !entry.iter().any(headers::connection_close) {
entry.append(HeaderValue::from_static("close"));
}
}
Entry::Vacant(entry) => {
entry.insert(HeaderValue::from_static("close"));
}
}
}
}
_ => (),
}
// ...
}
对端是 HTTP/1.0:响应版本降成 1.0;fix_keep_alive 在响应没带 keep-alive 时,要么关掉 keep-alive(响应本身是 1.0),要么在还想复用时补一个 connection: keep-alive。HTTP/1.0 没有默认的持久连接,不写出来对端就会认为响应后连接关闭。对端是 HTTP/1.1 且 KA 已经 Disabled:补一个 connection: close,告诉对端这是最后一个响应。
connection: close 是 1.5.0 才自动补上的HTTP_11 那个分支是 #3725(v1.5.0)加的。它修的是 #3720:「When HTTP1 connection draining is activated, the Connection: close header is not attached to responses sent to active connections」。在那之前,graceful_shutdown 会让连接在当前响应后关闭,但响应里不说,客户端无从知道。提交说明说,这个改动也覆盖「a request error that ruins the connection」等 hyper 已知连接会结束的情况。
最初的写法是 head.headers.insert(CONNECTION, "close")。#4110(v1.11.1)让客户端请求带 Connection: close 时也把 KA 设成 Disabled,于是客户端写请求时也会走到这个 insert。#4195 随即报告:insert 会删掉 Connection 头原有的值,客户端发的 connection: close, x-hop 到了服务端只剩 connection: close,自定义的逐跳头声明丢了。#4196(v1.12.0)改成上面的样子:已有 Connection 头就 append,并且已经有 close 时不重复加。
try_keep_alive:两端都到 KeepAlive 才回 Init
fn try_keep_alive<T: Http1Transaction>(&mut self) {
match (&self.reading, &self.writing) {
(&Reading::KeepAlive, &Writing::KeepAlive) => {
if let KA::Busy = self.keep_alive.status() {
self.idle::<T>();
} else {
trace!(
"try_keep_alive({}): could keep-alive, but status = {:?}",
T::LOG,
self.keep_alive
);
self.close();
}
}
(&Reading::Closed, &Writing::KeepAlive) | (&Reading::KeepAlive, &Writing::Closed) => {
self.close();
}
_ => (),
}
}
三种结局:
- 两端都
KeepAlive,KA是Busy:idle把KA设成Idle,读写都拨回Init,清掉method。 - 两端都
KeepAlive,但KA是Disabled:close,读写都Closed。 - 一端
KeepAlive、另一端已经Closed:也close。一端关了,另一端停在KeepAlive没有意义。
Conn 的 try_keep_alive 在它之后再调 maybe_notify:读写刚回到 Init,而上一次读 socket 没有遇到 Pending(读缓冲里可能还有下一条消息),就置 notify_read。读缓冲为空时它会先试读一次 socket,读到 EOF 并且连接空闲,就直接 close。Dispatcher 每圈末尾通过 wants_read_again 取走 notify_read,决定要不要再转一圈,见 Dispatcher。
try_keep_alive 的调用点决定了「什么时候算写完」:服务端在 poll_flush 刷新成功之后,以及 poll_read_body 读完 body 之后。也就是说,响应的最后一个字节离开写缓冲区以后,读端才回到 Init,下一个请求才会被读。这就是 Dispatcher 里「请求严格串行」实验的原因。
实验:几种请求头各怎么收场
每种请求单独开一条连接,用原始字节发过去,打印服务端回的字节,再看 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"] }
use std::{convert::Infallible, time::Duration};
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}, time::timeout};
async fn probe(addr: std::net::SocketAddr, name: &str, req: &str) {
let mut s = TcpStream::connect(addr).await.unwrap();
s.write_all(req.as_bytes()).await.unwrap();
let mut out = Vec::new();
let mut buf = [0u8; 1024];
let end = loop {
match timeout(Duration::from_millis(300), s.read(&mut buf)).await {
Ok(Ok(0)) => break "server closed",
Ok(Ok(n)) => out.extend_from_slice(&buf[..n]),
Ok(Err(e)) => panic!("{e}"),
Err(_) => break "still open after 300ms",
}
};
println!("== {name}\n{}\n-- {end}", String::from_utf8_lossy(&out).replace("\r\n", "\\r\\n\n"));
}
#[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 svc = service_fn(|req: Request<Incoming>| async move {
let mut rsp = Response::new(Full::new(Bytes::from("ok")));
if req.uri().path() == "/bye" {
rsp.headers_mut().insert("connection", "close".parse().unwrap());
}
Ok::<_, Infallible>(rsp)
});
tokio::spawn(async move {
let r = http1::Builder::new()
.auto_date_header(false) // keep the output stable
.serve_connection(TokioIo::new(stream), svc)
.await;
if let Err(e) = r {
println!("server conn error: {e}");
}
});
}
});
probe(addr, "1.1, no Connection header", "GET / HTTP/1.1\r\nHost: x\r\n\r\n").await;
probe(addr, "1.1, Connection: close", "GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n").await;
probe(addr, "1.1, handler sets Connection: close", "GET /bye HTTP/1.1\r\nHost: x\r\n\r\n").await;
probe(addr, "1.0, no Connection header", "GET / HTTP/1.0\r\n\r\n").await;
probe(addr, "1.0, Connection: keep-alive", "GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n").await;
probe(
addr,
"1.1, close then keep-alive",
"GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\nConnection: keep-alive\r\n\r\n",
)
.await;
}
M2 Max、rustc 1.99.0、--release,连跑 4 次输出相同:
== 1.1, no Connection header
HTTP/1.1 200 OK\r\n
content-length: 2\r\n
\r\n
ok
-- still open after 300ms
== 1.1, Connection: close
HTTP/1.1 200 OK\r\n
connection: close\r\n
content-length: 2\r\n
\r\n
ok
-- server closed
== 1.1, handler sets Connection: close
HTTP/1.1 200 OK\r\n
connection: close\r\n
content-length: 2\r\n
\r\n
ok
-- server closed
== 1.0, no Connection header
HTTP/1.0 200 OK\r\n
content-length: 2\r\n
\r\n
ok
-- server closed
== 1.0, Connection: keep-alive
HTTP/1.0 200 OK\r\n
connection: keep-alive\r\n
content-length: 2\r\n
\r\n
ok
-- still open after 300ms
== 1.1, close then keep-alive
HTTP/1.1 200 OK\r\n
connection: close\r\n
content-length: 2\r\n
\r\n
ok
-- server closed
逐个对回源码:
| 场景 | 状态怎么走 | 线上看到的 |
|---|---|---|
1.1,无 Connection | KA 保持 Busy,读写都到 KeepAlive,idle 回 Init | 没有 Connection 头,连接保持 |
1.1,Connection: close | &= false 让 KA 变 Disabled;enforce_version 补 close;is_last 为真,写端直接 Closed | connection: close,随后关闭 |
服务自己加 Connection: close | KA 还是 Busy,但编码时看到 close,is_last 为真,写端 Closed,try_keep_alive 把读端也关了 | 服务加的那个头,随后关闭 |
1.0,无 Connection | parse 给 keep_alive = false,KA 变 Disabled;fix_keep_alive 不补头 | HTTP/1.0,没有 Connection 头,随后关闭 |
1.0,Connection: keep-alive | KA 保持 Busy;fix_keep_alive 补 keep-alive,版本降成 1.0 | HTTP/1.0 加 connection: keep-alive,连接保持 |
1.1,close 后跟 keep-alive | #4201 之后 close 优先,同第二行 | connection: close,随后关闭 |
KA 什么时候变 Disabled
Disabled 是单向的:busy() 不覆盖它,只有新建 Conn 才会回到 Busy。会把它设成 Disabled 的地方:
| 来源 | 位置 |
|---|---|
请求或响应解析出 keep_alive = false | poll_read_head 里的 &= |
| 任何一端被关闭 | State::close、close_read、close_write 都会顺手 disable |
Builder::keep_alive(false)、graceful_shutdown | Conn::disable_keep_alive |
客户端请求带 Connection: close | encode_head(#4110),见 conn::http1 客户端 |
Conn::disable_keep_alive 分两种情况:连接正空闲(KA::Idle),直接 close;正在处理消息,只设 Disabled,等这条消息走完由 try_keep_alive 关。graceful_shutdown「处理完当前请求再关、空闲的立刻关」就是这两个分支,外层的逻辑见 服务端 Builder 与 Connection。
Expect: 100-continue
客户端发了 Expect: 100-continue,意思是「先告诉我你愿意收,我再发 body」。读端进 Reading::Continue,100 Continue 在 poll_read_body 里写:
Reading::Continue(decoder) => {
// Write the 100 Continue if not already responded...
if let Writing::Init = self.state.writing {
trace!("automatically sending 100 Continue");
let cont = b"HTTP/1.1 100 Continue\r\n\r\n";
self.io.headers_buf().extend_from_slice(cont);
}
// And now recurse once in the Reading::Body state...
self.state.reading = Reading::Body(decoder.clone());
return self.poll_read_body(cx);
}
poll_read_body 只有在 body 通道要数据时才会被 Dispatcher 调用,而 Expect 请求的 body 通道一开始是「不要」的(见 Dispatcher 的 poll_read)。所以 100 Continue 发出的时机是服务第一次 poll 请求 body 的时候。如果此时响应头已经写了(Writing 不是 Init),就不再发 100 Continue。
服务根本不读 body、直接回响应时:body 的接收端被 drop,Dispatcher 调 poll_drain_or_close_read,它把 Continue 换成 Body(跳过 100 Continue),试着读一下;客户端还在等 100,什么也没发,读端仍在 Body,于是 close_read。读端 Closed、写端 KeepAlive,try_keep_alive 关掉整条连接。
两种情况都跑一遍:
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::{BodyExt, 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}, time::timeout};
#[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 {
for i in 1.. {
let (stream, _) = listener.accept().await.unwrap();
let svc = service_fn(move |req: Request<Incoming>| async move {
if req.uri().path() == "/read" {
println!("{:>4}ms server: handler called, reads body in 200ms", ms());
tokio::time::sleep(Duration::from_millis(200)).await;
let body = req.into_body().collect().await.unwrap().to_bytes();
println!("{:>4}ms server: body = {body:?}", ms());
} else {
println!("{:>4}ms server: handler called, rejects without reading body", ms());
}
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from("done"))))
});
tokio::spawn(async move {
let r = http1::Builder::new()
.auto_date_header(false)
.serve_connection(TokioIo::new(stream), svc)
.await;
println!("{:>4}ms server: connection {i} ended {r:?}", ms());
});
}
});
for path in ["/read", "/reject"] {
println!("== {path}");
let mut s = TcpStream::connect(addr).await.unwrap();
let head = format!("POST {path} HTTP/1.1\r\nHost: x\r\nContent-Length: 5\r\nExpect: 100-continue\r\n\r\n");
s.write_all(head.as_bytes()).await.unwrap();
let mut buf = [0u8; 1024];
let mut sent_body = false;
loop {
match timeout(Duration::from_millis(500), s.read(&mut buf)).await {
Ok(Ok(0)) => {
println!("{:>4}ms client: EOF", ms());
break;
}
Ok(Ok(n)) => {
let text = String::from_utf8_lossy(&buf[..n]).replace("\r\n", "\\r\\n");
println!("{:>4}ms client: got {text}", ms());
if text.starts_with("HTTP/1.1 100") && !sent_body {
s.write_all(b"hello").await.unwrap();
sent_body = true;
}
}
Ok(Err(e)) => panic!("{e}"),
Err(_) => {
println!("{:>4}ms client: nothing more for 500ms", ms());
break;
}
}
}
}
}
连跑 4 次,顺序相同,毫秒数差 1ms 以内:
== /read
0ms server: handler called, reads body in 200ms
203ms client: got HTTP/1.1 100 Continue\r\n\r\n
203ms server: body = b"hello"
204ms client: got HTTP/1.1 200 OK\r\ncontent-length: 4\r\n\r\ndone
705ms client: nothing more for 500ms
== /reject
706ms server: connection 1 ended Ok(())
706ms server: handler called, rejects without reading body
706ms server: connection 2 ended Ok(())
706ms client: got HTTP/1.1 200 OK\r\ncontent-length: 4\r\n\r\ndone
706ms client: EOF
/read:服务在 0ms 就被调用了,但100 Continue到 203ms 才发出,正是服务开始读 body 的时刻。之后连接保持(connection 1在客户端 drop 它时才结束,打印在/reject那一段的开头)。/reject:没有100 Continue;响应是正常的 200,不带connection: close,然后连接被关闭。响应头写出去的时候KA还是Busy,关连接的决定是在服务的 future 完成、body 接收端被 drop 之后才做的,那时头已经编码好了。客户端只能从 EOF 得知连接不能再用。
对端半关闭
TCP 允许只关一个方向。客户端发完请求后 shutdown(Write),意思是「我不再发了,但还在等你的响应」。hyper 在服务端默认不接受这种用法,Builder::half_close 的文档:
/// Set whether HTTP/1 connections should support half-closures.
///
/// Clients can chose to shutdown their write-side while waiting
/// for the server to respond. Setting this to `true` will
/// prevent closing the connection immediately if `read`
/// detects an EOF in the middle of a request.
///
/// Default is `false`.
「in the middle of a request」对应的代码是 poll_read_keep_alive。请求的头和 body 都读完、响应还没写完时,Dispatcher 的 poll_read 每次都会走到它:
pub(crate) fn poll_read_keep_alive(&mut self, cx: &mut Context<'_>) -> Poll<crate::Result<()>> {
debug_assert!(!self.can_read_head() && !self.can_read_body());
if self.is_read_closed() {
Poll::Pending
} else if self.is_mid_message() {
self.mid_message_detect_eof(cx)
} else {
self.require_empty_read(cx)
}
}
fn mid_message_detect_eof(&mut self, cx: &mut Context<'_>) -> Poll<crate::Result<()>> {
debug_assert!(!self.can_read_head() && !self.can_read_body() && !self.is_read_closed());
debug_assert!(self.is_mid_message());
if self.state.allow_half_close || !self.io.read_buf().is_empty() {
return Poll::Pending;
}
let num_read = ready!(self.force_io_read(cx)).map_err(crate::Error::new_io)?;
if num_read == 0 {
trace!("found unexpected EOF on busy connection: {:?}", self.state);
self.state.close_read();
Poll::Ready(Err(crate::Error::new_incomplete()))
} else {
Poll::Ready(Ok(()))
}
}
处理一个请求期间,hyper 仍然去读 socket,目的是尽早发现对端断开,不必把响应算完再写进一个死连接。代价是它分不清「对端整个断开」和「对端只关了写方向」:读到 EOF,一律 close_read 并返回 IncompleteMessage,这个错误从 Connection future 返回,在途请求的响应不会再写。allow_half_close 为真时直接返回 Pending,不去读;读缓冲里还有字节(下一个 pipelined 请求)时也不读,因为那些字节总得先处理。
require_empty_read 是客户端连接空闲时用的分支(服务端空闲时读端在 Init,会去读头,不会走到这里)。服务端在空闲时读到 EOF 是正常关闭:on_read_head_error 看到读缓冲为空、不是解析到一半,就关掉写端返回 None,连接以 Ok 结束。
客户端一次写三个 pipelined 请求,紧接着 shutdown()(发 FIN)。服务立即返回响应。half_close(false)(默认)和 half_close(true) 各跑一次:
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;
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}};
async fn run(half_close: bool) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let mut client = TcpStream::connect(listener.local_addr().unwrap()).await.unwrap();
// three pipelined requests, then FIN: "I have nothing more to send"
client
.write_all(b"GET /1 HTTP/1.1\r\nHost: x\r\n\r\nGET /2 HTTP/1.1\r\nHost: x\r\n\r\nGET /3 HTTP/1.1\r\nHost: x\r\n\r\n")
.await
.unwrap();
client.shutdown().await.unwrap();
let (stream, _) = listener.accept().await.unwrap();
let svc = service_fn(|req: Request<Incoming>| async move {
// the response is ready immediately
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from(req.uri().path().to_string()))))
});
let res = http1::Builder::new()
.half_close(half_close)
.serve_connection(TokioIo::new(stream), svc)
.await;
let mut out = String::new();
client.read_to_string(&mut out).await.unwrap();
println!(
"half_close({half_close}): connection {res:?}, client got {} responses",
out.matches("HTTP/1.1 200").count()
);
}
#[tokio::main]
async fn main() {
run(false).await;
run(true).await;
}
动脑筋答案
M2 Max、rustc 1.99.0、--release,连跑 4 次输出相同:
half_close(false): connection Err(hyper::Error(IncompleteMessage)), client got 2 responses
half_close(true): connection Ok(()), client got 3 responses
默认配置下少了一个响应,少的是最后一个:
- 请求 1、2:读完头、调用服务以后,
poll_read接着走到mid_message_detect_eof。读缓冲里还有后面的请求,不去读 socket,返回Pending。响应正常写出。 - 请求 3:读完它的头,读缓冲空了。
Dispatcher的poll_read是一个循环,poll_read_head返回后马上再走到mid_message_detect_eof,这一次真去读 socket,读到客户端的 FIN,返回IncompleteMessage。这发生在同一圈的poll_write之前,所以即使服务的响应已经就绪,也没有机会写出去。错误从Connectionfuture 返回。 half_close(true):mid_message_detect_eof不读 socket,三个响应都写完。之后连接空闲,再读到 EOF 就是正常关闭,Ok(())。
小结
Conn的状态是三样:Reading(Init/Continue/Body/KeepAlive/Closed)、Writing(Init/Body/KeepAlive/Closed)和KA(Idle/Busy/Disabled)。解码器和编码器装在Body状态里。- 读写各自走到
KeepAlive后,try_keep_alive看KA:Busy就一起回Init(idle),否则两端都关。服务端只在刷新成功后才调它,所以下一个请求要等上一个响应完全写出。 KA变Disabled是单向的:解析到keep_alive = false、任一端关闭、keep_alive(false)、graceful_shutdown都会让它变;HTTP/1.1 下enforce_version据此补connection: close,HTTP/1.0 下按fix_keep_alive降版本、补keep-alive。多行Connection头里只要有close就算 close(1.12.0,#4201)。Expect: 100-continue让读端进Continue,100 Continue在服务第一次读 body 时才发;服务不读就回响应,连接在响应后关闭,响应里不带connection: close。- 处理请求期间 hyper 仍会读 socket 检测断开;默认
half_close(false)下,读缓冲为空时读到 EOF 就以IncompleteMessage结束连接,在途请求得不到响应。