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请求的通用处理
最简单直接的方法是声明“端点”特征,该特征描述特定请求的实现:
现在,我们需要在特定的框架中实现此处理程序。 例如,在actix-web中,它看起来如下所示:
我们可以使用结构通过上下文传递请求参数。 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请求的处理程序就足够了,因为我们已经创建了一个与实现细节无关的特征。 但是,我们发现此解决方案还不够先进。
端点特质的缺点
编写处理程序时会生成大量辅助代码:
理想情况下,我们需要能够通过一个简单的闭包作为处理程序,从而显着减少语法噪声。
let elements = elements.clone(); actix_backend.endpoint("/v1/elements_count", move || { Ok(elements.borrow().len()) });
下面我们将讨论如何做到这一点。
将光浸入泛型编程
我们需要添加自动生成适配器的功能,该适配器以正确的关联类型实现“端点”特征。 输入将仅包含带有HTTP请求处理程序的闭包。
参数和闭包的结果可以具有不同的类型,因此我们必须在这里处理方法重载。 Rust不直接支持重载,但允许使用Into和From特性来模拟它。
另外,闭包值的返回类型不必与Endpoint实现的返回值匹配。 要操作此类型,必须从接收到的闭包的类型中提取它。
从`Fn`特性中获取类型
在Rust中,每个闭包都有其自己的唯一类型,该类型无法在程序中明确指出。 对于带有闭包的操纵,我们使用`Fn`特性。 该特征包含具有参数类型和返回值的函数签名,但是,单独检索这些元素并不容易。
主要思想是使用以下形式的辅助结构:
我们必须使用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, };
专业化和标记类型
现在我们有了一个带有显式参数化参数类型的函数,可以使用该函数代替“ Endpoint”特征。 例如,我们可以轻松实现从SimpleExtractor到RequestHandler的转换。 但是,这不是一个完整的解决方案。 我们需要以某种方式区分类型级别的GET和POST请求的处理程序(以及同步处理程序和异步处理程序)。 在此任务中,标记类型对我们有帮助。
首先,让我们重写SimpleExtractor,以便它可以区分同步结果和异步结果。 同时,我们将为每种情况实现`From`特性。 请注意,可以为通用结构的特定变体实现特征。
现在,我们需要声明将请求处理程序及其名称和类型组合在一起的结构:
#[derive(Debug)] pub struct NamedWith<Q, I, R, F, K> {
接下来,我们声明几个将用作标记类型的空结构。 标记将允许我们为每个处理程序实现自己的代码,以将处理程序转换为先前描述的RequestHandler。
现在,我们可以为模板参数R和K的所有组合(处理程序的返回值和请求的类型)定义`From`特性的四个不同实现。
后端立面
最后一步是设计一个可以接受闭包的外观,并将它们添加到相应的后端中。 在给定的情况下,我们只有一个后端-actix-web。 但是,在幕后还可能有其他实施方案。 例如:Swagger规范的生成器。
pub struct ServiceApiScope { actix_backend: actix::ApiBuilder, } impl ServiceApiScope {
请注意如何从其签名自动得出请求参数的类型,请求结果的类型以及处理程序的同步/异步。 此外,我们需要明确指定请求的名称和类型。
方法的缺点
尽管上述方法非常有效,但是具有其缺点。 特别是,
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细节。 这些处理程序可以与任何后端一起使用,甚至可以同时与多个后端一起使用。