Rust中的通用方法:Exonum如何从铁转变为Actix-web

Rust生态系统仍在增长。 结果,具有改进功能的新库经常被发布到开发者社区中,而旧库则被淘汰。 最初设计Exonum时,我们使用的是Iron网络框架。 在本文中,我们描述了如何使用通用编程将Exonum框架移植到actix-web。



铁的Exonum


在Exonum平台中,使用Iron框架时没有任何抽象。 我们为某些资源安装了处理程序,并通过使用辅助方法解析URL来获取请求参数。 结果仅以字符串形式返回。

该过程看起来(大约)如下所示:

fn set_blocks_response(self, router: &mut Router) { let blocks = move |req: &mut Request| -> IronResult<Response> { let count: usize = self.required_param(req, "count")?; let latest: Option<u64> = self.optional_param(req, "latest")?; let skip_empty_blocks: bool = self.optional_param(req, "skip_empty_blocks")? .unwrap_or(false); let info = self.blocks(count, latest.map(Height), skip_empty_blocks)?; self.ok_response(&::serde_json::to_value(info).unwrap()) }; router.get("/v1/blocks", blocks, "blocks"); } 

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

我们决定远离铁的决定


Iron是一个很好的库,带有许多插件。 但是,它是在不存在诸如期货和东京等项目的时代编写的。

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

为什么我们选择Actix-Web


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

以前,人们担心Actix-Web包含许多不安全的代码。 但是,当用安全的编程语言Rust重写框架时,不安全代码的数量大大减少了。 Bitfury的工程师亲自审查了此代码,并对它的长期稳定性充满信心。

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

我们对Web框架的要求


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

要了解此前端的外观,让我们定义任何HTTP API的真正含义:

  • 要求完全由客户提出; 服务器仅响应它们(服务器不发起请求)。
  • 请求读取数据或更改数据。
  • 作为请求处理的结果,如果成功,服务器将返回一个响应,其中包含所需的数据; 或有关错误的信息(如果发生故障)。

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

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

其他所有内容都可以视为此基本实体的扩展。 因此,为了独立于Web框架的特定实现,我们需要以类似于上面示例的样式编写处理程序。

特征“端点”,用于HTTP请求的通用处理


最简单直接的方法是声明“端点”特征,该特征描述特定请求的实现:

 // 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的特定实现包装到actix-web的RequestHandler中。 请注意以下事实:在执行此操作时,有关请求和响应类型的信息会消失。 这种技术称为类型擦除-将静态分派转换为动态分派。

 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请求的处理程序就足够了,因为我们已经创建了一个与实现细节无关的特征。 但是,我们发现此解决方案还不够先进。

端点特质的缺点


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

 // 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()) }); 

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

将光浸入泛型编程


我们需要添加自动生成适配器的功能,该适配器以正确的关联类型实现“端点”特征。 输入将仅包含带有HTTP请求处理程序的闭包。

参数和闭包的结果可以具有不同的类型,因此我们必须在这里处理方法重载。 Rust不直接支持重载,但允许使用Into和From特性来模拟它。

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

从`Fn`特性中获取类型


在Rust中,每个闭包都有其自己的唯一类型,该类型无法在程序中明确指出。 对于带有闭包的操纵,我们使用`Fn`特性。 该特征包含具有参数类型和返回值的函数签名,但是,单独检索这些元素并不容易。

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

 /// 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特性)。 类型参数A和B不直接在其中使用。

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

 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.", qb, qa) } 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 {}", qb, qa, c) }; let closure_extractor = SimpleExtractor::from(my_closure); 

专业化和标记类型


现在我们有了一个带有显式参数化参数类型的函数,可以使用该函数代替“ Endpoint”特征。 例如,我们可以轻松实现从SimpleExtractor到RequestHandler的转换。 但是,这不是一个完整的解决方案。 我们需要以某种方式区分类型级别的GET和POST请求的处理程序(以及同步处理程序和异步处理程序)。 在此任务中,标记类型对我们有帮助。

首先,让我们重写SimpleExtractor,以便它可以区分同步结果和异步结果。 同时,我们将为每种情况实现`From`特性。 请注意,可以为通用结构的特定变体实现特征。

 /// 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 } 

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

方法的缺点


尽管上述方法非常有效,但是具有其缺点。 特别是, endpoint和endpoint_mut方法应考虑特定后端的实现特性 。 此限制使我们无法在旅途中添加后端,尽管很少需要此功能。

另一个问题是, 如果没有其他参数就无法定义处理程序的专业化 。 换句话说,如果我们编写以下代码,将不会编译它,因为它与现有的通用实现冲突:

 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 ++风格的专业化解决,但目前仅在夜间版本的编译器中可用,尚不清楚何时将成为稳定的功能。

类似地, 返回值的类型不能为specialized 。 即使请求不暗示返回值的某种类型,它仍将传递带有null的JSON。

解码GET请求中的URL查询也对参数的类型施加了一些不明显的限制 ,但此问题与Serde-urlencoded实现的特殊性有关。

结论


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





Source: https://habr.com/ru/post/zh-CN439258/


All Articles