
使用 Warp 和 Tokio 构建简单异步 API 的示例项目。
本教程通过构建异步 CRUD API 来介绍 Warp 框架。 我写这篇文章时思考了以下目标:
在开始编码之前,先勾勒出 API 的设计。 这将有助于确定必要的端点、处理函数以及如何存储数据。
对于这个 API,我只需要两个路由。
/customers
- GET -> 列出数据存储中的所有客户
- POST -> 创建新客户并插入数据存储
/customers/{guid}
- GET -> 列出客户的信息
- POST ->更新客户信息
- DELETE -> 从数据存储中删除客户基于定义的路由,我将需要以下处理程序:
list_customers ->返回数据库中所有客户的列表
create_customer -> 创建一个新客户并将其添加到数据库中
get_customer -> 返回单个客户的详细信息
update_customer -> 更新单个客户的详细信息
delete_customer -> 从数据库中删除客户目前,将只使用内存中的数据存储,使其在路由处理程序之间共享。
我使用 Mockaroo 生成客户数据的 JSON 数据集。 数据是一个 JSON 数组,其中每个对象都具有以下结构:
{
"guid": "String",
"first_name": "String",
"last_name": "String",
"email": "String",
"address": "String"
}此外,数据模块需要能够在服务器启动后初始化数据存储。
截至目前,我知道我将需要以下依赖项:
我要做的第一件事是定义我的客户模型,并开始向代码中添加一些结构。
在 main.rs 中,定义一个名为 models 的新模块,如下所示:
mod models;
fn main() {
// ...
}然后创建一个名为 models.rs 的新文件并添加以下内容:
pub struct Customer {
pub guid: String,
pub first_name: String,
pub last_name: String,
pub email: String,
pub address: String,
}由于我正在设计一个 API,因此该数据结构需要能够与 JSON 相互转换。 我还希望能够将结构复制到数据存储中或从中复制出来,而不必担心借用检查器。
为此,我将添加一个派生语句以使用 Serde 库和 Rust 中的几个宏。 目前 models.rs 看起来像这样:

此示例 API 的数据库将是一个内存数据库,它是 Customer 模型的一个向量(vector)。 不过,数据存储将需要在多个路由之间共享,因此我们可以使用 Rust 的智能指针(smart pointer)和互斥体(Mutex)来实现线程安全。
第一,用一个名为 db 的新模块更新 main.rs:
mod db;
mod models;
fn main() {
// ...
}然后创建一个名为 db.rs 的新文件。
在这个文件中有几件事要做,但第一要做的是定义数据存储的外观。
一个简单的数据存储只是一个 Customer 结构的向量,但它需要包装在一个线程安全的引用中,以便能够在多个异步处理程序中使用数据存储的多个引用。
将以下内容添加到 db.rs:
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::models::Customer;
pub type Db = Arc<Mutex<Vec<Customer>>>;目前我们已经定义了数据存储的结构,我们需要一种方法来初始化数据存储。 初始化数据存储有两种结果,要么是空数据存储,要么是从数据文件中加载数据的数据存储。
一个空荡荡的存储是相当直截了当的。
pub fn init_db() -> Db {
Arc::new(Mutex::new(Vec::new()))
}但是为了从文件中加载数据,我们需要添加另一个依赖项:
serde_json — 用于读取原始 JSON
将以下内容添加到 Cargo.toml 文件中:
serde_json = "1.0"目前我们可以使用以下内容更新 db.rs:
use std::fs::File;
use serde_json::from_reader;
pub fn init_db() -> Db {
let file = File::open("./data/customers.json");
match file => {
Ok(json) => {
let customers = from_reader(json).unwrap();
Arc::new(Mutex::new(customers))
},
Err(_) => {
Arc::new(Mutex::new(Vec::new()))
}
}
}此函数尝试读取位于 ./data/customers.json 的文件。 如果成功,该函数将返回一个加载了客户数据的数据存储,否则将返回一个空向量。
db.rs 目前应该是这样的:

至此,我们有了模型和数据库设置。 目前我们需要一种方法将它们联系在一起。 这就是处理程序。
第一让我们在 main.rs 中定义一个新模块并创建一个名为 handlers.rs 的新文件。
mod handlers;我们还需要添加导入。 在 handlers.rs 文件中添加以下内容:
use std::convert::Infallible;
use warp;
use crate::models::Customer;
use crate::db::Db;此代码段使我们在其他模块中定义的 Customer 模型和 Db 类型在 handlers 模块中可用。 它还导入 warp 模块和 Infallible 枚举。
目前提醒一下,这里是我们要实现的处理程序:
list_customers 处理程序将引用数据存储作为参数并返回一个包含 JSON 响应的结果类型。
函数定义如下所示:
pub async fn list_customers(db: Db) -> Result<impl warp::Reply, Infallible> {
// ...
}对于函数体,我们需要从数据存储中获取客户列表并将其作为 JSON 对象返回。 为了方便起见,warp 提供了一个 reply 方法,可以将 vector 转换为 json 对象。
使用以下内容更新函数:

该行 let customers = db.lock().await; 导致当前任务产生,直到可以获取锁并且可以安全地引用数据存储。
let customers: Vec<Customer> = customers.clone() 这一行从 MutexGuard 中取出内部向量。
最后一行 Ok(warp::reply::json(&customers)) 将 JSON 回复包装在 Result 类型的 Ok 变体中。
create_customer 处理程序将采用 Customer 对象和对数据存储的引用作为参数,如果新客户已添加到客户列表中,则返回已创建状态代码;如果客户已存在,则返回错误请求代码。
在我们使用函数之前,我们需要更新 warp 导入语句以允许使用状态代码。
在 handlers.rs 中,更改行 use warp;内容:
use warp::{self, http::StatusCode};这将允许使用 StatusCode 枚举作为响应。
函数定义将类似于 list_customers 处理程序,因此我们可以跳转到完整定义。

get_customer 处理程序将 guid 和数据存储引用作为参数,如果找到客户,则返回客户的 JSON 对象,否则返回默认客户。
在编写此实现之前,我们需要向 Customer 结构添加一个宏。 将 models.rs 中的 Customer 结构更新为以下内容:

函数定义如下所示:
pub async fn get_customer(guid: String, db: Db) -> Result<Box<dyn warp::Reply>, Infallible> {
}返回类型与其他函数略有不同。 缘由是我们需要能够返回 JSON 对象或指示未找到错误的状态代码。 由于 warp::reply::json() 和 StatusCode 实现了 warp::Reply ,我们可以使用动态调度来返回适当的类型。
有了正确的返回类型,我们的函数体就相当简单了:

update_customer 处理程序将客户和数据存储引用作为参数,如果找到并更新了客户,则返回状态代码 OK,如果客户不在数据存储中,则返回 NOT FOUND。
该函数应如下所示:

delete_customer 处理程序将采用 guid 和对数据存储的引用作为参数。 该函数将删除具有匹配 guid 的客户并返回 NO CONTENT 状态代码。 如果未找到匹配项,则它将返回 NOT FOUND 状态代码。
该函数应如下所示:

我们目前已经实现了所有的处理函数。 接下来我们需要将调用处理程序的路由拼凑在一起。
在 main.rs 中,定义另一个模块:
mod routes;然后我们在 src 目录中创建一个名为 routes.rs 的文件并添加以下内容:
use std::convert::Infallible;
use warp::{self, Filter};
use crate::db::Db;
use crate::handlers;
use crate::models::Customer;第一,我们需要一个辅助函数来将数据存储的引用从路由传递到处理程序。
将以下内容添加到 routes.rs:
fn with_db(db: Db) -> impl Filter<Extract = (Db,), Error = Infallible> {
warp::any().map(move || db.clone())
}此函数允许将数据存储注入到路由中并传递到处理程序中。 Filter 是 warp 库中的一个特征。 Filter trait 提供了组合路由的功能,这些路由是一个或多个 Filter 方法的结果。 这将通过示例更有意义。
提醒一下,这里是我们需要定义的路线:
/customers
- GET -> 列出数据存储中的所有客户
- POST -> 创建新客户并插入数据存储
/customers/{guid}
- GET -> 列出客户的信息
- POST ->更新客户信息
- DELETE -> 从数据存储中删除客户第一个路由将简单地获取数据存储中的所有客户。 将以下内容添加到 routes.rs:

该函数返回一个实现 Filter 特征的类型。 发生匹配时使用 Extract 并返回 Extract 的值。
基本上,该函数只定义了一个路由,并只当请求的路径是“/customers”并且它是一个 GET 请求时匹配。
此外,为了后来节省一些工作,我将实现另一个函数,该函数将作为所有客户路线的包装器。 当我们将所有东西连接在一起时,它会让事情变得更容易。
所以将以下内容添加到 routes.rs 中:
pub fn customer_routes(db: Db) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
customers_list(db.clone())
}如果新客户尚不存在,此路由会将其添加到数据存储中。
在我们为路由添加函数之前要添加的一件事是一个辅助函数,用于从 POST 请求正文中提取 JSON。
将以下内容添加到 routes.rs:
fn json_body() -> impl Filter<Extract = (Customer,), Error = warp::Rejection> + Clone {
warp::body::content_length_limit(1024 * 16)
.and(warp::body::json())
}除了处理程序外,该功能与 customers_list 超级类似。 将以下内容添加到 routes.rs:

该函数定义了一个路由,当路径为“/customers”并且是一个post请求时匹配。 然后从 post 请求和数据存储引用中提取 JSON 并将其传递给处理程序。
此路由将尝试从数据存储中检索单个客户。
这个路由函数会引入path!宏。 这个宏使我们能够创建一个带有变量的路径。
将以下内容添加到 routes.rs:

这定义了将匹配“customers/{some string value}”和 GET 请求的路由。 然后它提取数据存储并将其传递给处理程序。
路由需要思考的一件事是第一检查最具体的路由,否则路由可能不匹配。
例如,如果路由的辅助函数更新为:
pub fn customer_routes(
db: Db,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
customers_list(db.clone())
.or(create_customer(db.clone()))
.or(get_customer(db.clone()))
}get_customer 路由永远不会匹配,由于共享一个公共根路径 - “/customers” - 这意味着客户列表路由将匹配“/customers”和“/customers/{guid}”。
要解决不匹配问题,需要组织路由,以便第一匹配最具体的匹配项。 像这样:
pub fn customer_routes(
db: Db,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
get_customer(db.clone())
.or(customers_list(db.clone()))
.or(create_customer(db.clone()))
}此路由将尝试更新客户(如果存在)并返回 OK 状态代码,否则返回 NOT FOUND 状态代码。
该路线看起来类似于创建客户路由,但它会匹配不同的路径。 将以下内容添加到 routes.rs:

然后更新客户路由包装器:
pub fn customer_routes(
db: Db,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
get_customer(db.clone())
.or(update_customer(db.clone()))
.or(create_customer(db.clone()))
.or(customers_list(db))
}最后一条路由只是从数据存储中删除客户,如果客户与给定的 guid 匹配,返回 NO CONTENT 状态代码,否则返回 NOT FOUND 状态代码。
将以下内容添加到 routes.rs:

然后更新客户路由包装器。 添加所有路由后,包装器应如下所示:

这样就完成了所有路由。 目前我们可以继续将所有内容捆绑在一起。
main.rs 将把所有放在一起。 它将初始化数据存储、获取所有路由并启动服务器。 它也是相当短的文件,所以我展示完整内容:

我们已经看到了前几行,所以让我们看一下 main 函数。
函数属性 #[tokio::main] 设置 tokio 运行时的入口点。 这允许我们将主函数声明为异步的。
main 的前两行是从我们的模块中调用函数。 第一个初始化数据存储,第二个获取我们的客户路由包装器。
最后一行使用 warp::server 创建服务器,然后运行以在提供的主机和端口上启动服务器。 我们使用 await 关键字来 yield 直到 run 函数完成。
这样就完成了一个使用 Rust 和 Warp 框架的简单 API。
但是,可以进行一些改善。
这里有一些想法: