Rust in Everything
基于 hyper 1.12.0

Service 与 Body

主线位置:服务端主线的中段。Dispatcher 解析出 Request<Incoming> 后调 service.call(req),等出 Response<B>,再一帧一帧 poll B 写出去。客户端主线上方向相反:你交出的请求 body 是 B,收到的响应 body 是 Incoming。

flowchart TB
  req["Request 带 Incoming"] --> callreq["service.call(req)"]
  callreq --> fut["in_flight 里的 future"]
  fut --> resp["Response 带 body B"]
  resp --> eos{"is_end_stream?"}
  eos -->|是| nobody["没有 body"]
  eos -->|否| hint{"size_hint 精确?"}
  hint -->|是| known["Known(n)"]
  hint -->|否| unknown["Unknown"]
  known --> frames["循环 poll_frame"]
  unknown --> frames

这一页讲图上的两个接口:左上角的 Service,右下角的 Body。中间的 in_flight 和后面怎么把 Known / Unknown 变成响应头,分别在 Dispatcher 和 body 的编解码。

hyper 自己的 Service

hyper::service::Service,去掉类型上的文档注释:

pub trait Service<Request> {
    type Response;
    type Error;
    type Future: Future<Output = Result<Self::Response, Self::Error>>;

    /// Process the request and return the response asynchronously.
    /// `call` takes `&self` instead of `mut &self` because:
    /// - It prepares the way for async fn,
    ///   since then the future only borrows `&self`, and thus a Service can concurrently handle
    ///   multiple outstanding requests at once.
    /// - It's clearer that Services can likely be cloned.
    /// - To share state across clones, you generally need `Arc<Mutex<_>>`
    ///   That means you're not really using the `&mut self` and could do with a `&self`.
    ///   The discussion on this is here: <https://github.com/hyperium/hyper/issues/3040>
    fn call(&self, req: Request) -> Self::Future;
}

和 tower 的 Service(见 Service trait)比:

hyper 1.xtower 0.5
poll_ready没有有,call 之前必须拿到 Ready(Ok)
call 的接收者&self&mut self
转发实现&S、&mut S、Box<S>、Rc<S>、Arc<S>&mut S、Box<S>
由谁定义hyper 自己tower-service

两处删减各有来由。

call(&self):注释里列了三条理由,最后一条最实在:服务通常要被克隆(每条连接一份),克隆之间共享状态本来就要 Arc<Mutex<_>>,那时 &mut self 已经没有用处。第一条说的是以后 call 写成 async fn 时,返回的 future 只借用 &self,同一个服务就能同时有多个在途请求。因为是 &self,Rc<S> 和 Arc<S> 也能直接转发。

没有 poll_ready:理由写在 hyper 仓库的 docs/ROADMAP-1.0.md 里:

### Should there be a simplified `hyper::Service` trait, or should hyper depend on `tower-service`?

- There's still a few uncertain decisions around tower, such as if it should be
  changed to `async fn call`, and if `poll_ready` is the best way to handle
  backpressure.
- It's not clear that the backpressure is something needed at the `Server`
  boundary, thus meaning we should remove `poll_ready` from hyper.

HTTP/1 连接上,一个请求的响应(连同 body)没写完,Conn 不会去读下一个请求头,这是真正让请求串行的那道闸(实验见 Dispatcher)。Dispatcher 内部还有自己的 poll_ready,看的不是服务,而是 Server::poll_ready 里那一个在途槽位,是另一道保险:

        fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), ()>> {
            if self.in_flight.is_some() {
                Poll::Pending
            } else {
                Poll::Ready(Ok(()))
            }
        }

上一个请求的 future 没完成,就不读下一个请求头,客户端发得再快也只会堆在 socket 和读缓冲里。这个槽位怎么让同一条连接上的请求严格串行,见 Dispatcher。

📝NOTE
冷知识:poll_ready 在 hyper 里加过,又拿掉了

hyper 0.12.25 通过 #1767 给当时的 Service 和 MakeService 加上了 poll_ready,PR 说明是「These methods corresponds to tower_service::Service::poll_ready and prevents to ignore calling this method when using tower's services in hyper::Server or hyper::server::conn::Connection.」也就是说,那时 hyper 的服务器要先问服务的 poll_ready。从 v0.13.0 起 hyper 干脆直接用 tower_service::Service。

1.0 的第一个预发布 v1.0.0-rc.1 包含了 #2920(关联 issue #2853:「Make a simplified Service trait that is only the call method」),提交信息是「This removes the dependency on tower-service, and simplifies the Service trait to be used by hyper's server connections.」,BREAKING CHANGE 一栏写着「The poll_ready method has been removed.」那时 call 还是 &mut self,到 v1.0.0-rc.4 才由 #3223 改成 &self(讨论在 issue #3040),上面 call 的那段注释就是这个 PR 写进去的,三条理由就是 PR 说明里的那三条(后来的文档提交只改了措辞和格式)。

HttpService:封闭的别名

http1::Builder::serve_connection 的约束不是 Service,而是 HttpService:

pub trait HttpService<ReqBody>: sealed::Sealed<ReqBody> {
    type ResBody: Body;
    type Error: Into<Box<dyn StdError + Send + Sync>>;
    type Future: Future<Output = Result<Response<Self::ResBody>, Self::Error>>;

    #[doc(hidden)]
    fn call(&mut self, req: Request<ReqBody>) -> Self::Future;
}

impl<T, B1, B2> HttpService<B1> for T
where
    T: Service<Request<B1>, Response = Response<B2>>,
    B2: Body,
    T::Error: Into<Box<dyn StdError + Send + Sync>>,
{
    type ResBody = B2;

    type Error = T::Error;
    type Future = T::Future;

    fn call(&mut self, req: Request<B1>) -> Self::Future {
        Service::call(self, req)
    }
}

它是一个 trait 别名:凡是「收 Request<B1>、回 Response<B2>、B2 是 body、错误能转成 Box<dyn Error>」的 Service 都自动是 HttpService<B1>。好处是 hyper 内部的约束可以按 body 类型写(S: HttpService<IncomingBody>,然后用 S::ResBody),不用每处都把 Service<Request<..>, Response = Response<..>> 展开一遍。超 trait Sealed 外部实现不了,所以别人只能实现 Service,没法绕过这条 blanket impl。

HttpService::call 是隐藏的,接收者还是 &mut self,对使用者没有影响:它只在 hyper 内部调用,比如 HTTP/1 的 Dispatcher 收到请求时的 self.service.call(req),转进来就是 Service::call(&self)。

关于 Error,Service 的文档注释说得很直接:服务返回错误时,「In most cases, hyper will cause the connection to be abruptly aborted」,HTTP/2 下是给这个流发 RST_STREAM;想让客户端看到一个状态码,就返回一个 4xx / 5xx 的 Response,而不是 Err。各种错误最后变成什么,见 hyper::Error。

service_fn

大多数时候不用手写 Service。service_fn 把一个闭包包成 ServiceFn:

impl<F, ReqBody, Ret, ResBody, E> Service<Request<ReqBody>> for ServiceFn<F, ReqBody>
where
    F: Fn(Request<ReqBody>) -> Ret,
    ReqBody: Body,
    Ret: Future<Output = Result<Response<ResBody>, E>>,
    E: Into<Box<dyn StdError + Send + Sync>>,
    ResBody: Body,
{
    type Response = crate::Response<ResBody>;
    type Error = E;
    type Future = Ret;

    fn call(&self, req: Request<ReqBody>) -> Self::Future {
        (self.f)(req)
    }
}

闭包要求是 Fn,不是 FnMut,这是 call(&self) 的直接结果。ServiceFn 在 F: Clone 时 Clone,在 F: Copy 时 Copy,所以每条连接 service_fn(handler) 一次的写法成本很低。

📝NOTE
没有蠢问题

问: call 只给 &self,闭包又只能是 Fn,那想在服务里数一数处理了多少请求怎么办?

答: 把要改的状态放进能通过共享引用修改的东西里:Arc<AtomicUsize>、Arc<Mutex<_>>。闭包捕获一个 Arc 的克隆,每条连接克隆一次闭包,所有连接看到的是同一个计数器。这正是 call(&self) 注释里第三条理由说的情况:既然跨克隆共享状态总要走 Arc,&mut self 本来也帮不上忙。

TowerToHyperService:每个请求克隆一次

tower 的服务(axum 的 Router 也是)不能直接交给 hyper:签名对不上,而且它有 poll_ready。hyper-util 的 TowerToHyperService 负责转换,call 只有一行:

impl<S, R> hyper::service::Service<R> for TowerToHyperService<S>
where
    S: tower_service::Service<R> + Clone,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = TowerToHyperServiceFuture<S, R>;

    fn call(&self, req: R) -> Self::Future {
        TowerToHyperServiceFuture {
            future: Oneshot::new(self.service.clone(), req),
        }
    }
}

hyper 只给 &self,而 tower 的 poll_ready 和 call 都要 &mut self,所以只能先克隆一份自己拥有的服务。克隆出来的服务连同请求一起交给 Oneshot,这是从 tower 抄过来的一份(注释写着「Vendored from tower::util to reduce dependencies」),poll 先等 poll_ready,就绪后 call,再等响应 future:

            match this {
                OneshotProj::NotReady { svc, req } => {
                    ready!(svc.poll_ready(cx))?;
                    let fut = svc.call(req.take().expect("already called"));
                    self.set(Oneshot::Called { fut });
                }
                OneshotProj::Called { fut } => {
                    let res = ready!(fut.poll(cx))?;
                    self.set(Oneshot::Done);
                    return Poll::Ready(Ok(res));
                }
                OneshotProj::Done => panic!("polled after complete"),
            }

于是 tower 的「先 poll_ready 再 call」契约照样被遵守,只是每次都发生在一个刚克隆出来的副本上(tower 一侧的状态机见 ServiceExt 与 Oneshot)。poll_ready 返回 Pending 时,挂起的是这个请求的响应 future,HTTP/1 的 Dispatcher 在这个响应写完之前,也不会去读下一个请求。

下面的服务在 Clone、poll_ready、call 里各计一次数,call 还会给自己身上的 hits 加一。一条连接上流水线式地发三个请求:

hyper = { version = "=1.12.0", features = ["server", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio", "service"] }
tower = "=0.5.3"
http-body-util = "0.1"
bytes = "1"
tokio = { version = "=1.54.1", features = ["full"] }
use std::convert::Infallible;
use std::future::{ready, Ready};
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
use std::task::{Context, Poll};

use bytes::Bytes;
use http_body_util::Full;
use hyper::{body::Incoming, server::conn::http1, Request, Response};
use hyper_util::{rt::TokioIo, service::TowerToHyperService};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};

static CLONES: AtomicUsize = AtomicUsize::new(0);
static READY: AtomicUsize = AtomicUsize::new(0);
static CALLS: AtomicUsize = AtomicUsize::new(0);

// A tower service that counts clones, poll_ready and call.
struct Counted {
  hits: usize, // per-instance state
}

impl Clone for Counted {
  fn clone(&self) -> Self {
    CLONES.fetch_add(1, SeqCst);
    Counted { hits: self.hits }
  }
}

impl tower::Service<Request<Incoming>> for Counted {
  type Response = Response<Full<Bytes>>;
  type Error = Infallible;
  type Future = Ready<Result<Self::Response, Infallible>>;

  fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Infallible>> {
    READY.fetch_add(1, SeqCst);
    Poll::Ready(Ok(()))
  }

  fn call(&mut self, _req: Request<Incoming>) -> Self::Future {
    CALLS.fetch_add(1, SeqCst);
    self.hits += 1;
    ready(Ok(Response::new(Full::new(Bytes::from(format!("hits={}", self.hits))))))
  }
}

#[tokio::main]
async fn main() {
  let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
  let addr = listener.local_addr().unwrap();
  let server = tokio::spawn(async move {
    let (stream, _) = listener.accept().await.unwrap();
    let svc = TowerToHyperService::new(Counted { hits: 0 });
    println!("after new:     clones={}", CLONES.load(SeqCst));
    http1::Builder::new().serve_connection(TokioIo::new(stream), svc).await.unwrap();
  });

  let mut client = TcpStream::connect(addr).await.unwrap();
  // three pipelined requests on one connection, the last one closes it
  client
    .write_all(
      b"GET / HTTP/1.1\r\nHost: x\r\n\r\n\
        GET / HTTP/1.1\r\nHost: x\r\n\r\n\
        GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n",
    )
    .await
    .unwrap();
  let mut buf = String::new();
  client.read_to_string(&mut buf).await.unwrap();
  server.await.unwrap();
  let bodies: Vec<&str> = buf.match_indices("hits=").map(|(i, _)| &buf[i..i + 6]).collect();
  println!("bodies:        {bodies:?}");
  println!(
    "after 3 reqs:  clones={} poll_ready={} call={}",
    CLONES.load(SeqCst),
    READY.load(SeqCst),
    CALLS.load(SeqCst)
  );
}

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

after new:     clones=0
bodies:        ["hits=1", "hits=1", "hits=1"]
after 3 reqs:  clones=3 poll_ready=3 call=3
  • TowerToHyperService::new 只是把服务移进去,不克隆。
  • 三个请求,三次克隆,每个副本 poll_ready 一次、call 一次。
  • 三个响应都是 hits=1:每次 call 改的是刚克隆出来的副本,副本随响应 future 一起丢掉,连接手里那一份的 hits 始终是 0。服务字段上的状态活不过一个请求,要跨请求就得放进 Arc。axum 在这之上还有「每条连接从 make service 拿一份」的一层,见 Steer 与 MakeService。

Body:一次一帧

hyper 的 hyper::body::Body 就是 http-body 的 Body,body/mod.rs 里原样 re-export,连同 Frame、SizeHint 和 bytes 的 Buf、Bytes。hyper 自己只提供一个实现:收到的 body Incoming,它是怎么由连接任务喂数据的,见 Incoming 与 body 通道。Full、Empty、StreamBody 这些现成的 body 在 http-body-util 里,见 http-body-util。

http-body 1.1.0 的 Body,去掉文档注释:

pub trait Body {
    type Data: Buf;
    type Error;

    fn poll_frame(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>>;

    fn is_end_stream(&self) -> bool {
        false
    }

    fn size_hint(&self) -> SizeHint {
        SizeHint::default()
    }
}
方法含义hyper 怎么用
poll_frame下一帧:Some(Ok(frame))、Some(Err(e))、None(结束,之后一直 None)写 body 时循环调用,每帧写一次
is_end_stream提示「已经没有帧了」;返回 false 不代表一定还有写响应头之前问一次,true 就当没有 body
size_hint剩余长度的上下界,上下界相等时 exact() 给出精确值写响应头之前问一次,决定分帧方式

Frame 里是一个私有的 Kind 枚举,现在只有 Data(T) 和 Trailers(HeaderMap) 两种,源码里还留着一行注释掉的 Unknown。外面只能通过 is_data、into_data、trailers_ref 这类方法看它。hyper 的 ROADMAP-1.0.md 里「How could the Body trait prepare for unknown frames?」一节讨论的就是给以后的新帧种类留位置。

Dispatcher 写响应的地方(poll_write)把两个提示变成 BodyLength:

                    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);

三种结果:None(没有 body,body 对象直接丢掉,不再 poll)、Known(n)、Unknown。write_head 再按请求方法、状态码、版本和响应里已有的头,把它们变成 content-length 或 transfer-encoding: chunked,规则见 body 的编解码。之后的循环每次 poll_frame 一帧:数据帧交给 write_body,如果拿到这一帧后 is_end_stream() 已经是 true,就用 write_body_and_end 把这一帧和结束一起写;trailers 帧交给 write_trailers;None 就 end_body。空的数据帧直接丢掉。

❗IMPORTANT
动脑筋:四种 body,四个响应头
hyper = { version = "=1.12.0", features = ["server", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio"] }
http-body-util = "0.1"
bytes = "1"
futures = "0.3"
tokio = { version = "=1.54.1", features = ["full"] }
use std::convert::Infallible;

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

async fn handler(req: Request<Incoming>) -> Result<Response<BoxBody<Bytes, Infallible>>, Infallible> {
  let body = match req.uri().path() {
    "/full" => Full::new(Bytes::from_static(b"hello")).boxed(),
    "/full-empty" => Full::new(Bytes::new()).boxed(),
    "/empty" => Empty::<Bytes>::new().boxed(),
    _ => {
      let frames = futures::stream::iter([Ok::<_, Infallible>(Frame::data(Bytes::from_static(b"hello")))]);
      StreamBody::new(frames).boxed()
    }
  };
  Ok(Response::new(body))
}

#[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 (s, _) = listener.accept().await.unwrap();
    let mut b = http1::Builder::new();
    b.auto_date_header(false);
    b.serve_connection(TokioIo::new(s), service_fn(handler)).await.unwrap();
  });
  let mut c = TcpStream::connect(addr).await.unwrap();
  c.write_all(
    b"GET /full HTTP/1.1\r\nHost: x\r\n\r\n\
      GET /full-empty HTTP/1.1\r\nHost: x\r\n\r\n\
      GET /empty HTTP/1.1\r\nHost: x\r\n\r\n\
      GET /stream HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n",
  )
  .await
  .unwrap();
  let mut out = String::new();
  c.read_to_string(&mut out).await.unwrap();
  print!("{}", out.replace("\r\n", "\\r\\n\n"));
}

auto_date_header(false) 去掉了 date 头,好让输出每次都一样。四个响应里,哪几个带 content-length、值是多少?哪个是 chunked?Full::new(Bytes::new()) 和 Empty 有没有区别?

动脑筋答案

M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同(每行末尾的 \r\n 是程序替换出来的,便于看清分隔):

HTTP/1.1 200 OK\r\n
content-length: 5\r\n
\r\n
helloHTTP/1.1 200 OK\r\n
content-length: 0\r\n
\r\n
HTTP/1.1 200 OK\r\n
content-length: 0\r\n
\r\n
HTTP/1.1 200 OK\r\n
connection: close\r\n
transfer-encoding: chunked\r\n
\r\n
5\r\n
hello\r\n
0\r\n
\r\n
  • /full:Full 的 size_hint 是精确的 5,Known(5),写出 content-length: 5,后面紧跟 5 个字节的 hello,下一个响应直接接在后面。
  • /full-empty:Full::new 发现数据为空就不存它(http-body-util 0.1.5 里 data.has_remaining() 为假时存 None),is_end_stream() 为 true,走「没有 body」的分支。
  • /empty:Empty::is_end_stream 恒为 true,和上一个完全一样。没有 body 的 200 响应,hyper 写的是 content-length: 0。
  • /stream:StreamBody 不知道总长度,size_hint 是默认的「下界 0、没有上界」,exact() 为 None,Unknown,于是用 chunked:5\r\nhello\r\n 一块,0\r\n\r\n 结束。connection: close 是因为请求里带了 Connection: close。

BoxBody 把 is_end_stream 和 size_hint 原样转发给里面的 body,所以装箱不影响结果。

小结

  • hyper 的 Service 只有 call(&self):没有 poll_ready(ROADMAP 的理由是服务器边界上不需要这层背压,#2920,v1.0.0-rc.1),&self 让克隆之间共享状态、同时处理多个请求更自然(#3223,v1.0.0-rc.4)。
  • HTTP/1 连接的背压在连接本身:当前响应(含 body)写完之前,Conn 不读下一个请求头;Server::poll_ready 检查在途槽位是另一道保险。
  • HttpService<ReqBody> 是封闭的别名,所有「Request 进、Response 出」的 Service 自动实现它;服务返回 Err 通常意味着连接被中止,想回错误码就返回一个 Response。
  • TowerToHyperService::call 每个请求 clone 一次 tower 服务,交给 vendored 的 Oneshot 走 poll_ready → call → 等响应;服务字段上的状态活不过一个请求。
  • Body::poll_frame 一次一帧(数据或 trailers)。写响应头前,is_end_stream() 为真就当没有 body,否则 size_hint().exact() 有值就是 Known(n),没有就是 Unknown;实测分别对应 content-length: 0、content-length: n、transfer-encoding: chunked。

💬 关联讨论

发起讨论
加载中...