Rust in Everything
基于 hyper 1.12.0

读写缓冲

主线位置:两条主线的最底层。服务端主线上,socket 读到的每个字节先进读缓冲,解析与编码和 body 的解码都从这里取;响应的头和 body 先放进写缓冲,再由 flush 写到 socket。客户端主线完全一样,只是读的是响应、写的是请求。

flowchart TB
  io["IO (rt::Read + rt::Write)"] -->|poll_read| rfi["poll_read_from_io"]
  rfi --> rbuf["read_buf: BytesMut"]
  rbuf --> parse["parse:解析消息头"]
  rbuf --> mem["read_mem:body 字节"]
  head["Conn 编码的头"] --> hdrs["headers: Vec u8"]
  bodyb["EncodedBuf"] -->|Flatten 拷贝| hdrs
  bodyb -->|Queue| queue["queue: BufList"]
  hdrs --> flush["poll_flush"]
  queue --> flush
  flush -->|"write 或 writev"| io

Conn 不直接碰 IO,它持有的是一个 Buffered:

pub(crate) struct Buffered<T, B> {
    flush_pipeline: bool,
    io: T,
    partial_len: Option<usize>,
    read_blocked: bool,
    read_buf: BytesMut,
    read_buf_strategy: ReadStrategy,
    write_buf: WriteBuf<B>,
}

T 是 IO 类型,B 是要写的 body 块的类型(在 Conn 里是 EncodedBuf<_>)。这一层回答两个问题:每次从 socket 读多少、写出去的字节先在哪攒着。

读:每次读多少

poll_read_from_io:

    pub(crate) fn poll_read_from_io(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<usize>> {
        self.read_blocked = false;
        // Get the next amount to allocate, but make sure we don't go over
        // the max read buf size configured.
        let next = cmp::min(
            self.read_buf_strategy.next(),
            self.read_buf_strategy
                .max()
                .saturating_sub(self.read_buf.len()),
        );
        if self.read_buf_remaining_mut() < next {
            self.read_buf.reserve(next);
        }

        // SAFETY: ReadBuf and poll_read promise not to set any uninitialized
        // bytes onto `dst`.
        let dst = unsafe { self.read_buf.chunk_mut().as_uninit_slice_mut() };
        let mut buf = ReadBuf::uninit(dst);
        match Pin::new(&mut self.io).poll_read(cx, buf.unfilled()) {
            Poll::Ready(Ok(())) => {
                let n = buf.filled().len();
                trace!("received {} bytes", n);
                // Safety: we just read that many bytes into the
                // uninitialized part of the buffer, so this is okay.
                // @tokio pls give me back `poll_read_buf` thanks
                unsafe {
                    self.read_buf.advance_mut(n);
                }
                self.read_buf_strategy.record(n);
                Poll::Ready(Ok(n))
            }
            Poll::Pending => {
                self.read_blocked = true;
                Poll::Pending
            }
            Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
        }
    }

三点:

  • 读缓冲一开始容量是 0(Buffered::new 里 BytesMut::with_capacity(0)),第一次读时才 reserve。只有剩余容量不够 next 时才扩,所以交给 IO 的空间(chunk_mut 的长度)至少是 next,可能更大。
  • 交给 IO 的是 BytesMut 未初始化的那部分,包成 ReadBuf::uninit,读多少 advance_mut 多少,不先清零。ReadBuf 为什么能安全地交出未初始化内存,见 IO trait。IO 底下是 tokio 的 TcpStream 时,一次 poll_read 怎么走到 read(2) 或挂起,见从 TcpStream::read 到挂起。
  • 每次读完都把读到的字节数交给 read_buf_strategy.record(n),决定下一次的 next。

ReadStrategy::Adaptive

ReadStrategy:

enum ReadStrategy {
    Adaptive {
        decrease_now: bool,
        next: usize,
        max: usize,
    },
    #[cfg(feature = "client")]
    Exact(usize),
}

默认是 Adaptive,next 从 INIT_BUFFER_SIZE(8192)开始,max 是 DEFAULT_MAX_BUFFER_SIZE:

/// The default maximum read buffer size. If the buffer gets this big and
/// a message is still not complete, a `TooLarge` error is triggered.
// Note: if this changes, update server::conn::Http::max_buf_size docs.
pub(crate) const DEFAULT_MAX_BUFFER_SIZE: usize = 8192 + 4096 * 100;

8192 + 409600 = 417792 字节,408 KiB,Builder::max_buf_size 的文档写作「~400kb」。它不是 2 的幂。

record:

    fn record(&mut self, bytes_read: usize) {
        match self {
            ReadStrategy::Adaptive {
                decrease_now,
                next,
                max,
                ..
            } => {
                if bytes_read >= *next {
                    *next = cmp::min(incr_power_of_two(*next), *max);
                    *decrease_now = false;
                } else {
                    let decr_to = prev_power_of_two(*next);
                    if bytes_read < decr_to {
                        if *decrease_now {
                            *next = cmp::max(decr_to, INIT_BUFFER_SIZE);
                            *decrease_now = false;
                        } else {
                            // Decreasing is a two "record" process.
                            *decrease_now = true;
                        }
                    } else {
                        // A read within the current range should cancel
                        // a potential decrease, since we just saw proof
                        // that we still need this size.
                        *decrease_now = false;
                    }
                }
            }
            #[cfg(feature = "client")]
            ReadStrategy::Exact(_) => (),
        }
    }

规则:

  • 读满了就翻倍:这次读到的不少于 next,说明 socket 里可能还有,下次多要一倍,但不超过 max。
  • 连续两次读得少才减半:读到的不足 prev_power_of_two(next)(当 next 是 2 的幂时就是它的一半),第一次只记一笔 decrease_now,第二次才真的减;中间只要有一次读到了「一半到 next 之间」,这笔记录就作废。最低不低于 8192。
  • 增长一次一步、减少要两次确认,是为了不在突发流量上来回抖:一次小读可能只是对端刚好暂停。

next 一路翻倍时都是 2 的幂,直到碰到 max。默认 max 是 417792,262144 再翻倍会被截成 417792。从 417792 往下减,prev_power_of_two 算的是:

fn prev_power_of_two(n: usize) -> usize {
    // Only way this shift can underflow is if n is less than 4.
    // (Which would means `usize::MAX >> 64` and underflowed!)
    debug_assert!(n >= 4);
    (usize::MAX >> (n.leading_zeros() + 2)) + 1
}

417792 的最高位是第 18 位(2^18 = 262144),leading_zeros 是 45,usize::MAX >> 47 再加 1 是 2^17 = 131072。所以从上限往下减,一步就回到 131072,而不是 262144。io.rs 的单元测试 read_strategy_adaptive_max_fuzz 用一串不是 2 的幂的 max 检查「decrement should be powers of two」,保证的就是这一点:减下来的值总是 2 的幂。

客户端还可以用 read_buf_exact_size 换成 Exact(sz):每次都读 sz,max() 也是 sz。它和 max_buf_size 互斥,设一个会清掉另一个。

实验:上传 1 MiB 时每次读多少

用一个包装类型记下 hyper 对 IO 的每一次读写:room 是交给 poll_read 的空间(ReadBufCursor::remaining()),got 是实际读到的字节数。

hyper = { version = "=1.12.0", features = ["server", "http1"] }
http-body-util = "=0.1.5"
bytes = "1"
futures = "0.3"
tokio = { version = "=1.54.1", features = ["rt", "net", "macros", "io-util", "time", "sync"] }
#[path = "../common.rs"]
mod common;

use std::convert::Infallible;
use std::io;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll};
use std::time::Duration;

use bytes::Bytes;
use http_body_util::{combinators::BoxBody, BodyExt, Full, StreamBody};
use hyper::body::{Frame, Incoming};
use hyper::rt::{Read, ReadBufCursor, Write};
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};

type Log = Arc<Mutex<Vec<String>>>;

// Wraps a TcpStream and records every read/write hyper makes.
struct Spy {
  inner: TcpStream,
  vectored: bool,
  log: Log,
}

impl Read for Spy {
  fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, mut buf: ReadBufCursor<'_>) -> Poll<io::Result<()>> {
    let room = buf.remaining();
    let mut tmp = vec![0u8; room];
    let mut rb = tokio::io::ReadBuf::new(&mut tmp);
    match Pin::new(&mut self.inner).poll_read(cx, &mut rb) {
      Poll::Ready(Ok(())) => {
        let n = rb.filled().len();
        self.log.lock().unwrap().push(format!("read  room={room:>6} got={n:>6}"));
        buf.put_slice(&tmp[..n]);
        Poll::Ready(Ok(()))
      }
      other => other,
    }
  }
}

impl Write for Spy {
  fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, b: &[u8]) -> Poll<io::Result<usize>> {
    let r = Pin::new(&mut self.inner).poll_write(cx, b);
    if let Poll::Ready(Ok(n)) = r {
      self.log.lock().unwrap().push(format!("write  {n}B"));
    }
    r
  }
  fn poll_write_vectored(mut self: Pin<&mut Self>, cx: &mut Context<'_>, bufs: &[io::IoSlice<'_>]) -> Poll<io::Result<usize>> {
    let r = Pin::new(&mut self.inner).poll_write_vectored(cx, bufs);
    if let Poll::Ready(Ok(n)) = r {
      let lens: Vec<usize> = bufs.iter().map(|b| b.len()).collect();
      self.log.lock().unwrap().push(format!("writev {n}B iovecs={lens:?}"));
    }
    r
  }
  fn is_write_vectored(&self) -> bool {
    self.vectored
  }
  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)
  }
}

type B = BoxBody<Bytes, Infallible>;

async fn handle(req: Request<Incoming>) -> Result<Response<B>, Infallible> {
  match req.uri().path() {
    "/stream" => {
      let frames = vec![Ok::<_, Infallible>(Frame::data(Bytes::from("abc"))), Ok(Frame::data(Bytes::from("defg")))];
      Ok(Response::new(StreamBody::new(futures::stream::iter(frames)).boxed()))
    }
    "/upload" => {
      let n = req.into_body().collect().await.unwrap().to_bytes().len();
      Ok(Response::new(Full::new(Bytes::from(n.to_string())).boxed()))
    }
    _ => Ok(Response::new(Full::new(Bytes::from("hello")).boxed())),
  }
}

async fn run(name: &str, vectored: bool, cfg: impl Fn(&mut http1::Builder), parts: Vec<Vec<u8>>) {
  println!("== {name}");
  let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
  let addr = listener.local_addr().unwrap();
  let log: Log = Default::default();
  let mut builder = http1::Builder::new();
  cfg(&mut builder);
  let server_log = log.clone();
  let server = tokio::spawn(async move {
    let (stream, _) = listener.accept().await.unwrap();
    let io = Spy { inner: stream, vectored, log: server_log };
    builder.serve_connection(io, service_fn(handle)).await.map_err(|e| common::err_chain(&e))
  });

  let mut tcp = TcpStream::connect(addr).await.unwrap();
  for (i, p) in parts.iter().enumerate() {
    if i > 0 {
      tokio::time::sleep(Duration::from_millis(50)).await;
    }
    if tcp.write_all(p).await.is_err() {
      break;
    }
  }
  let mut got = Vec::new();
  let mut buf = vec![0u8; 64 * 1024];
  loop {
    match tokio::time::timeout(Duration::from_millis(300), tcp.read(&mut buf)).await {
      Ok(Ok(n)) if n > 0 => got.extend_from_slice(&buf[..n]),
      _ => break,
    }
  }
  drop(tcp);
  let result = server.await.unwrap();
  for line in log.lock().unwrap().iter() {
    println!("{line}");
  }
  let first_line = String::from_utf8_lossy(&got).lines().next().unwrap_or("").to_string();
  println!("client got {} bytes, first line {first_line:?}", got.len());
  println!("server: {result:?}");
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  // 1. Read sizes while a 1 MiB body streams in.
  let mut upload = b"POST /upload HTTP/1.1\r\ncontent-length: 1048576\r\n\r\n".to_vec();
  upload.extend(std::iter::repeat_n(b'x', 1 << 20));
  run("upload 1 MiB", true, |_| {}, vec![upload]).await;

  // 2. Write strategy for a chunked response.
  let get_stream = b"GET /stream HTTP/1.1\r\n\r\n".to_vec();
  run("stream, queue (vectored io)", true, |_| {}, vec![get_stream.clone()]).await;
  run("stream, flatten (io not vectored)", false, |_| {}, vec![get_stream.clone()]).await;
  run("stream, vectored io but writev(false)", true, |b| { b.writev(false); }, vec![get_stream]).await;

  // 3. Two pipelined requests in one write.
  let two = b"GET /a HTTP/1.1\r\n\r\nGET /b HTTP/1.1\r\n\r\n".to_vec();
  run("pipelined, default", false, |_| {}, vec![two.clone()]).await;
  run("pipelined, pipeline_flush(true)", false, |b| { b.pipeline_flush(true); }, vec![two]).await;

  // 4. max_buf_size(8192), a 12 KiB head split over two writes (issue #4081's shape).
  let mut p1 = b"GET / HTTP/1.1\r\nHost: x\r\nX: ".to_vec();
  p1.extend(std::iter::repeat_n(b'a', 7000));
  let mut p2 = vec![b'a'; 5000];
  p2.extend_from_slice(b"\r\n\r\n");
  run("max_buf_size(8192), 12 KiB head", true, |b| { b.max_buf_size(8192); }, vec![p1, p2]).await;
}

common.rs 只用到其中的 err_chain,见解析与编码页。M2 Max、rustc 1.99.0、--release。第一项依赖 socket 里攒了多少数据,每次运行不同;跑了 5 次,前 6 次读每次都一样,后面各不相同。两次运行的输出:

== upload 1 MiB
read  room=  8192 got=  8192
read  room= 16384 got= 16384
read  room= 32768 got= 32768
read  room= 65536 got= 65536
read  room=131072 got=131072
read  room=262144 got= 73260
read  room=262144 got=262144
read  room=417792 got=130604
read  room=417792 got= 90112
read  room=327680 got= 32664
read  room=295016 got=130656
read  room=164360 got= 75234
writev 82B iovecs=[75, 7]
read  room=131072 got=     0
client got 82 bytes, first line "HTTP/1.1 200 OK"
server: Ok(())
== upload 1 MiB
read  room=  8192 got=  8192
read  room= 16384 got= 16384
read  room= 32768 got= 32768
read  room= 65536 got= 65536
read  room=131072 got=131072
read  room=262144 got= 73260
read  room=262144 got=146988
read  room=262144 got= 81660
read  room=262144 got= 81660
read  room=180484 got= 16332
read  room=164152 got=164152
read  room=262144 got=230622
writev 82B iovecs=[75, 7]
read  room=262144 got=     0
client got 82 bytes, first line "HTTP/1.1 200 OK"
server: Ok(())

按第一次运行逐行对照 record:

  • 前五次都读满,next 从 8192 翻到 262144。
  • 第 6 次只读到 73260,小于 prev_power_of_two(262144) = 131072,记一笔 decrease_now。第 7 次读满 262144,翻倍并清掉这笔记录,next = min(524288, 417792) = 417792,第 8 行的 room 正是它。
  • 第 8 次读到 130604,小于 131072,记一笔;第 9 次 90112,又小于,next 减到 131072。之后的 room(327680、295016、164360)比 next 大,是因为 BytesMut 里还有剩余容量,只有不够 next 时才 reserve。
  • 1 MiB 的 body 比 max_buf_size 大一倍多,照样读完:body 的字节被 read_mem 切走交给 Incoming,读缓冲不会累积。

头部太大:max_buf_size 与 431

Buffered::parse 是一个循环:解析、没收全就检查长度、再读一次。

        loop {
            if let Some(msg) = super::role::parse_headers::<S>(
                &mut self.read_buf,
                self.partial_len,
                ParseContext {
                    // ...
                },
            )? {
                debug!("parsed {} headers", msg.head.headers.len());
                self.partial_len = None;
                return Poll::Ready(Ok(msg));
            } else {
                let curr_len = self.read_buf.len();
                if let Some(max_header_size) = parse_ctx.h1_max_header_size {
                    if curr_len >= max_header_size {
                        debug!("max_header_size ({}) reached, closing", max_header_size);
                        return Poll::Ready(Err(crate::Error::new_too_large()));
                    }
                }
                let max = self.read_buf_strategy.max();
                if curr_len >= max {
                    debug!("max_buf_size ({}) reached, closing", max);
                    return Poll::Ready(Err(crate::Error::new_too_large()));
                }
                if curr_len > 0 {
                    trace!("partial headers; {} bytes so far", curr_len);
                    self.partial_len = Some(curr_len);
                } else {
                    // 1xx gobled some bytes
                    self.partial_len = None;
                }
            }
            if ready!(self.poll_read_from_io(cx)).map_err(crate::Error::new_io)? == 0 {
                trace!("parse eof");
                return Poll::Ready(Err(crate::Error::new_incomplete()));
            }
        }

缓冲涨到 max_buf_size 还没解析出完整的头,就是「message head is too large」,服务端回 431(on_error 见解析与编码)。partial_len 就是解析与编码页里 is_complete_fast 用的 prev_len。读到 0 字节(对端关了)是 IncompleteMessage。

这个检查只在「头没收全」的分支里做。头一旦完整,就不再看长度。所以要保证缓冲永远不会在头完整之前超过 max,靠的是 poll_read_from_io 里 next 的那个 min:每次最多只读到 max - read_buf.len(),缓冲恰好填到 max 时还不完整,下一轮循环就报错。

📝NOTE
冷知识:max_buf_size 在 1.11.0 之前可以被绕过

issue #4081 报告:「In HTTP/1, Buffered::parse, the check only runs when parse_headers returns None (incomplete headers).」当时 poll_read_from_io 的第一行是 let next = self.read_buf_strategy.next();,没有和 max - read_buf.len() 取小。issue 给的复现是 max_buf_size(8192),先发 7000 多字节的头,停一下,再发 5000 字节和结尾的空行:第二次读的空间仍是 8192,一次就把剩下的全读进来,头完整了,长度检查被跳过,「The server happily responds HTTP/1.1 200 OK and the handler sees the oversized X header.」

#4093(首次发布于 v1.11.0)加上了上面那个 cmp::min,同时在 tests/server.rs 里加了回归测试。下面的实验用的就是 issue 里的形状。

实验第 4 项,连跑 3 次输出相同:

== max_buf_size(8192), 12 KiB head
read  room=  8192 got=  7028
read  room=  1164 got=  1164
writev 123B iovecs=[123]
client got 123 bytes, first line "HTTP/1.1 431 Request Header Fields Too Large"
server: Err("message head is too large")

第一次读到 7028 字节,没收全;第二次的空间是 8192 − 7028 = 1164,恰好把缓冲填到 8192,仍然没收全,回 431。剩下的近 4000 字节根本没读。

几个相关的值:

  • max_buf_size 不能小于 8192(MINIMUM_MAX_BUFFER_SIZE,等于 INIT_BUFFER_SIZE),否则 builder 直接 panic:「the max_buf_size cannot be smaller than the minimum that h1 specifies.」
  • max_header_size(1.12.0 新增,默认 None)是一个更直接的头部上限,在同一个循环里先检查;解析成功后 Server::parse 还会再对比一次完整头部的长度。
  • max_buf_size 同时也是写缓冲的上限,见下一节。

写:先攒再写

WriteBuf:

pub(super) struct WriteBuf<B> {
    /// Re-usable buffer that holds message headers.
    headers: Cursor<Vec<u8>>,
    max_buf_size: usize,
    /// Deque of user buffers if strategy is `Queue`.
    queue: BufList<B>,
    strategy: WriteStrategy,
}

headers 是一个可复用的 Vec<u8>,初始容量 8192,Conn::encode_head 把消息头直接写进去(Buffered::headers_buf)。body 的块(EncodedBuf)怎么放,看 WriteStrategy。WriteBuf::buffer:

        match self.strategy {
            WriteStrategy::Flatten => {
                let head = self.headers_mut();

                head.maybe_unshift(buf.remaining());
                // ...
                loop {
                    let adv = {
                        let slice = buf.chunk();
                        if slice.is_empty() {
                            return;
                        }
                        head.bytes.extend_from_slice(slice);
                        slice.len()
                    };
                    buf.advance(adv);
                }
            }
            WriteStrategy::Queue => {
                // ...
                self.queue.push(buf.into());
            }
        }
  • Flatten:把 body 拷进 headers 这个 Vec 后面,flush 时一次 poll_write 写一整段。maybe_unshift 在空间不够时先把已写出的前缀挪掉,复用内存。
  • Queue:body 的 buf 原样放进一个 VecDeque(BufList),flush 时用 chunks_vectored 收集成最多 64 个 IoSlice(MAX_WRITEV_BUFS),一次 poll_write_vectored。EncodedBuf 里的 Chain 每一段都是一个 IoSlice,用户数据不拷贝。

选哪个,在 Buffered::new 里按 IO 自己说的定:

    pub(crate) fn new(io: T) -> Buffered<T, B> {
        let strategy = if io.is_write_vectored() {
            WriteStrategy::Queue
        } else {
            WriteStrategy::Flatten
        };

这就是 writev 选项文档里的「Default is auto」。hyper-util 的 TokioIo 把 is_write_vectored 转给 tokio,TcpStream 返回 true,所以裸 TCP 默认是 Queue。显式设置会覆盖:writev(true) 强制 Queue,writev(false) 强制 Flatten。文档说关掉它「may also improve performance when an IO transport doesn't support vectored writes well, such as most TLS implementations」:TLS 层要把明文加密成记录,多个小 IoSlice 未必比一整段好处理。

实验第 2 项,同一个两帧(abc、defg)的 chunked 响应,连跑 3 次输出相同:

== stream, queue (vectored io)
read  room=  8192 got=    24
writev 106B iovecs=[84, 3, 3, 2, 3, 4, 2, 5]
read  room=  8192 got=     0
client got 106 bytes, first line "HTTP/1.1 200 OK"
server: Ok(())
== stream, flatten (io not vectored)
read  room=  8192 got=    24
write  106B
read  room=  8192 got=     0
client got 106 bytes, first line "HTTP/1.1 200 OK"
server: Ok(())
== stream, vectored io but writev(false)
read  room=  8192 got=    24
write  106B
read  room=  8192 got=     0
client got 106 bytes, first line "HTTP/1.1 200 OK"
server: Ok(())

Queue 下 8 个 IoSlice:84 字节的响应头,3\r\n、abc、\r\n,4\r\n、defg、\r\n,最后 0\r\n\r\n。两帧都立即就绪,Dispatcher 在 flush 前把整个响应都放进了写缓冲,所以只有一次系统调用。Flatten 下同样是一次,只是 106 字节先拷进了一个 Vec。上面上传实验的响应是 Full,iovecs=[75, 7]:头和 "1048576" 两段。

写缓冲的上限与顺序

WriteBuf::can_buffer:

    fn can_buffer(&self) -> bool {
        match self.strategy {
            WriteStrategy::Flatten => self.remaining() < self.max_buf_size,
            WriteStrategy::Queue => {
                self.queue.bufs_cnt() < MAX_BUF_LIST_BUFFERS && self.remaining() < self.max_buf_size
            }
        }
    }

写缓冲里攒的字节不能超过 max_buf_size,Queue 下 buf 的个数也不能超过 MAX_BUF_LIST_BUFFERS(16)。不能再攒时,Dispatcher 的 poll_write 先 flush,flush 不动就返回 Pending,不再 poll_frame 响应 body。socket 写不出去,body 就不会被继续拉取,这是写方向的背压。

Queue 还有一个顺序上的约束,can_headers_buf:

    /// Return whether we can append to the headers buffer.
    ///
    /// Reasons we can't:
    /// - The write buf is in queue mode, and some of the past body is still
    ///   needing to be flushed.
    pub(crate) fn can_headers_buf(&self) -> bool {
        !self.write_buf.queue.has_remaining()
    }

WriteBuf 作为 Buf 时永远先吐 headers 再吐 queue。如果上一个响应的 body 还在 queue 里,下一个响应的头就写进 headers,它会跑到上一个 body 的前面。所以 Conn::can_write_head 在 Writing::Init 时还要问一句 can_headers_buf,queue 没写完就不写下一个头。Flatten 没有这个问题,所有东西都按顺序拷在同一个 Vec 里。

pipeline_flush

HTTP/1.1 允许客户端不等响应就连发几个请求(pipelining)。Dispatcher 仍然一个一个处理,但每处理完一个,poll_loop 都会 flush 一次。pipeline_flush(true) 改的是 Buffered::poll_flush 的第一个分支:

    pub(crate) fn poll_flush(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
        if self.flush_pipeline && !self.read_buf.is_empty() {
            Poll::Ready(Ok(()))
        } else if self.write_buf.remaining() == 0 {
            Pin::new(&mut self.io).poll_flush(cx)
        } else {
            if let WriteStrategy::Flatten = self.write_buf.strategy {
                return self.poll_flush_flattened(cx);
            }
            // ...

读缓冲里还有没处理的字节(多半是下一个流水线请求),就假装 flush 完了,先不写。等读缓冲空了再一起写。打开它时 set_flush_pipeline 还会把写策略切成 Flatten。另外 Buffered::can_buffer 是 self.flush_pipeline || self.write_buf.can_buffer():打开后上一节的写缓冲上限不再生效。文档把它标为「Experimental, may have bugs」,默认关。

交还 IO

Buffered::into_inner 返回 (io, read_buf.freeze()):IO 本身,加上读缓冲里已经读进来、还没被解析的字节。写缓冲里的字节不在返回值里。Connection::into_parts 交给用户的 read_buf 和协议升级后 Upgraded 前面那段 Rewind 的字节,都来自这里,见服务端 Builder 与 Connection 和协议升级。

❗IMPORTANT
动脑筋:两个流水线请求,几次写?

实验第 3 项:一次 write_all 发出两个请求,GET /a 和 GET /b,每个响应都是 Full::new("hello")。IO 不支持向量写(Flatten)。默认配置下,服务端调了几次 poll_write?打开 pipeline_flush(true) 呢?

动脑筋答案

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

== pipelined, default
read  room=  8192 got=    38
write  80B
write  80B
read  room=  8192 got=     0
client got 160 bytes, first line "HTTP/1.1 200 OK"
server: Ok(())
== pipelined, pipeline_flush(true)
read  room=  8192 got=    38
write  160B
read  room=  8192 got=     0
client got 160 bytes, first line "HTTP/1.1 200 OK"
server: Ok(())
  • 两个请求共 38 字节,一次读进读缓冲。
  • 默认:处理 /a、写好响应后,poll_loop 调 poll_flush,80 字节写出去;再处理 /b,又写一次。两次 poll_write。
  • pipeline_flush(true):写好 /a 的响应时,读缓冲里还躺着 GET /b 的 19 字节,poll_flush 直接返回 Ready,什么都不写;处理完 /b,读缓冲空了,两个响应 160 字节一次写出。一次 poll_write。
  • 每个响应 80 字节:HTTP/1.1 200 OK\r\n、content-length: 5\r\n、date: 加 29 字节的日期和 \r\n\r\n,再加 hello。

小结

  • Buffered 持有 IO、一个 BytesMut 读缓冲和一个 WriteBuf 写缓冲;Conn 的所有 IO 都经过它。
  • 每次读的大小由 ReadStrategy::Adaptive 决定:从 8192 起,读满就翻倍,连续两次读得不到一半才减半,最低 8192,最高 max_buf_size(默认 8192 + 4096×100 = 417792)。客户端可以用 read_buf_exact_size 固定大小。
  • 解析消息头时,读缓冲到 max_buf_size(或 max_header_size)还不完整就是「message head is too large」,服务端回 431。每次读最多读到 max - len,保证头完整前不会越过上限(1.11.0 的 #4093 起)。body 不受这个上限影响。
  • 写缓冲按 is_write_vectored 自动选:Queue 把 EncodedBuf 排进队列,用 writev 一次写出,不拷贝;Flatten 拷进一个 Vec,一次 write。writev(bool) 可以覆盖,TLS 下关掉可能更好。
  • 写缓冲同样以 max_buf_size 为上限(Queue 下还限 16 个 buf),满了就先 flush,flush 不动就不再拉 body。Queue 下上一个 body 没写完不写下一个头,保证顺序。
  • pipeline_flush 在读缓冲非空时推迟 flush,把流水线请求的响应攒成一次写;会强制 Flatten 并取消写缓冲上限,是实验性选项。
  • into_inner 交还 IO 和读缓冲里没解析的字节,into_parts 和协议升级都靠它。

💬 关联讨论

发起讨论
加载中...