你好,Hyper!
对于那些不熟悉的人来说,Hyper是Rust的一个HTTP实现,建立在Tokio之上。它是一个低级别的库,为Warp和Rocket等框架以及reqwest客户端库提供动力。对于大多数人来说,大多数时候,使用像这样的高层次包装器是正确的事情。
但有时我们喜欢弄脏自己的手,有时直接用Hyper工作是正确的选择。而且从学习的角度来看,肯定是值得这样做的,至少是一次。还有什么比遵循Hyper主页上的例子更容易呢?要做到这一点,cargo new 一个新的项目,添加以下依赖项。
hyper = { version = "0.14", features = ["full"] }
tokio = { version = "1", features = ["full"] }
并将以下内容添加到main.rs 。
use std::convert::Infallible;
use std::net::SocketAddr;
use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};
async fn hello_world(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
Ok(Response::new("Hello, World".into()))
}
#[tokio::main]
async fn main() {
// We'll bind to 127.0.0.1:3000
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
// A `Service` is needed for every connection, so this
// creates one from our `hello_world` function.
let make_svc = make_service_fn(|_conn| async {
// service_fn converts our function into a `Service`
Ok::<_, Infallible>(service_fn(hello_world))
});
let server = Server::bind(&addr).serve(make_svc);
// Run this server for... forever!
if let Err(e) = server.await {
eprintln!("server error: {}", e);
}
}
如果你有兴趣,在Hyper的网站上有一个关于这个代码的快速解释。但我们的重点将是对这段代码做一个永远微不足道的修改。开始吧!
计数器
还记得Geocities网站的好日子吗,每个页面都必须有一个访客计数器?我也想这样。让我们修改我们的hello_world 函数来做这件事。
use std::sync::{Arc, Mutex};
type Counter = Arc<Mutex<usize>>; // Bonus points: use an AtomicUsize instead
async fn hello_world(counter: Counter, _req: Request<Body>) -> Result<Response<Body>, Infallible> {
let mut guard = counter.lock().unwrap(); // unwrap poisoned Mutexes
*guard += 1;
let message = format!("You are visitor number {}", guard);
Ok(Response::new(message.into()))
}
这很容易,现在我们已经完成了hello_world 。唯一的问题是重写main ,向它传递一个Counter 的值。让我们在这个问题上做第一次天真的尝试。
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let counter: Counter = Arc::new(Mutex::new(0));
let make_svc = make_service_fn(|_conn| async {
Ok::<_, Infallible>(service_fn(|req| hello_world(counter, req)))
});
let server = Server::bind(&addr).serve(make_svc);
if let Err(e) = server.await {
eprintln!("server error: {}", e);
}
不幸的是,由于移出了捕获的变量,这个方法失败了。(这是我们在闭合训练模块中详细介绍的一个主题)。
error[E0507]: cannot move out of `counter`, a captured variable in an `FnMut` closure
--> src\main.rs:21:58
|
18 | let counter: Counter = Arc::new(Mutex::new(0));
| ------- captured outer variable
...
21 | Ok::<_, Infallible>(service_fn(|req| hello_world(counter, req)))
| ^^^^^^^ move occurs because `counter` has type `Arc<std::sync::Mutex<usize>>`, which does not implement the `Copy` trait
error[E0507]: cannot move out of `counter`, a captured variable in an `FnMut` closure
--> src\main.rs:20:50
|
18 | let counter: Counter = Arc::new(Mutex::new(0));
| ------- captured outer variable
19 |
20 | let make_svc = make_service_fn(|_conn| async {
| __________________________________________________^
21 | | Ok::<_, Infallible>(service_fn(|req| hello_world(counter, req)))
| | -------------------------------
| | |
| | move occurs because `counter` has type `Arc<std::sync::Mutex<usize>>`, which does not implement the `Copy` trait
| | move occurs due to use in generator
22 | | });
| |_____^ move out of `counter` occurs here
克隆
这个错误并不太令人惊讶。我们把我们的Mutex 放在一个Arc 里面是有原因的:我们需要对它进行多次克隆,并把这些克隆传递给每个新的请求处理程序。但是我们还没有调用过一次clone!再一次,让我们做最天真的事情,改变一下。
Ok::<_, Infallible>(service_fn(|req| hello_world(counter, req)))
到
Ok::<_, Infallible>(service_fn(|req| hello_world(counter.clone(), req)))
这就是错误信息开始变得更有趣的地方。
error[E0597]: `counter` does not live long enough
--> src\main.rs:21:58
|
20 | let make_svc = make_service_fn(|_conn| async {
| ____________________________________-------_-
| | |
| | value captured here
21 | | Ok::<_, Infallible>(service_fn(|req| hello_world(counter.clone(), req)))
| | ^^^^^^^ borrowed value does not live long enough
22 | | });
| |_____- returning this value requires that `counter` is borrowed for `'static`
...
29 | }
| - `counter` dropped here while still borrowed
async 块和闭包在默认情况下都会通过引用从环境中获取变量,而不是获取所有权。我们的闭包需要有一个'static 的生命周期,因此不能在我们的main 函数中保持对数据的引用。
move 万事俱备!
对此的标准解决方案是简单地将moves洒在每个async 块和闭包上。这将迫使每个闭包拥有Arc 本身,而不是对它的引用。这样做看起来很简单。
let make_svc = make_service_fn(move |_conn| async move {
Ok::<_, Infallible>(service_fn(move |req| hello_world(counter.clone(), req)))
});
事实上,这确实解决了上面的错误。但它却给了我们一个新的错误。
error[E0507]: cannot move out of `counter`, a captured variable in an `FnMut` closure
--> src\main.rs:20:60
|
18 | let counter: Counter = Arc::new(Mutex::new(0));
| ------- captured outer variable
19 |
20 | let make_svc = make_service_fn(move |_conn| async move {
| ____________________________________________________________^
21 | | Ok::<_, Infallible>(service_fn(move |req| hello_world(counter.clone(), req)))
| | --------------------------------------------
| | |
| | move occurs because `counter` has type `Arc<std::sync::Mutex<usize>>`, which does not implement the `Copy` trait
| | move occurs due to use in generator
22 | | });
| |_____^ move out of `counter` occurs here
双倍的闭包,双倍的克隆!
好吧,即使是这个错误也是很有意义的。让我们更好地理解我们的代码在做什么:
- 创建一个闭包以传递给
make_service_fn,该闭包将为每一个新进入的连接被调用 - 在这个闭包中,创建一个新的闭包传递给
service_fn,这个闭包将为现有连接上的每一个新传入的请求被调用。
这就是直接使用Hyper工作的技巧所在。每一层闭包都需要拥有自己的Arc 的克隆。在我们上面的代码中,我们试图将Arc 从外层闭包的捕获变量转移到内层闭包的捕获变量。如果你仔细观察,这就是上面的错误信息所说的。我们的外层闭包是一个FnMut ,它必须可以被多次调用。因此,我们不能从它的捕获变量中移出。
看起来这应该是一个很容易解决的问题:只要再clone!
let make_svc = make_service_fn(move |_conn| async move {
let counter_clone = counter.clone();
Ok::<_, Infallible>(service_fn(move |req| hello_world(counter_clone.clone(), req)))
});
这就是我们遇到的真正的难题:我们得到了几乎完全相同的错误信息。
error[E0507]: cannot move out of `counter`, a captured variable in an `FnMut` closure
--> src\main.rs:20:60
|
18 | let counter: Counter = Arc::new(Mutex::new(0));
| ------- captured outer variable
19 |
20 | let make_svc = make_service_fn(move |_conn| async move {
| ____________________________________________________________^
21 | | let counter_clone = counter.clone();
| | -------
| | |
| | move occurs because `counter` has type `Arc<std::sync::Mutex<usize>>`, which does not implement the `Copy` trait
| | move occurs due to use in generator
22 | | Ok::<_, Infallible>(service_fn(move |req| hello_world(counter_clone.clone(), req)))
23 | | });
| |_____^ move out of `counter` occurs here
范式的转变
我们需要做的是稍微重写我们的代码,以揭示问题所在。让我们添加一堆不必要的大括号。我们将上面的代码。
let make_svc = make_service_fn(move |_conn| async move {
let counter_clone = counter.clone();
Ok::<_, Infallible>(service_fn(move |req| hello_world(counter_clone.clone(), req)))
});
变成这个语义相同的代码:
let make_svc = make_service_fn(move |_conn| { // outer closure
async move { // async block
let counter_clone = counter.clone();
Ok::<_, Infallible>(service_fn(move |req| { // inner closure
hello_world(counter_clone.clone(), req)
}))
}
});
错误信息基本上是相同的,只是来源位置略有不同。但现在我可以更正确地走完counter 的所有权。我在上面的代码中添加了注释,以强调三个不同的实体,它们可以通过某种环境获得值的所有权。
- 外部闭包,它处理每个连接
- 一个
async块,它构成了外层闭包的主体 - 内部闭包,负责处理每个请求
在最初的代码结构中,我们把move |_conn| async move 挨在一起,至少对我来说,这掩盖了一个事实,即闭包和async 块是两个完全独立的实体。有了这个改变,让我们来跟踪counter 的所有权:
- 我们在
main函数中创建了Arc;它被counter变量所拥有。 - 我们将
Arc从main函数的counter变量移到外层闭包的捕获变量中。 - 我们将
counter变量从外层闭包中移出,并进入async块的捕获变量中。 - 在
async块的主体中,我们创建了一个counter的克隆,称为counter_clone。这并没有从async块中移出,因为clone方法只需要对Arc的引用。 - 我们将
Arc从counter_clone变量中移出,并进入内部闭包。 - 在内部闭包的主体中,我们克隆了
Arc(正如(4)中所解释的,它不会移动),并将其传递到hello_world函数中。
根据这个分解,你能看出问题出在哪里吗?是在步骤(3)。我们不想从外层闭包的捕获变量中移动出来。我们试图通过克隆counter 来避免这种移动。但我们克隆得太晚了!通过在async move 块内使用counter ,我们迫使编译器进行移动。万岁,我们已经发现了问题所在
非解决方法:不移动async
看起来我们在上面的 "洒上move"的尝试简直是好高骛远了。问题是,async 块正在夺取counter 的所有权。让我们尝试简单地删除那里的move 关键字。
let make_svc = make_service_fn(move |_conn| {
async {
let counter_clone = counter.clone();
Ok::<_, Infallible>(service_fn(move |req| {
hello_world(counter_clone.clone(), req)
}))
}
});
不幸的是,这并不是一个解决方案。
error: captured variable cannot escape `FnMut` closure body
--> src\main.rs:21:9
|
18 | let counter: Counter = Arc::new(Mutex::new(0));
| ------- variable defined here
19 |
20 | let make_svc = make_service_fn(move |_conn| {
| - inferred to be a `FnMut` closure
21 | / async {
22 | | let counter_clone = counter.clone();
| | ------- variable captured here
23 | | Ok::<_, Infallible>(service_fn(move |req| {
24 | | hello_world(counter_clone.clone(), req)
25 | | }))
26 | | }
| |_________^ returns an `async` block that contains a reference to a captured variable, which then escapes the closure body
|
= note: `FnMut` closures only have access to their captured variables while they are executing...
= note: ...therefore, they cannot allow references to captured variables to escape
这里的问题是,外层闭包将返回由async 块生成的Future 。而如果async 块没有move ,counter ,它就会持有对外层闭包捕获变量的引用。而这是不允许的。
真正的解决方案:尽早克隆,经常克隆
好吧,撤销async move 到async 的转换,这是一个死胡同。事实证明,我们所要做的就是在启动async move 块之前克隆counter ,就像这样。
let make_svc = make_service_fn(move |_conn| {
let counter_clone = counter.clone(); // this moved one line earlier
async move {
Ok::<_, Infallible>(service_fn(move |req| {
hello_world(counter_clone.clone(), req)
}))
}
});
现在,我们在外层闭包中创建一个临时的counter_clone 。这是以引用的方式工作,因此不会移动任何东西。然后我们通过捕获将新的、临时的counter_clone 移到async move 块中,并从那里将其移到内部闭包中。这样,我们所有的闭包捕获的变量都没有被移动,因此,FnMut 的要求得到了满足。
就这样,我们终于可以享受Geocities访客计数器的光辉岁月了。
异步闭包
rustfmt 推荐的格式掩盖了这样一个事实,即在外部闭包和async block 之间有两个不同的环境在起作用,通过将两者移到move |_conn| async move 的一行中。这让人感觉这两个实体在某种程度上是一体的。但正如我们所展示的,它们并不是。
理论上这可以通过一个异步闭包来解决。nightly-2021-03-02我在#![feature(async_closure)] 上进行了测试,但没有找到使用异步闭包来解决这个问题的方法,与我上面的解决方法不同。但这可能是我自己对async_closure 的不熟悉。
目前,主要的收获是,闭包和async 块是两个不同的实体,各自有各自的环境。