Rust生态系统尚未完全解决。 新的库经常出现在其中,比以前的库明显要好,并且以前流行的框架也已过时。 这正是我们在开发Exonum时使用的Iron Web框架所发生的事情。
Actix-web被选为Iron的替代品。 此外,我将告诉我们如何使用通用编程技术将现有代码移植到新的解决方案中。
ulleo PD的图像我们如何使用铁
在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标头形式出现的中间件附加组件。 为了将所有处理程序合并到一个API中,我们使用了mount。
你为什么要抛弃他
铁是许多添加物的好主力。 但是,它是在遥远的时代写的,当时没有期货和
东京等项目。
铁结构提供了同步的请求处理,因此可以轻松地将其安装在具有大量同时打开的连接的肩blade骨上。 为了使Iron具有可伸缩性,必须使其成为异步的。 为此,有必要重新考虑并重写整个框架,但是开发人员逐渐放弃了对其的工作。
为什么我们切换到actix-web
这是
在TechEmpower基准测试中排名很高的流行框架。 同时,与Iron不同,他正在积极发展。 Actix-web具有精心设计的API和基于actix actor框架的高质量实现。 请求由线程池异步处理,如果处理导致紧急情况,则参与者会自动重新启动。
当然,actix-web有缺陷,例如,它包含大量不安全的代码。 但是后来用Safe Rust重写了它,从而解决了这个问题。
切换到actix解决了稳定性问题。 铁后端可能会由于大量连接而掉线。 通常,新的API是更简单,更高效和统一的解决方案。 用户和开发人员使用该软件界面将变得更加容易,并且其速度将提高。
我们想要的Web框架
对于我们而言,重要的不仅是将Iron转变为actix-web,而且为未来奠定基础-制定新的API架构,以从特定的Web框架进行抽象。 这使您几乎无需考虑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, }
这与“端点特征”中关联的“请求”类型一致。
现在,让我们编写一个适配器,该适配器在RequstHandler中为actix-web包装一个特定的Endpoint实现。 请注意,有关请求和响应类型的信息会在此过程中丢失。 这种技术称为类型擦除。 它的任务是将静态调度转变为动态调度。
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()) });
稍后再说。
易于沉浸在一般编程中
我们需要实现自动生成具有正确关联类型的Endpoint的适配器的功能。 在这种情况下,只有带有HTTP请求处理程序的闭包将被输入到输入中。
参数和闭包的结果可以有多种类型,因此在这里您必须使用方法重载。 Rust不直接支持重载,但允许使用Into和From特征对其进行仿真。
另外,闭包的返回值类型不必与Endpoint实现的返回值匹配。 要操作此类型,必须从接收到的闭包的类型中提取它。
从Fn类型中提取类型
在Rust中,每个闭包都有其自己的唯一类型,该类型无法在程序中明确编写。 要操作闭包,有一个Fn类型。 它包含具有参数类型和返回值类型的函数签名,但是,单独提取它们并不是那么简单。
主要思想是使用以下形式的辅助结构:
由于Rust要求所有泛化参数都在结构定义中,因此我们被迫使用PhantomData。 但是,闭包或函数F的特定类型未通用化(尽管它实现了通用类型Fn)。 类型参数A和B没有直接在其中使用。
正是Rust类型系统的这种局限性不允许采取更简单的策略-直接为闭包实现端点特征:
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 trait。 例如,我们可以轻松实现从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字符串,该字符串会反序列化为()。 此问题可以通过C ++样式的专业化解决,但是到目前为止,它仅在夜间版本的编译器中可用,并且尚不清楚何时“稳定”。
同样,您不能专用于返回值的类型。 即使请求没有暗示,它也将始终返回null的JSON。
GET请求中的URL查询解码也对参数类型施加了一些不明显的限制,但是这些已经是serde-urlencoded实现的功能。
结论
因此,我们实现了一个API,使您可以轻松地创建处理程序,而几乎无需考虑Web细节。 以后,它们可以转移到任何后端,甚至可以同时使用多个后端。