协议升级
主线位置:服务端主线的一条岔路。正常情况下一个请求处理完,连接回到
Init等下一个请求;升级时,101响应写完以后这条连接不再说 HTTP,Dispatcher退出,IO 交给服务自己。客户端主线上是对称的:收到101(或CONNECT的 2xx)后,Connection任务把 IO 交给拿着响应的一方。
sequenceDiagram participant C as 客户端 participant D as Dispatcher participant S as 服务 participant U as 升级任务 C->>D: 请求,带 Upgrade 头 D->>D: prepare_upgrade:Pending 留在 State D->>S: call,extensions 里有 OnUpgrade S->>U: spawn,await upgrade::on S-->>D: 响应 101 D->>C: 写出 101 并刷新 D->>D: 读写都 Closed,返回 Upgrade D->>U: fulfill:IO 加剩余字节 U->>C: 之后说别的协议
hyper 不认识 WebSocket 或任何升级后的协议。它只负责三件事:认出「这个请求可能要升级」,在响应写完后停止解析 HTTP,把 IO 原样交出去,连同已经读进缓冲区、但不属于 HTTP 的那些字节。
起点:解析时认出升级请求
role::Server::parse 里,CONNECT 方法一开始就算升级(wants_upgrade 的初值),带 Upgrade 头 的 HTTP/1.1 请求也算:
header::UPGRADE => {
// Upgrades are only allowed with HTTP/1.1
wants_upgrade = is_http_11;
}
Conn::poll_read_head 把它变成 Wants::UPGRADE,Dispatcher 的 poll_read_head 看到这个标志,在调用服务之前往请求头的 extensions 里插一个 OnUpgrade:
if wants.contains(Wants::UPGRADE) {
let upgrade = self.conn.on_upgrade();
debug_assert!(!upgrade.is_none(), "empty upgrade");
debug_assert!(
head.extensions.get::<OnUpgrade>().is_none(),
"OnUpgrade already set"
);
head.extensions.insert(upgrade);
}
on_upgrade 调 State::prepare_upgrade,用 pending() 建一对 tokio oneshot:
pub(super) fn pending() -> (Pending, OnUpgrade) {
let (tx, rx) = oneshot::channel();
(
Pending { tx },
OnUpgrade {
rx: Some(Arc::new(Mutex::new(rx))),
},
)
}
发送端 Pending 留在 Conn 的 State.upgrade 里,接收端 OnUpgrade 随请求交给服务。这一步只是「准备」:服务回不回 101、升不升级,此时还不知道。
客户端方向在 role::Client::decoder 里:响应是 101,或者请求是 CONNECT 且响应是 2xx,就算升级,OnUpgrade 放进响应的 extensions。客户端的 Connection 同样要 with_upgrades() 才会交出 IO,见 conn::http1 客户端。
upgrade::on:从 extensions 里取出来
服务拿到请求后,用 upgrade::on 取出这个 future。它对 Request、&mut Request、Response、&mut Response 都能用,四个实现都一样(CanUpgrade):
impl<B> CanUpgrade for &'_ mut http::Request<B> {
fn on_upgrade(self) -> OnUpgrade {
self.extensions_mut()
.remove::<OnUpgrade>()
.unwrap_or_else(OnUpgrade::none)
}
}
是 remove,所以第二次调用拿到的是 OnUpgrade::none()。传 &mut req 的好处是请求本身还在,服务可以接着读它的头。
OnUpgrade 是一个 future,poll 有四种结局:
| 情况 | 结果 | 错误的 Display |
|---|---|---|
| extensions 里没有(请求不是升级请求,或者已经被取走) | 立即 Err | no upgrade available |
Pending::fulfill 送来了 IO | Ok(Upgraded) | — |
Pending::manual 送来了错误(连接没用 with_upgrades) | Err | upgrade expected but low level API in use |
Pending 没送任何东西就被 drop | Err,source() 是 upgrade expected but not completed | operation was canceled |
OnUpgrade 里是 Arc<Mutex<oneshot::Receiver>> 而不是一个裸的 Receiver,因为它要放进 http::Extensions,而 http 1.x 的 Extensions::insert 要求 T: Clone + Send + Sync + 'static。oneshot::Receiver 不能 Clone,hyper 升级到 http 1.0 时(#3406,v1.0.0,提交说明「To do so, implements Clone for OnUpgrade」)给它套上了 Arc<Mutex<…>>。
0.14 之前,拿升级 future 的方法是 Body::on_upgrade()。#2086 列了它的两个问题:「It makes the Body type fatter」,以及它要求调用方一直留着 Body,「they can't use stream combinators to read the body first, and then wait for the upgrade to finish」,想把 Request<Body> 转成别的 body 类型也很麻烦。提议是改成一个自由函数,参数是请求或响应本身,状态放进 http::Extensions。
#2337(v0.14.0)做了对外的部分:「The method Body::on_upgrade() is gone. It is essentially replaced with hyper::upgrade::on(msg)」。#2353(v0.14.0)做了内部的部分:给内部的 MessageHead 加了 extensions 字段,proto::h1 把升级状态从 body 移到头上。上面 poll_read_head 里的 head.extensions.insert(upgrade) 和 CanUpgrade 的 remove::<OnUpgrade>() 就是这次改动的结果。
101 写出去之后:读写两端都关
服务回 101 Switching Protocols,role::Server::encode 把它标成最后一条消息(encode):
let (ret, is_last) = if msg.head.subject == StatusCode::SWITCHING_PROTOCOLS {
(Ok(()), true)
} else if msg.req_method == &Some(Method::CONNECT) && msg.head.subject.is_success() {
// Sending content-length or transfer-encoding header on 2xx response
// to CONNECT is forbidden in RFC 7231.
wrote_len = true;
(Ok(()), true)
} else if msg.head.subject.is_informational() {
// ...
CONNECT 的 2xx 也是 is_last,并且不写 Content-Length / Transfer-Encoding。之后的过程完全是 Conn 的读写状态机 的常规路径:头写完,写端 Closed;升级请求一般没有 body,读端早已是 KeepAlive;刷新成功后 try_keep_alive 看到「读 KeepAlive、写 Closed」,两端都关。Dispatcher::is_done 为真,poll_inner 先查有没有挂着的升级:
if self.is_done() {
if let Some(pending) = self.conn.pending_upgrade() {
self.conn.take_error()?;
return Poll::Ready(Ok(Dispatched::Upgrade(pending)));
} else if should_shutdown {
// ...
有就返回 Dispatched::Upgrade(pending),不对 IO 调 poll_shutdown。try_keep_alive 在刷新成功之后才调,所以 101 的最后一个字节一定已经写出去了,IO 交出去时写缓冲区是空的。
谁来交出 IO:with_upgrades
Dispatcher 只把 Pending 交到外层,IO 还在它自己身上。外层有两种。
普通的 Connection,它的 poll:
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match ready!(Pin::new(&mut self.conn).poll(cx)) {
Ok(done) => {
match done {
proto::Dispatched::Shutdown => {}
proto::Dispatched::Upgrade(pending) => {
// With no `Send` bound on `I`, we can't try to do
// upgrades here. In case a user was trying to use
// `Body::on_upgrade` with this API, send a special
// error letting them know about that.
pending.manual();
}
}
Poll::Ready(Ok(()))
}
Err(e) => Poll::Ready(Err(e)),
}
}
它调 manual,等升级的一方收到 upgrade expected but low level API in use。注释给了原因:Connection 对 I 没有 Send 约束,而 Upgraded 里装的是 Box<dyn Io + Send>,造不出来。注释里的 Body::on_upgrade 是上面冷知识里那个旧 API 的名字。
with_upgrades 返回的 UpgradeableConnection,它的 poll 要求 I: Read + Write + Unpin + Send + 'static:
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if let Some(conn) = self.inner.as_mut() {
match ready!(Pin::new(&mut conn.conn).poll(cx)) {
Ok(proto::Dispatched::Shutdown) => Poll::Ready(Ok(())),
Ok(proto::Dispatched::Upgrade(pending)) => {
let (io, buf, _) = self
.inner
.take()
.expect("upgradeable server connection missing after upgrade")
.conn
.into_inner();
pending.fulfill(Upgraded::new(io, buf));
Poll::Ready(Ok(()))
}
Err(e) => Poll::Ready(Err(e)),
}
} else {
// inner is `None`, meaning the connection was upgraded, thus it's `Poll::Ready(Ok(()))`
Poll::Ready(Ok(()))
}
}
into_inner 拆出 IO 和读缓冲(Buffered::into_inner 把 BytesMut 冻结成 Bytes),服务被丢掉;fulfill 经 oneshot 把 Upgraded 送给等待的一方。UpgradeableConnection 自己随即以 Ok(()) 完成:从这一刻起,这条连接不再归 hyper 管,它的生命由拿到 Upgraded 的那个任务决定。
还有一条旧路:poll_without_shutdown / without_shutdown 加 into_parts,在连接「HTTP 意义上结束」时把 IO、读缓冲和服务一起拿回来,它内部同样对 Upgrade 调 manual()。这条路属于 Connection 的 API,见 服务端 Builder 与 Connection。hyper-util 的 auto::Builder 也有升级版本的入口,见 auto:一个端口两个协议。
Upgraded:Rewind 加类型擦除
pub struct Upgraded {
io: Rewind<Box<dyn Io + Send>>,
}
两层包装各管一件事。
Rewind 管多读的字节。 客户端完全可以把升级请求和新协议的第一批字节一次写过来(升级到 WebSocket 时,客户端发完请求通常要等 101,但协议没有禁止);服务端读请求头时,这些字节一起进了 hyper 的读缓冲。直接把 socket 交出去,它们就丢了。Rewind 把它们放在 pre 里,poll_read 先吐 pre,吐完才读底层 IO:
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
mut buf: ReadBufCursor<'_>,
) -> Poll<io::Result<()>> {
if let Some(mut prefix) = self.pre.take() {
// If there are no remaining bytes, let the bytes get dropped.
if !prefix.is_empty() {
let copy_len = cmp::min(prefix.len(), buf.remaining());
// TODO: There should be a way to do following two lines cleaner...
buf.put_slice(&prefix[..copy_len]);
prefix.advance(copy_len);
// Put back what's left
if !prefix.is_empty() {
self.pre = Some(prefix);
}
return Poll::Ready(Ok(()));
}
}
Pin::new(&mut self.inner).poll_read(cx, buf)
}
写方向原样转发。hyper-util 的 auto 用同一个思路把判断协议时「偷看」过的字节(最多 24 个)塞回去,见 auto:一个端口两个协议。
Box<dyn Io + Send> 管类型。 OnUpgrade 的输出类型不能带 I:它装在 http::Extensions 里,服务拿到的是 Request<Incoming>,类型上不知道连接的 IO 是什么。于是 IO 被装箱成 trait object,Upgraded 自己实现 rt::Read / rt::Write,可以直接当 IO 用。
知道具体类型时,可以用 downcast 拆回来:
pub fn downcast<T: Read + Write + Unpin + 'static>(self) -> Result<Parts<T>, Self> {
let (io, buf) = self.io.into_inner();
match io.__hyper_downcast() {
Ok(t) => Ok(Parts {
io: *t,
read_buf: buf,
}),
Err(io) => Err(Upgraded {
io: Rewind::new_buffered(io, buf),
}),
}
}
判断靠 TypeId:私有 trait Io 有一个默认方法 __hyper_type_id 返回 TypeId::of::<Self>(),__hyper_downcast 比对相等后用裸指针转换,和 std::error::Error::downcast 的做法一样(源码注释原话「Taken from std::error::Error::downcast()」)。TypeId 要求 'static,这就是 with_upgrades 要 I: 'static 的原因。类型必须和交给 serve_connection 的完全一致:传的是 TokioIo<TcpStream>,就要 downcast 成 TokioIo<TcpStream>,不是 TcpStream。失败时 Upgraded 原样还回来,pre 里的字节也还在。
拆出来的 Parts 有 io 和 read_buf 两个字段(#[non_exhaustive],以后可能加字段)。read_buf 里就是那些多读的字节,绕过 Rewind 直接用 io 时,要自己先处理它们。
问: 服务返回 101 的那一刻,升级就完成了吗?为什么还要另起一个任务去 await OnUpgrade?
答: 没有。服务返回的只是一个 Response,它还要经过 Dispatcher 编码、写进缓冲区、刷到 socket,然后读写两端关闭,Dispatcher 才退出,IO 才空出来。这些都发生在连接的任务里,而且发生在服务的 future 完成之后。如果在服务的 future 里直接 await OnUpgrade,就成了服务等连接交出 IO,连接等服务给出响应,互相等。所以 upgrade 模块文档的说法是:调用 on(),然后「can spawn a task awaiting it」。
实验:三种连接,同一个服务
服务对每个请求都 spawn 一个任务等 upgrade::on;路径是 /up 时回 101。第 1 条连接用 with_upgrades(),客户端把升级请求和 hello 一次写过去,收到之后再发 world;第 2 条连接不用 with_upgrades();第 3 条连接发一个普通请求:
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::Empty;
use hyper::{body::{Bytes, Incoming}, server::conn::http1, service::service_fn, upgrade, Request, Response, StatusCode};
use hyper_util::rt::TokioIo;
use tokio::{io::{AsyncReadExt, AsyncWriteExt}, net::{TcpListener, TcpStream}, time::timeout};
static START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
fn ms() -> u128 {
START.get().unwrap().elapsed().as_millis()
}
async fn handle(mut req: Request<Incoming>) -> Result<Response<Empty<Bytes>>, Infallible> {
let path = req.uri().path().to_string();
let is_up = path == "/up";
let on = upgrade::on(&mut req);
tokio::spawn(async move {
match on.await {
Err(e) => println!("{:>4}ms server {path}: on_upgrade error: {e}", ms()),
Ok(up) => match up.downcast::<TokioIo<TcpStream>>() {
Err(_) => println!("{:>4}ms server {path}: downcast failed", ms()),
Ok(parts) => {
println!("{:>4}ms server {path}: upgraded, read_buf = {:?}", ms(), parts.read_buf);
let mut io = parts.io.into_inner();
io.write_all(&parts.read_buf).await.unwrap(); // echo what hyper had buffered
let mut buf = [0u8; 64];
let n = io.read(&mut buf).await.unwrap();
io.write_all(&buf[..n]).await.unwrap(); // then echo from the raw socket
}
},
}
});
let mut rsp = Response::new(Empty::new());
if is_up {
*rsp.status_mut() = StatusCode::SWITCHING_PROTOCOLS;
rsp.headers_mut().insert("upgrade", "echo".parse().unwrap());
rsp.headers_mut().insert("connection", "upgrade".parse().unwrap());
}
Ok(rsp)
}
async fn client(addr: std::net::SocketAddr, name: &str, steps: &[&str]) {
println!("== {name}");
let mut s = TcpStream::connect(addr).await.unwrap();
let mut buf = [0u8; 1024];
for step in steps {
s.write_all(step.as_bytes()).await.unwrap();
loop {
match timeout(Duration::from_millis(100), s.read(&mut buf)).await {
Ok(Ok(0)) => {
println!("{:>4}ms client: EOF", ms());
return;
}
Ok(Ok(n)) => {
let text = String::from_utf8_lossy(&buf[..n]).replace("\r\n", "\\r\\n");
println!("{:>4}ms client: got {text}", ms());
}
Ok(Err(e)) => panic!("{e}"),
Err(_) => break, // quiet for 100ms: next step
}
}
}
drop(s);
tokio::time::sleep(Duration::from_millis(50)).await;
}
#[tokio::main]
async fn main() {
START.set(Instant::now()).unwrap();
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 mut b = http1::Builder::new();
b.auto_date_header(false);
let conn = b.serve_connection(TokioIo::new(stream), service_fn(handle));
tokio::spawn(async move {
// connection 2 is served without with_upgrades()
let r = if i == 2 { conn.await } else { conn.with_upgrades().await };
println!("{:>4}ms server: connection {i} ended {r:?}", ms());
});
}
});
let up = "GET /up HTTP/1.1\r\nHost: x\r\nUpgrade: echo\r\nConnection: upgrade\r\n\r\nhello";
client(addr, "1: with_upgrades, 101", &[up, "world"]).await;
client(addr, "2: without with_upgrades, 101", &[up]).await;
client(addr, "3: no Upgrade header", &["GET /plain HTTP/1.1\r\nHost: x\r\n\r\n"]).await;
}
M2 Max、rustc 1.99.0、--release。连跑 4 次,顺序相同,毫秒数差 2ms 以内:
== 1: with_upgrades, 101
0ms server: connection 1 ended Ok(())
0ms server /up: upgraded, read_buf = b"hello"
0ms client: got HTTP/1.1 101 Switching Protocols\r\nupgrade: echo\r\nconnection: upgrade\r\n\r\n
0ms client: got hello
104ms client: got world
104ms client: EOF
== 2: without with_upgrades, 101
105ms server: connection 2 ended Ok(())
105ms server /up: on_upgrade error: upgrade expected but low level API in use
105ms client: got HTTP/1.1 101 Switching Protocols\r\nupgrade: echo\r\nconnection: upgrade\r\n\r\n
105ms client: EOF
== 3: no Upgrade header
105ms server /plain: on_upgrade error: no upgrade available
105ms client: got HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n
207ms server: connection 3 ended Ok(())
- 第 1 条:
hello和请求头在同一次写里到达,被 hyper 读进了缓冲区,downcast后出现在read_buf里。UpgradeableConnection在交出 IO 的同时就以Ok(())结束了,比客户端收到101打印得还早。101响应里没有Content-Length,只有服务设置的两个头(实验关掉了Date)。之后的world由升级任务直接从 socket 读、再写回。升级任务结束、drop 掉 socket,客户端看到 EOF。 - 第 2 条:
101照样写出去了,但Connection拿到Upgrade后只调manual(),升级任务拿到错误,IO 随Connection一起被 drop,客户端在101之后立刻看到 EOF。客户端以为升级成功了,服务端却已经把连接扔了,这个错误只在服务端的日志里。 - 第 3 条:普通请求的 extensions 里没有
OnUpgrade,on立即返回no upgrade available。连接照常 keep-alive,客户端 drop 之后才结束。
HTTP/2 也能升级:HTTP/2 服务端对所有 CONNECT 请求(包括扩展 CONNECT)同样放一个 OnUpgrade,Upgraded 里装的是一个把 h2 流包装成 IO 的类型,downcast 成 TCP 类型会失败。见 HTTP/2 服务端。
服务和上面的 handle 一样,连接用 with_upgrades()。客户端发一个带 Upgrade: echo 的请求,路径是 /no-101,服务回 200;然后在同一条连接上发一个普通请求,再关闭连接:
// handle() and client() are the same as above
#[tokio::main]
async fn main() {
START.set(Instant::now()).unwrap();
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let mut b = http1::Builder::new();
b.auto_date_header(false);
let r = b.serve_connection(TokioIo::new(stream), service_fn(handle)).with_upgrades().await;
println!("{:>4}ms server: connection ended {r:?}", ms());
});
client(
addr,
"Upgrade header, answered 200, then another request",
&[
"GET /no-101 HTTP/1.1\r\nHost: x\r\nUpgrade: echo\r\nConnection: upgrade\r\n\r\n",
"GET /plain HTTP/1.1\r\nHost: x\r\n\r\n",
],
)
.await;
}
为 /no-101 spawn 的那个任务,on.await 什么时候返回、返回什么?答案在本页末尾。
动脑筋答案
M2 Max、rustc 1.99.0、--release。连跑 5 次,顺序相同,毫秒数差 2ms 以内:
== Upgrade header, answered 200, then another request
0ms client: got HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n
103ms server /plain: on_upgrade error: no upgrade available
103ms client: got HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n
206ms server: connection ended Ok(())
206ms server /no-101: upgraded, read_buf = b""
/no-101 的 on.await 一直等到连接结束(206ms),返回的是 Ok(Upgraded),read_buf 为空,里面的 socket 对端已经关了。
- 回
200时is_last不为真,连接照常 keep-alive,读写回到Init。Conn里挂着的Pending没有人清:源码里只有prepare_upgrade写它、pending_upgrade在连接结束时取它。 - 第二个请求没有
Upgrade头,不会调prepare_upgrade,旧的Pending继续挂着;这个请求的on拿到no upgrade available。 - 客户端关闭后,服务端在空闲状态读到 EOF,读写两端关闭,
is_done为真。poll_inner先查pending_upgrade(),取到了那个旧的Pending,于是返回Dispatched::Upgrade,UpgradeableConnection把一个已经结束的连接当作升级结果交了出去。
所以 on.await 返回 Ok 不能说明升级真的发生了。判断依据应该是自己写出的响应:只在回了 101 的那条路径上等 OnUpgrade,或者拿到 Upgraded 后先确认这是一个回了 101 的请求。
小结
CONNECT或带Upgrade头的 HTTP/1.1 请求会被标成Wants::UPGRADE;Dispatcher建一对oneshot,Pending留在Conn,OnUpgrade放进请求的 extensions,upgrade::on把它remove出来。- 回
101(或CONNECT的 2xx)让写端在头写完后Closed,刷新成功后读写都关,Dispatcher返回Dispatched::Upgrade,不关 IO。 - 只有
with_upgrades()的UpgradeableConnection会把 IO 和读缓冲交出去(要求I: Send + 'static);普通Connection调manual(),等待方得到upgrade expected but low level API in use,而101已经发给了客户端。 Upgraded是Rewind<Box<dyn Io + Send>>:Rewind先吐出 HTTP 解析时多读的字节,trait object 擦掉 IO 类型;downcast按TypeId拆回原类型,多读的字节在Parts::read_buf里,要自己处理。OnUpgrade要在服务的 future 之外等(另起任务),因为 IO 要等响应写完才会交出。挂着的升级状态不会因为回了非101而清除,连接结束时仍会被当成升级交出去。