Rust 中的范型编程-Exonum 是如何从 Iron 转移到 Actix-web

2018-11-23 11:07:06 +08:00
 krircc

本文同步与Rust 中文社区 本文时间:2018-11-23,作者:krircc, 简介:天青色

欢迎向 Rust 中文社区投稿,投稿地址,好文将在以下地方直接展示

  1. Rust 中文社区首页
  2. Rust 中文社区[Rust 文章栏目]( https://rustlang-cn.org/read/rust/2018/Rust 中的范型编程-Exonum 是如何从 Iron 转移到 Actix-web.html)
  3. 知乎专栏Rust 语言

Rust 生态系统仍在增长。因此,具有改进功能的新库经常被发布到开发人员社区中,而较旧的库变得过时。当我们最初设计 Exonum 时,我们使用了 Iron Web 框架。我们现在决定将Exonum 平台转移到 actix-web 框架。

在本文中,我们将介绍如何使用泛型编程将 Exonum 框架移植到actix-web

Exonum on Iron

在 Exonum 平台中,Iron 框架在没有任何抽象的情况下使用。我们为某些资源安装了 handlers,并通过使用辅助方法解析 URL 来获取请求参数; 结果只是以字符串的形式返回。

另外,我们以 CORS 头的形式使用了一些中间件插件。我们使用 mount 将所有处理程序合并到一个 API 中。

我们决定摆脱 Iron

Iron是一个很好的库,有很多插件。然而,它是在futuretokio等项目不存在的日子写的。

Iron 的体系结构涉及同步请求处理,这很容易受到大量同时打开的连接(并发)的影响。为了实现可扩展性,Iron 需要变为异步,这将涉及重新思考和重写整个框架。结果,我们看到软件工程师逐渐不使用 Iron。

为什么我们选择 Actix-Web

Actix-web 是一个受欢迎的框架,在TechEmpower 基准测试中排名很高。它拥有一个活跃的开发人员社区,与 Iron 不同,它拥有精心设计的 API 和基于 actix actor 框架的高质量实现。线程池异步处理请求; 如果请求处理 panics,则 actor 会自动重启。

以前,人们担心 actix-web 包含许多不安全的代码。但是,当框架逐渐以安全的编程语言( Rust )重写时,不安全代码的数量显着减少。Bitfury 的工程师(Exonum 框架的工程师)自己对这些代码进行了审查,并对其长期稳定性充满信心。

对于 Exonum 框架,转向 actix 解决了操作稳定性问题。如果存在大量连接,则 Iron 框架可能会失败。我们还发现 actix-web API 更简单,更高效,更统一。我们相信,用户和开发人员可以更轻松地使用 Exonum 编程界面,由于采用了 actix-web 设计,现在可以更快地运行。

我们对 Web 框架的要求

在此过程中,我们意识到,不仅要简单地转换框架,而且要设计独立于任何特定 Web 框架的新 API 体系结构,这对我们非常重要。这种架构允许创建处理程序,几乎不关心 Web 细节,并将它们转移到任何后端。这个概念可以通过编写一个应用基本类型和trait的前端来实现。

要了解这个前端需要看起来像什么,让我们定义任何 HTTP API 的真正含义:

如果我们要分析所有抽象层,事实证明任何 HTTP 请求只是一个函数调用:

fn  request ( context:& ServiceContext,query:Query ) - > Result <Response,ServiceError>

其他一切都可以被视为这个基本实体的延伸。因此,为了独立于 Web 框架的特定实现,我们需要以类似于上面示例的样式编写处理程序。

用于 HTTP 请求的范型处理的 trait “ Endpoint ”

最简单直接的方法是声明Endpoint trait,它描述了特定请求的实现:

// A trait describing GET request handlers. It should be possible to call each of the handlers from any freed
// thread. This requirement imposes certain restrictions on the trait. Parameters and request results are
// configured using associated types.
trait Endpoint: Sync + Send + 'static {
    type Request: DeserializeOwned + 'static;
    type Response: Serialize + 'static;

    fn handle(&self, context: &Context, request: Self::Request) -> Result<Self::Response, io::Error>;
}

现在我们需要在特定的框架中实现这个处理程序。例如,在 actix-web 中,它看起来如下所示:

// Response type in actix-web. Note that they are asynchronous, even though `Endpoint` assumes that 
// processing is synchronous.
type FutureResponse = actix_web::FutureResponse<HttpResponse, actix_web::Error>;

// A raw request handler for actix-web. This is what the framework ultimately works with. The handler 
// receives parameters from an arbitrary context, through which the request parameters are passed.
type RawHandler = dyn Fn(HttpRequest<Context>) -> FutureResponse + 'static + Send + Sync;

// For convenience, let ’ s put everything we need from the handler into a single structure.
#[derive(Clone)]
struct RequestHandler {
    /// The name of the resource.
    pub name: String,
    /// HTTP method.
    pub method: actix_web::http::Method,
    /// The raw handler. Note that it will be used from multiple threads.
    pub inner: Arc<RawHandler>,
}

我们可以使用结构通过上下文传递请求参数。Actix-web 可以使用 serde 自动反序列化参数。例如,a = 15 & b = hello 被反序列化为如下结构:

#[derive(Deserialize)]
struct SimpleQuery {
    a: i32,
    b: String,
}

这种反序列化功能与来自“ Endpoint ” trait 的相关类型请求很好地吻合。

接下来,让我们设计一个适配器,它将特定的Endpoint实现包装到actix-webRequestHandler中。请注意,在执行此操作时,请求和响应类型的信息将消失。这种技术称为类型擦除 - 它将静态调度转换为动态调度。

impl RequestHandler {
    fn from_endpoint<E: Endpoint>(name: &str, endpoint: E) -> RequestHandler {
        let index = move |request: HttpRequest<Context>| -> FutureResponse {
            let context = request.state();
            let future = Query::from_request(&request, &())
                .map(|query: Query<E::Request>| query.into_inner())
                .and_then(|query| endpoint.handle(context, query).map_err(From::from))
                .and_then(|value| Ok(HttpResponse::Ok().json(value)))
                .into_future();
            Box::new(future)
        };

        Self {
            name: name.to_owned(),
            method: actix_web::http::Method::GET,
            inner: Arc::from(index) as Arc<RawHandler>,
        }
    }
}

在这个阶段,仅为 POST 请求添加处理程序就足够了,因为我们创建了一个独立于实现细节的 trait。 但是,我们发现这个解决方案还不够先进。

“ Endpoint ” trait 的缺点

编写处理程序时会生成大量辅助代码:

// A structure with the context of the handler.
struct ElementCountEndpoint {
    elements: Rc<RefCell<Vec<Something>>>,
}

// Implementation of the `Endpoint` trait.
impl Endpoint for ElementCountEndpoint {
    type Request = ();
    type Result = usize;

    fn handle(&self, context: &Context, _request: ()) -> Result<usize, io::Error> {
        Ok(self.elements.borrow().len())
    }
}

// Installation of the handler in the backend.
let endpoint = ElementCountEndpoint::new(elements.clone());
let handler = RequestHandler::from_endpoint("/v1/element_count", endpoint);
actix_backend.endpoint(handler);

理想情况下,我们需要能够将一个简单的闭包作为处理程序传递,从而显着减少语法噪声的数量。

let elements = elements.clone();
actix_backend.endpoint("/v1/elements_count", move || {
    Ok(elements.borrow().len())
});

下面我们将讨论如何做到这一点。

深入范型编程

我们需要添加自动生成适配器的功能,该适配器使用正确的关联类型实现Endpoint trait。 输入将仅包含具有 HTTP 请求处理程序的闭包。

参数和闭包的结果可以有不同的类型,因此我们必须在这里使用方法重载。Rust 不支持直接重载,但允许使用IntoFrom trait 进行模拟。

此外,闭包值的返回类型不必与Endpoint实现的返回值匹配。 要操纵此类型,必须从接收到的闭包的类型中提取它。

从“ Fn ” trait 中获取类型

在 Rust 中,每个闭包都有自己独特的类型,无法在程序中明确指出。 对于带闭包的操作,我们使用Fn trait。 trait 包含函数的签名以及参数类型和返回值,但是,分别检索这些元素并不容易。

主要思想是使用以下形式的辅助结构:

/// Simplified example of extracting types from an F closure: Fn(A) -> B.
struct SimpleExtractor<A, B, F>
{
    // The original function.
    inner: F,
    _a: PhantomData<A>,
    _b: PhantomData<B>,
}

我们必须使用 PhantomData,因为 Rust 要求所有范型参数都在结构的定义中指出。 但是,闭包或函数 F 本身的类型不是范型的(尽管它实现了范型的Fn trait )。 类型参数 A 和 B 不直接使用。

正是 Rust 类型系统的这种限制使得我们无法通过直接为闭包实现Endpoint trait 来应用更简单的策略:

impl<A, B, F> Endpoint for F where F: Fn(&Context, A) -> B {
    type Request = A;
    type Response = B;

    fn handle(&self, context: &Context, request: A) -> Result<B, io::Error> {
        // ...
    }
}

在上面的例子中,编译器返回一个错误:

error[E0207]: the type parameter `A` is not constrained by the impl trait, self type, or predicates
  --> src/main.rs:10:6
   |
10 | impl<A, B, F> Endpoint for F where F: Fn(&Context, A) -> B {
   |      ^ unconstrained type parameter

辅助结构 SimpleExtractor 可以描述“ From ”的转换。 这种转换允许我们保存任何函数并提取其参数的类型:

impl<A, B, F> From<F> for SimpleExtractor<A, B, F>
where
    F: Fn(&Context, A) -> B,
    A: DeserializeOwned,
    B: Serialize,
{
    fn from(inner: F) -> Self {
        SimpleExtractor {
            inner,
            _a: PhantomData,
            _b: PhantomData,
        }
    }
}

以下代码成功编译:

#[derive(Deserialize)]
struct Query {
    a: i32,
    b: String,
};

// Verification of the ordinary structure.
fn my_handler(_: &Context, q: Query) -> String {
    format!("{} has {} apples.", q.b, q.a)
}
let fn_extractor = SimpleExtractor::from(my_handler);

// Verification of the closure.
let c = 15;
let my_closure = |_: &Context, q: Query| -> String {
    format!("{} has {} apples, but Alice has {}", q.b, q.a, c)
};
let closure_extractor = SimpleExtractor::from(my_closure);

专业化和标记类型

现在我们有一个带有显式参数化参数类型的函数,可以使用它来代替Endpoint trait。 例如,我们可以轻松实现从SimpleExtractorRequestHandler的转换。 不过,这不是一个完整的解决方案。 我们需要以某种方式区分类型级别(以及同步和异步处理程序之间)的GETPOST请求的处理程序。 在此任务中,标记类型可以帮助我们。

首先,让我们重写SimpleExtractor,以便它可以区分同步和异步结果。 同时,我们将为每个案例实施“ From ” trait。 注意,可以针对范型结构的特定变体实现 trait。

/// Generic handler for HTTP-requests.
pub struct With<Q, I, R, F> {
    /// A specific handler function.
    pub handler: F,
    /// Structure type containing the parameters of the request.
    _query_type: PhantomData<Q>,
    /// Type of the request result.
    _item_type: PhantomData<I>,
    /// Type of the value returned by the handler.
    /// Note that this value can differ from the result of the request.
    _result_type: PhantomData<R>,
}

// Implementation of an ordinary synchronous returned value.
impl<Q, I, F> From<F> for With<Q, I, Result<I>, F>
where
    F: Fn(&ServiceApiState, Q) -> Result<I>,
{
    fn from(handler: F) -> Self {
        Self {
            handler,
            _query_type: PhantomData,
            _item_type: PhantomData,
            _result_type: PhantomData,
        }
    }
}

// Implementation of an asynchronous request handler.
impl<Q, I, F> From<F> for With<Q, I, FutureResult<I>, F>
where
    F: Fn(&ServiceApiState, Q) -> FutureResult<I>,
{
    fn from(handler: F) -> Self {
        Self {
            handler,
            _query_type: PhantomData,
            _item_type: PhantomData,
            _result_type: PhantomData,
        }
    }
}

现在我们需要声明将请求处理程序与其名称和类型组合在一起的结构:

#[derive(Debug)]
pub struct NamedWith<Q, I, R, F, K> {
    /// The name of the handler.
    pub name: String,
    /// The handler with the extracted types.
    pub inner: With<Q, I, R, F>,
    /// The type of the handler.
    _kind: PhantomData<K>,
}

接下来,我们声明几个将用作标记类型的空结构。 标记将允许我们为每个处理程序实现自己的代码,以将处理程序转换为先前描述的RequestHandler

/// A handler that does not change the state of the service. In HTTP, GET-requests correspond to this 
// handler.
pub struct Immutable;
/// A handler that changes the state of the service. In HTTP, POST, PUT, UPDATE and other similar 
//requests correspond to this handler, but for the current case POST will suffice.
pub struct Mutable;

现在我们可以为模板参数 R 和 K 的所有组合(处理程序的返回值和请求的类型)定义“ From ”特征的四种不同实现。

// Implementation of a synchronous handler of GET requests.
impl<Q, I, F> From<NamedWith<Q, I, Result<I>, F, Immutable>> for RequestHandler
where
    F: Fn(&ServiceApiState, Q) -> Result<I> + 'static + Send + Sync + Clone,
    Q: DeserializeOwned + 'static,
    I: Serialize + 'static,
{
    fn from(f: NamedWith<Q, I, Result<I>, F, Immutable>) -> Self {
        let handler = f.inner.handler;
        let index = move |request: HttpRequest| -> FutureResponse {
            let context = request.state();
            let future = Query::from_request(&request, &())
                .map(|query: Query<Q>| query.into_inner())
                .and_then(|query| handler(context, query).map_err(From::from))
                .and_then(|value| Ok(HttpResponse::Ok().json(value)))
                .into_future();
            Box::new(future)
        };

        Self {
            name: f.name,
            method: actix_web::http::Method::GET,
            inner: Arc::from(index) as Arc<RawHandler>,
        }
    }
}
// Implementation of a synchronous handler of POST requests.
impl<Q, I, F> From<NamedWith<Q, I, Result<I>, F, Mutable>> for RequestHandler
where
    F: Fn(&ServiceApiState, Q) -> Result<I> + 'static + Send + Sync + Clone,
    Q: DeserializeOwned + 'static,
    I: Serialize + 'static,
{
    fn from(f: NamedWith<Q, I, Result<I>, F, Mutable>) -> Self {
        let handler = f.inner.handler;
        let index = move |request: HttpRequest| -> FutureResponse {
            let handler = handler.clone();
            let context = request.state().clone();
            request
                .json()
                .from_err()
                .and_then(move |query: Q| {
                    handler(&context, query)
                        .map(|value| HttpResponse::Ok().json(value))
                        .map_err(From::from)
                })
                .responder()
        };

        Self {
            name: f.name,
            method: actix_web::http::Method::POST,
            inner: Arc::from(index) as Arc<RawHandler>,
        }
    }
}
// Implementation of an asynchronous handler of GET requests.
impl<Q, I, F> From<NamedWith<Q, I, FutureResult<I>, F, Immutable>> for RequestHandler
where
    F: Fn(&ServiceApiState, Q) -> FutureResult<I> + 'static + Clone + Send + Sync,
    Q: DeserializeOwned + 'static,
    I: Serialize + 'static,
{
    fn from(f: NamedWith<Q, I, FutureResult<I>, F, Immutable>) -> Self {
        let handler = f.inner.handler;
        let index = move |request: HttpRequest| -> FutureResponse {
            let context = request.state().clone();
            let handler = handler.clone();
            Query::from_request(&request, &())
                .map(move |query: Query<Q>| query.into_inner())
                .into_future()
                .and_then(move |query| handler(&context, query).map_err(From::from))
                .map(|value| HttpResponse::Ok().json(value))
                .responder()
        };

        Self {
            name: f.name,
            method: actix_web::http::Method::GET,
            inner: Arc::from(index) as Arc<RawHandler>,
        }
    }
}
// Implementation of an asynchronous handler of POST requests.
impl<Q, I, F> From<NamedWith<Q, I, FutureResult<I>, F, Mutable>> for RequestHandler
where
    F: Fn(&ServiceApiState, Q) -> FutureResult<I> + 'static + Clone + Send + Sync,
    Q: DeserializeOwned + 'static,
    I: Serialize + 'static,
{
    fn from(f: NamedWith<Q, I, FutureResult<I>, F, Mutable>) -> Self {
        let handler = f.inner.handler;
        let index = move |request: HttpRequest| -> FutureResponse {
            let handler = handler.clone();
            let context = request.state().clone();
            request
                .json()
                .from_err()
                .and_then(move |query: Q| {
                    handler(&context, query)
                        .map(|value| HttpResponse::Ok().json(value))
                        .map_err(From::from)
                })
                .responder()
        };

        Self {
            name: f.name,
            method: actix_web::http::Method::POST,
            inner: Arc::from(index) as Arc<RawHandler>,
        }
    }
}

处理后端

最后一步是设计一个可以接受闭包并将它们添加到相应后端的外观。 在给定的情况下,我们有一个后端--actix-web。 但是,幕墙背后还有可能进行额外的实施。 例如:Swagger规范的生成器。

pub struct ServiceApiScope {
    actix_backend: actix::ApiBuilder,
}

impl ServiceApiScope {
    /// This method adds an Immutable handler to all backends.
    pub fn endpoint<Q, I, R, F, E>(&mut self, name: &'static str, endpoint: E) -> &mut Self
    where
        // Here we list the typical restrictions which we have encountered earlier:
        Q: DeserializeOwned + 'static,
        I: Serialize + 'static,
        F: Fn(&ServiceApiState, Q) -> R + 'static + Clone,
        E: Into<With<Q, I, R, F>>,
        // Note that the list of restrictions includes the conversion from NamedWith into RequestHandler 
        // we have implemented earlier.
        RequestHandler: From<NamedWith<Q, I, R, F, Immutable>>,
    {
        self.actix_backend.endpoint(name, endpoint);
        self
    }

    /// A similar method for Mutable handlers.
    pub fn endpoint_mut<Q, I, R, F, E>(&mut self, name: &'static str, endpoint: E) -> &mut Self
    where
        Q: DeserializeOwned + 'static,
        I: Serialize + 'static,
        F: Fn(&ServiceApiState, Q) -> R + 'static + Clone,
        E: Into<With<Q, I, R, F>>,
        RequestHandler: From<NamedWith<Q, I, R, F, Mutable>>,
    {
        self.actix_backend.endpoint_mut(name, endpoint);
        self
    }
}

请注意请求参数的类型,请求结果的类型以及处理程序的同步 /异步是如何从其签名自动派生的。 此外,我们需要明确指定请求的名称和类型。

此方式的缺点

上述方法虽然非常有效,但也有其缺点。 特别是,endpointendpoint_mut方法应该考虑特定后端的实现 trait。 此限制阻止我们在旅途中添加后端,但很少需要此功能。

另一个问题是我们无法在没有其他参数的情况下定义处理程序的特化。 换句话说,如果我们编写以下代码,它将不会被编译,因为它与现有的范型实现相冲突:

impl<(), I, F> From<F> for With<(), I, Result<I>, F>
    where
        F: Fn(&ServiceApiState) -> Result<I>,
    {
        fn from(handler: F) -> Self {
            Self {
                handler,
                _query_type: PhantomData,
                _item_type: PhantomData,
                _result_type: PhantomData,
            }
        }
    }

因此,没有任何参数的请求仍必须接受 JSON 字符串null,该字符串被反序列化为()。 这个问题可以通过 C ++风格的专业化来解决,但是现在它只在编译器的夜间版本中可用,并且不清楚何时它将成为一个稳定的 trait。

同样,返回值的类型也不能专门化。 即使请求没有暗示某种类型的返回值,它仍然会传递带有null的 JSON。

GET请求中解码URL查询也对参数类型施加了一些不明显的限制,但是这个问题与serde-urlencoded实现的 trait 有关。

结论

如上所述,我们已经实现了一个改进的 API,它允许简单而清晰地创建处理程序,而无需担心 Web 细节。 这些处理程序可以与任何后端一起使用,甚至可以同时使用多个后端。

英文原文

2934 次点击
所在节点    Rust
0 条回复

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/510668

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX