如何在 Rust 中 Hyper 的异步闭包内从外部范围正确读取字符串值

How to correctly read a string value from an outer scope within an async closure for Hyper in Rust

提问人:Tim Perry 提问时间:5/20/2023 更新时间:5/24/2023 访问量:169

问:

我正在尝试学习 Rust,并尝试编写一些非常简单的 Web 服务器代码来做到这一点。

我以为我对生命周期和简单代码借用的基础知识有一个很好的了解,但我发现要么我在某个地方缺少一个基本的技术,要么我认为是一个简单的案例实际上由于某种原因要复杂得多。

我本质上想做的是:

use std::env;
use std::convert::Infallible;
use std::net::SocketAddr;
use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};

// A demo web server: takes a message on the command-line, then
// serves it back to incoming requests.

#[tokio::main]
pub async fn main() {
    let args: Vec<String> = env::args().collect();
    let message = format!("Arguments were: {:?}", &args[1..]);
    serve_message(message).await;
}

pub async fn serve_message(message: String) {
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    let make_svc = make_service_fn(|_conn| {
        async move {
            Ok::<_, Infallible>(service_fn(move |_: Request<Body>| async move {
                Ok::<_, Infallible>(
                    Response::new(Body::from(message))
                )
            }))
        }
    });

    let server = Server::bind(&addr).serve(make_svc);

    if let Err(e) = server.await {
        eprintln!("server error: {}", e);
    }
}

这无法编译:

error[E0507]: cannot move out of `message`, a captured variable in an `FnMut` closure
  --> src/main.rs:22:68
   |
17 |   pub async fn serve_message(message: String) {
   |                              ------- captured outer variable
...
22 |               Ok::<_, Infallible>(service_fn(move |_: Request<Body>| async move {
   |  ____________________________________________-----------------------_^
   | |                                            |
   | |                                            captured by this `FnMut` closure
23 | |                 Ok::<_, Infallible>(
24 | |                     Response::new(Body::from(message))
   | |                                              -------
   | |                                              |
   | |                                              variable moved due to use in generator
   | |                                              move occurs because `message` has type `String`, which does not implement the `Copy` trait
25 | |                 )
26 | |             }))
   | |_____________^ move out of `message` occurs here

error[E0507]: cannot move out of `message`, a captured variable in an `FnMut` closure
  --> src/main.rs:21:9
   |
17 |   pub async fn serve_message(message: String) {
   |                              ------- captured outer variable
...
20 |       let make_svc = make_service_fn(|_conn| {
   |                                      ------- captured by this `FnMut` closure
21 | /         async move {
22 | |             Ok::<_, Infallible>(service_fn(move |_: Request<Body>| async move {
23 | |                 Ok::<_, Infallible>(
24 | |                     Response::new(Body::from(message))
   | |                                              -------
   | |                                              |
   | |                                              variable moved due to use in generator
   | |                                              move occurs because `message` has type `String`, which does not implement the `Copy` trait
25 | |                 )
26 | |             }))
27 | |         }
   | |_________^ move out of `message` occurs here

我已经尝试了各种更复杂的修改,包括克隆、ARC、状态到带有句柄 impl 的结构体以及许多其他方法,但我很挣扎,每一种似乎都让我回到了上面相同的基本问题。我显然错过了一些关于异步、闭包和所有权如何交互以及管理它的工具的重要信息。我已经看过如何在 Rust 的闭包内重用外部作用域的值? 这是相似的,但唯一的答案的例子是一个更简单的演示,并没有清楚地转化为更大的问题 - 只是按照建议添加到处似乎不足以应对这种情况。.clone()

我发现最令人困惑的部分是,这与 Hyper 自己的一个示例非常相似:https://docs.rs/hyper/latest/hyper/service/fn.make_service_fn.html#example。但这个例子似乎没有遇到任何问题,而这确实如此。

这样做的正确和惯用方法是什么,为什么它有效,这个和那个 Hyper 示例案例有什么区别?非常感谢初学者级别的解释。

异步 闭包 Rust-Tokio Hyper

评论

0赞 Chayim Friedman 5/21/2023
克隆/应该有效。你是怎么尝试的?Arc
0赞 Tim Perry 5/22/2023
@ChayimFriedman,好吧,这是一个有用的线索!不过,我已经尝试了 10 种不同的方法,我只是非常超出我的深度,不确定如何使用它们。我能找到的所有例子都是微不足道的,对我来说显然不会映射到像这样稍微棘手的情况。您能否分享一个示例,说明您如何在这种情况下使用它们,并解释原因?
0赞 Chayim Friedman 5/22/2023
我希望你能分享你的尝试,然后我会解释出了什么问题。

答:

1赞 VonC 5/23/2023 #1

出现此错误的原因是字符串被移动到您传递给 的闭包中。在 Rust 中,每个值都有一个唯一的所有者,移动一个值会转移其所有权。一旦值被移动,就不能再从原始位置使用它。请参阅“所有权和移动messageservice_fn"

但是,在您的情况下,您希望在多个响应中使用该字符串,这意味着您需要在多个闭包之间共享它。这就是Arc(原子参考计数)派上用场的地方。
An 是一个线程安全的引用计数指针,它允许对 类型的值进行共享读取访问。可以克隆它以创建指向相同值的新指针,从而增加引用计数。
messageArc<T>T

您可以更新函数以将 包装在 an 中,然后为每个请求克隆它,如下所示(playground)serve_messagemessageArc

use bytes::Bytes;
use hyper::{
    service::{make_service_fn, service_fn},
    Body, Error as HyperError, Request, Response, Server,
};
use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::Arc;

pub async fn serve_message(message: String) {
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    let message = Arc::new(Bytes::from(message));

    let make_svc = make_service_fn(move |_conn| {
        let message = Arc::clone(&message);
        async {
            Ok::<_, Infallible>(service_fn(move |_: Request<Body>| {
                let message = Bytes::copy_from_slice(&*Arc::clone(&message));
                async move { Ok::<_, HyperError>(Response::new(Body::from(message))) }
            }))
        }
    });

    let server = Server::bind(&addr).serve(make_svc);

    if let Err(e) = server.await {
        eprintln!("server error: {}", e);
    }
}

它的工作方式是:

  • Arc::new(message)创建一个拥有 .这是唯一直接拥有 .ArcmessageArcmessage
  • 每次调用时,它都会克隆 (而不是 本身),这会增加引用计数,但不会移动 .make_service_fnArcmessagemessage
  • 在里面,我们再次克隆每个请求。这允许每个请求在不获取所有权的情况下引用 。service_fnArcmessage
  • 最后,再克隆一次以在响应中使用。这不会移动 ,并允许在后续响应中使用。Response::new(Body::from((*Arc::clone(&message)).clone()))Arcmessagemessage

关于最后一点:

代码也可以工作,但要记住一个关键的区别。Response::new(Body::from(Arc::clone(&message).to_string()))

使用 时,您将为每个请求创建一个新的请求。如果请求很大或请求很多,这可能效率低下,因为它涉及每次为新内存分配内存。Arc::clone(&message).to_string()StringmessageString

相反,是 类型 。let message = Bytes::copy_from_slice(&*Arc::clone(&message))Bytes

在此代码中,我们用于从每个请求的共享创建一个新实例。
这比直接共享效率低(正如我们理想情况下希望的那样),但它避免了您之前遇到的借用错误。
Bytes::copy_from_slice(&*Arc::clone(&message))BytesBytesBytes

细分如下:

  • Arc::clone(&message)创建一个指向与指向的相同值的新值。的类型是 。ArcBytesmessageArc::clone(&message)Arc<Bytes>
  • &*Arc::clone(&message)取消引用以获取对它所指向的值的引用。的类型是 。Arc<Bytes>Bytes&*Arc::clone(&message)&Bytes
  • Bytes::copy_from_slice(&*Arc::clone(&message))创建一个新值,该值包含与该值指向的字节序列相同的字节序列。的类型是 。BytesBytesmessageBytes::copy_from_slice(&*Arc::clone(&message))Bytes

该函数需要对字节切片 () 的引用,并且可以用作 because 实现 .Bytes::copy_from_slice&[u8]&Bytes&[u8]BytesDeref<Target=[u8]>

因此,总体效果是创建一个新值,其中包含原始值的字节副本,从而允许它在响应正文中独立使用。Bytes::copy_from_slice(&*Arc::clone(&message))BytesBytes


取消引用以获取引用 () 不起作用,因为 Body::from 不接受引用作为参数。
虽然 Rust 的特征通常适用于引用,但在这种情况下,该特征仅针对拥有的类型实现,而不是针对 .
Arc<Bytes>Bytes&Bytes&BytesFromFromBytesBytes

  • Body::from消耗其参数。它获取所提供值的所有权。
  • 当你取消引用时,你会得到一个(对 的引用),而不是一个拥有的。Arc<Bytes>&BytesBytesBytes
  • A 与 不一样。它们是不同的类型。前者是对价值的引用,后者是对自有价值的引用。&BytesBytesBytesBytes
  • 因为没有实现 ,所以不能将 传递给 。Body::from&Bytes&BytesBody::from

您链接的 Hyper 示例创建了一个 HTTP 响应,其类型为 ,该响应不涉及从外部作用域的任何借用。hyper::Body::empty()

在代码中,您尝试在服务函数中使用外部作用域的 (),这会导致您遇到的借用问题。Stringmessage

您的用例与 Hyper 示例不同,因为您希望在多个闭包之间共享一个拥有的,这需要如上所述使用 。允许多个闭包对相同的闭包进行只读引用,而无需拥有它的所有权。StringArcArcString

评论

0赞 Tim Perry 5/23/2023
呸,谢谢你,这非常有帮助!这里仍然有一个小错误,因为最里面需要一个 ,但除此之外,这一切都是有道理的,它现在👍终于起作用了。Arc::clone(&message).to_string()
0赞 VonC 5/23/2023
@TimPerry 但我认为 Hyper 中的函数可以接受任何可以被视为字节切片的东西,并满足该要求,因为 String 本身实现了 .因此,当您这样做时,它应该可以工作,因为它可以将 视为字节切片。Body::from(&[u8])Arc<String>AsRef<[u8]>Body::from(Arc::clone(&message))Arc<String>
0赞 Tim Perry 5/23/2023
不知道我害怕!就我而言,除非我在那里使用,否则我会得到。使用最新的 Hyper 版本 - 0.14.26。the trait From<Arc<String>> is not implemented for BodyArc::clone(&message).to_string()
1赞 VonC 5/23/2023
@TimPerry好的,我已经更新了代码,但使用 Byte 而不是 to_string(),以避免为每个请求创建一个新的。String
0赞 Tim Perry 5/23/2023
这种更改听起来很合理,但代码仍然存在相同的问题:.对于字节 1 + Hyper 0.14.26。the trait From<Arc<bytes::Bytes>> is not implemented for Body