Service 与 Body
主线位置:服务端主线的中段。
Dispatcher解析出Request<Incoming>后调service.call(req),等出Response<B>,再一帧一帧 pollB写出去。客户端主线上方向相反:你交出的请求 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.x | tower 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。
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) 一次的写法成本很低。
问: 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。空的数据帧直接丢掉。
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。