原文链接: https://rustasync.github.io/team/2018/09/11/tide.html
全文用有道翻译进行翻译,部分经过自己理解进行修改。
Rust 网络服务工作组今年的目标是在几个方面改进web开发:通过支持async/ await之类的基础,通过改进与web相关的crates的生态系统,以及通过将这些部分整合到一个名为Tide的框架和书籍中来改进web开发。“Tide”一词是指“水涨船高”;其目的是在Rust中改进所有web开发和框架之间的共享、兼容性和改进。
web框架的角色
简而言之,web框架的目标是让您以一种感觉“原生”于宿主语言的方式开发web应用程序。这意味着什么是一个有争议的问题,大多数语言都有多个流行的web框架采用不同的方法。
无论如何,核心问题是解决各种不匹配:
- web应用程序最终需要使用HTTP,但是程序员通常更愿意用纯粹的本地术语来考虑核心“业务逻辑”。在最简单的层次上,您希望您的核心逻辑直接处理整数和结构体之类的东西,而不是原始字符串数据。但这种划分也适用于身份验证、解析url,甚至是如何表示应用程序提供的服务集。
- 上述分解的一个特别重要的方面是处理序列化and/or模板:原始请求/响应主体中出现的数据类型是什么,以及它如何连接到Rust类型?
- 许多web应用程序也与数据库一起工作,在“本机”视图和以数据库为中心的视图之间存在另一组不匹配。像Diesel这样的对象关系映射是解决这个问题的一种方法。
除了解决这些不匹配之外,web框架有时还有更远大的目标,比如:
- 默认情况下安全性很好。
- 一致的应用程序结构,使导航使用框架的外部代码库变得容易。
- 其他关注点的标准版本,如线程和连接池等。
- 用于快速代码生成的工作流。
这是艰苦的工作!它需要大量的底层库来完成繁重的工作,并在其上进行精心的设计和符合人体工程学的工作,以实现所需的关注点分离。
Tide 的愿景
我们与Tide的目标是双重的:
- 首先,支持提供核心web功能的“繁重的工作”。像http和url这样的crates就是这个领域中相对成熟的核心库的例子。我们想要更多这样成熟的crates!其中很大一部分工作是确定和寻找改进、标准化and/or记录这些creates的方法。
- 其次,在这些crates的顶部构建一个严谨的框架,理想情况下作为一个非常小的层级。也就是说,只要可行,我们就想使用现有的crates;如果不可行,我们就想创建小的、独立的crates,而不是一个整体框架。
所有这些还将汇集在一本书中,记录底层的crates和框架。
我们希望从一开始就以开放和合作的方式来做这一切。这篇博客文章是开放式系列文章的第一篇,探索各种设计问题,并寻求反馈和其他想法。如果您想参与这项工作的任何方面,请访问team-web频道了解争议和冲突!
开始的话题
本文的其余部分旨在通过对生态系统的调查,开启对几个核心主题的讨论。我们计划有一个稳定的后续帖子,提出问题,提出“strawman”的想法,并作为一个社区项目开发框架。
基本服务API
大多数语言生态系统最终都提供了一个关键接口来讨论web服务:Ruby有Rack, Python有WSGI, Java有servlet,等等。
这些接口指定了web服务器的含义,web服务器通常看起来像某种给定请求并生成响应的函数(通常是HTTP)。拥有这样一个接口意味着您可以将web框架(它帮助您生成这样一个函数)与底层web服务器(它实际上处理实现HTTP的工作)解耦。它还使提供通用的底层中间件成为可能,这些中间件可以用于任何服务器和框架的选择。
今天在Rust中,大多数web框架(但不是所有web框架)都直接使用hyper来提供基本的HTTP功能,这就防止了这种解耦和简单的中间件应用程序。然而,Tower - Service crate准备通过它的 Service trait 提供一个更通用的接口,而且作为Tower project的一部分,已经有大量的中间件,尽管它还没有发布到crats .io。
简而言之,Service trait 对响应函数的异步请求建模,其中请求和响应类型本身是通用的。对于HTTP,这些将专门用于HTTP creats中的类型。trait还被设置为处理每个连接状态(通过NewService factory trait)和 backpressure (通过ready方法)。
这里有一些重要的开放性问题::
- 服务特性是在 async/await 中借用之前设计的,并且可能需要进行一些更改。特别是,在某些情况下,您可能借用了一些数据,希望将这些数据写入响应中,而不需要额外拥有副本。目前还不清楚在当前的设置下是否可以做到这一点。这里有一个问题需要进一步讨论。
- http crates 提供基本的请求和响应类型,而主体数据一般被处理。因此,要建立HTTP服务的标准化,我们还需要标准化这些类型(或围绕它们的约束),这也意味着要考虑流体。最近一篇关于tower-web的文章提出了一种基于BufStream新特性的方法。
简而言之,Rust中的服务抽象已经有了非常坚实的基础,希望工作组能够帮助推动标准化和改进。这种抽象也可以作为构建Tide的起点。
路由策略
将上面的抽象视为理所当然,您可以将web框架视为一种非常奇特的方式,帮助您编写最终是单个请求-响应函数的内容。正如本文开头所解释的,框架所做的许多工作是将关注点分离出来,这样就可以用自然的风格编写核心业务逻辑。
大多数框架的起点是一些路由系统,它将url和HTTP请求类型映射到包含业务逻辑的特定函数(通常称为端点)。路由机制通常是处理其他一些以http为中心的问题的地方,例如验证。因此,它可以是框架如何组合在一起的一个定义方面。
路由方法一般分为三类:
Endpoint-centric routing
这是Rocket 和 Tower Web采用的方法。您可以用属性“装饰”一个标准的Rust函数,这些属性包括关于应该映射到该函数的URL的信息,以及如何提取各种参数和(对于Rocket)执行附加验证。以下是来自Tower Web的一个片段:
#[get("/")]
#[content_type("json")]
fn hello_world(&self) -> Result<HelloResponse, ()> { .. }
其吸引力显而易见:这种设置具有非常低的进入障碍,并将所有重点放在实现 Endpoint 的““native Rust”函数上。另一方面,它通常需要基于宏的实现,并且可能比其他一些方法更难扩展或定制。
Table-of-contents routing
这是Gotham、Rouille和Actix Web采取的方法。通过使用构建器样式或基于宏的api,可以独立于端点构造路由逻辑。例如,在Gotham中,你可以创建一个显式的路由器数据类型:
fn router() -> Router {
build_simple_router(|route| {
route.request(vec![Get, Head], "/").to(index);
route.get_or_head("/products").to(products::index);
route.get("/bag").to(bag::index);
route.scope("/checkout", |route| {
route.get("/start").to(checkout::start);
route
.post("/payment_details")
.to(checkout::payment_details::create);
route
.put("/payment_details")
.to(checkout::payment_details::update);
route.post("/complete").to(checkout::complete);
});
})
}
这种“目录”分解使得从endpoint 逻辑独立地查看应用程序的高层结构更加容易。它还有助于处理endpoint问题,比如一组路由,所有这些路由都共享一个公共验证需求。另一方面,它比以endpoint为中心的方法更重,而且处理提取(从请求中提取信息)往往更棘手。
通过自由形式的组合进行路由
这是Warp采用的方法。在某种程度上,这也可以称为“无路由”方法:不区分端点和应用程序的其他方面。相反,所有东西都是一个“过滤器”,本质上是一个http感知函数,通过组合过滤器来构建应用程序。因此,路由由特定的过滤器处理:
// Just the path segment "todos"...
let todos = warp::path("todos");
// Combined with `index`, this means nothing comes after "todos".
// So, for example: `GET /todos`, but not `GET /todos/32`.
let todos_index = todos.and(warp::path::index());
// `GET /todos`
let list = warp::get2()
.and(todos_index)
.and(db.clone())
.map(list_todos);
// `POST /todos`
let create = warp::post2()
.and(todos_index)
.and(warp::body::json())
.and(db.clone())
.and_then(create_todo);
// Combine our endpoints, since we want requests to match any of them:
let api = list
.or(create);
// View access logs by setting `RUST_LOG=todos`.
let routes = api.with(warp::log("todos"));
现在谈论这种方法在Rust中的优缺点还为时过早。
Routing in Tide
考虑到上面的类型,问题是哪种方法最适合Tide ——既能给生态系统带来最大的利益,又能模式化地适应(因此不针对Tide)。
目前,“table of contents”样式的路由似乎是最佳选择。它在其他语言中已经很成熟,并且已经在Rust中率先使用——但是也可以使用一些标准化和API审查。与以 endpoint 为中心的路由相比,它更易于模块化/可扩展。与自由形式的组合相比,权衡更容易理解。
不过,为了全面展开讨论,"strawman"是有帮助的。本系列的下一篇文章将介绍一个这样的"strawman",深入研究Rust中content -of-content -style路由中出现的一些API设计问题,并研究模块化和共享的途径。
网友评论