Rust in Everything
基于 hyper 1.12.0

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。

三个枚举

Reading、Writing、KA:

#[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。

📝NOTE
冷知识:先写 close 再写 keep-alive,曾经算 keep-alive

#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。

写端怎么走

write_head:

    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 决定的(见 解析与编码),和状态机有关的只有三条:

  1. encode 里 is_last 的默认值是 !msg.keep_alive,而 msg.keep_alive 就是 State::wants_keep_alive(),即 KA 不是 Disabled。
  2. 响应头里有 Connection: close(服务自己加的),编码时把 is_last 设成 true。
  3. 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,告诉对端这是最后一个响应。

📝NOTE
冷知识: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

State::try_keep_alive:

    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,无 ConnectionKA 保持 Busy,读写都到 KeepAlive,idle 回 Init没有 Connection 头,连接保持
1.1,Connection: close&= false 让 KA 变 Disabled;enforce_version 补 close;is_last 为真,写端直接 Closedconnection: close,随后关闭
服务自己加 Connection: closeKA 还是 Busy,但编码时看到 close,is_last 为真,写端 Closed,try_keep_alive 把读端也关了服务加的那个头,随后关闭
1.0,无 Connectionparse 给 keep_alive = false,KA 变 Disabled;fix_keep_alive 不补头HTTP/1.0,没有 Connection 头,随后关闭
1.0,Connection: keep-aliveKA 保持 Busy;fix_keep_alive 补 keep-alive,版本降成 1.0HTTP/1.0 加 connection: keep-alive,连接保持
1.1,close 后跟 keep-alive#4201 之后 close 优先,同第二行connection: close,随后关闭

KA 什么时候变 Disabled

Disabled 是单向的:busy() 不覆盖它,只有新建 Conn 才会回到 Busy。会把它设成 Disabled 的地方:

来源位置
请求或响应解析出 keep_alive = falsepoll_read_head 里的 &=
任何一端被关闭State::close、close_read、close_write 都会顺手 disable
Builder::keep_alive(false)、graceful_shutdownConn::disable_keep_alive
客户端请求带 Connection: closeencode_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)
        }
    }

mid_message_detect_eof:

    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 结束。

❗IMPORTANT
动脑筋:发完三个请求就关写方向,能收到几个响应?

客户端一次写三个 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;
}
两行里的「client got」分别是几?答案在本页末尾。

动脑筋答案

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 之前,所以即使服务的响应已经就绪,也没有机会写出去。错误从 Connection future 返回。
  • 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 结束连接,在途请求得不到响应。

💬 关联讨论

发起讨论
加载中...