手把手教你用 Rust 搭建 REST API

我是 Asel,今天我将展示如何用 Rust 搭建一个简单的 REST API。

教程中使用的是 Rocket 框架 编写 API,借助 Diesel ORM 框架处理持久特征。这个框架覆盖了以下所有的点,让我们可以更容易地从最基础开始搭建:

  • 启动网页服务器并打开一个端口。
  • 监听端口上的请求。
  • 如果有请求接入,查看 HTTP header 中的路径。
  • 根据路径将请求路由到处理器( handler
  • 提取请求中的信息
  • 打包由用户生成的数据( data ),并生成响应( response
  • 将响应( response )发回给发送者

安装 Nightly Rust

因为 Rocket 大量使用了 Rust 语法扩展及其他高级、不稳定的特性,所以我们必须要安装 nightly 版。

复制代码

rustup default nightly

如果只想将 nightly 安装到项目文件夹,那可以使用以下命令:

复制代码

rustup override set nightly

依赖

复制代码

[dependencies]
rocket = "0.4.4"
rocket_codegen = "0.4.4"
diesel = { version = "1.4.0", features = ["postgres"] }
dotenv = "0.9.0"
r2d2-diesel = "1.0"
r2d2 = "0.8"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
custom_derive ="0.1.7"
[dependencies.rocket_contrib]
version = "*"
default-features = false
features = ["json"]

在后面的应用部分,我会解释具体该怎么写。

安装 Diesel

下一步要做的就是安装 Diesel 。Diesel 有自己的 CLI(命令行界面),这是我们第一步要做的(假设您使用的是 PostgreSQL )。

复制代码

cargo install diesel_cli — no-default-features — features postgre

然后我们需要告诉 Diesel 该在哪里找到我们的数据库,以下命令将生成一个 .env 文件。

复制代码

echo DATABASE_URL=postgres://username:password@localhost:port/diesel_demo > .env

然后执行以下命令:

复制代码

diesel setup

这样可以搭建一个数据库(如果没有的话),并创建一个空的迁移目录,我们可以用该目录来管理我们的构架(更详细的会在后面讲到)。

运行代码的时候可能会出现以下错误信息:

复制代码

= note: LINK : fatal error LNK1181: cannot open input file ‘libpq.lib’

PG lib folder 路径添加到环境变量中就可以轻易解决。

复制代码

setx PQ_LIB_DIR “[path to pg lib folder]”

神奇的是 Diesel 文档 中竟然没有提及这种错误信息。

强烈建议在 CMD 或者 Powershell 中执行这些命令。如果你用的是 IDE 终端,那么你会看不到这个错误信息,最终把时间浪费在找错误上。

若要解决这个问题,可以把 PG 的 bin 文件路径添加到 Path 变量。

下面我们创建一个用户表并为此创建一个迁移:

复制代码

diesel migration generate users

执行完这个命令后,你会看到迁移文件夹中出现两个文件。

下一步是为迁移编写 SQL 命令:

up.sql

复制代码

CREATETABLEusers
(
idSERIALPRIMARYKEY,
usernameVARCHARNOTNULL,
passwordVARCHARNOTNULL,
first_nameVARCHARNOTNULL
)

down.sql

复制代码

DROPTABLEusers

应用迁移的话可以用这个命令:

复制代码

diesel migration run

最好先回滚之后再重新迁移,以确保 down.sql 准确无误。

复制代码

diesel migration redo

你可以看到 DB.right 出现了用户表。

差点忘了提,在运行 Diesel 安装命令的时候会生成一个文件 schema.rs 。应该是这样的:

复制代码

table! {
users (id) {
id -> Int4,
username -> Varchar,
password -> Varchar,
first_name -> Varchar,
}
}

下面是 Rust 部分

因为要使用 ORM,所以需要先将用户表映射到 Rust 中。Java 中用的是 Class 来映射表格,这种方式被称作 Beans 。Rust 中我们要用的是结构( struct )。首先先创建一个结构。

复制代码

usediesel;
usediesel::pg::PgConnection;
usediesel::prelude::*;
usesuper::schema::users;
usesuper::schema::users::dsl::usersasall_users;
// this is to get users from the database
#[derive(Serialize, Queryable)]
pubstructUser{
pubid:i32,
pubusername:String,
pubpassword:String,
pubfirst_name:String,
}

你大概会好奇结构定义中的这些标注都是什么。他们被称作导出(derives),也就是说,这些代码会导出序列化、可查询的 traits。 #[derive(Serialize)] 以及 #[derive(Deserialize)] 可以用来映射数据到响应和请求上。

下面再创建两个 struct,后面都会用到。

复制代码

// decode request data
#[derive(Deserialize)]
pubstructUserData{
pubusername:String,
}
// this is to insert users to database
#[derive(Serialize, Deserialize, Insertable)]
#[table_name ="users"]
pubstructNewUser{
pubusername:String,
pubpassword:String,
pubfirst_name:String,
}

下面要做的是应用 User 。这样就可以对数据库进行操作了。

这里可以看到,我们将连接传递到方法,返回用户向量(Vector of User)。我们获取了用户表中的所有行,然后将其映射到用户结构上。

出错可能在所难免,如果担心的话可以把错误信息打印出来。

复制代码

impl User {
pub fn get_all_users(conn: &PgConnection) -> Vec {
all_users
.order(users::id.desc())
.load::(conn)
.expect("error!")
}
pub fn insert_user(user: NewUser, conn: &PgConnection) -> bool {
diesel::insert_into(users::table)
.values(&user)
.execute(conn)
.is_ok()
}

pub fn get_user_by_username(user: UserData, conn: &PgConnection) -> Vec {
all_users
.filter(users::username.eq(user.username))
.load::(conn)
.expect("error!")
}
}

现在有了表和映射到表的结构,接下来就需要创建使用它的方法。首先,我们要建一个 route 文件,通常称之为 handler

复制代码

usesuper::db::ConnasDbConn;
userocket_contrib::json::Json;
usesuper::models::{User, NewUser};
useserde_json::Value;
usecrate::models::UserData;

#[post("/users", format ="application/json")]
pubfnget_all(conn: DbConn) -> Json {
   letusers = User::get_all_users(&conn);
    Json(json!({
       "status":200,
       "result": users,
    }))
}

#[post("/newUser", format ="application/json", data ="")]
pubfnnew_user(conn: DbConn, new_user: Json) -> Json {
    Json(json!({
       "status": User::insert_user(new_user.into_inner(), &conn),
       "result": User::get_all_users(&conn).first(),
    }))
}

#[post("/getUser", format ="application/json", data ="")]
pubfnfind_user(conn: DbConn, user_data: Json) -> Json {
    Json(json!({
       "status":200,
       "result": User::get_user_by_username(user_data.into_inner(), &conn),
    }))
}

现在要做的就只剩下设置连接池了。以下是从 Rocket 文档 中摘抄的关于连接池的简介。

“Rocket 内建了对 ORM 无关数据库的支持,Rocket 提供了一个过程宏,使您可以通过连接池轻松连接 Rocket 应用程序到数据库。
“数据库连接池是一种数据结构,用于维护活动的数据库连接以便后续在应用程序中使用。”

复制代码

usediesel::pg::PgConnection;
user2d2;
user2d2_diesel::ConnectionManager;
userocket::http::Status;
userocket::request::{self, FromRequest};
userocket::{Outcome, Request, State};
usestd::ops::Deref;

pubtypePool= r2d2::Pool<ConnectionManager>;

pubfninit_pool(db_url:String) -> Pool {
letmanager = ConnectionManager::::new(db_url);
r2d2::Pool::new(manager).expect("db pool failure")
}

pubstructConn(pubr2d2::PooledConnection<ConnectionManager>);

impl FromRequestforConn {
typeError= ();

fnfrom_request(request: &'aRequest) -> request::Outcome {
letpool = request.guard::<State>()?;
matchpool.get() {
Ok(conn) => Outcome::Success(Conn(conn)),
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())),
}
}
}

implDerefforConn {
typeTarget= PgConnection;

#[inline(always)]
fnderef(&self) -> &Self::Target {
&self.0
}
}

最后,我们需要在 main 文件中启动服务器。

复制代码

#![feature(plugin, const_fn, decl_macro, proc_macro_hygiene)]
#![allow(proc_macro_derive_resolution_fallback, unused_attributes)]
#[macro_use]
externcratediesel;
externcratedotenv;
externcrater2d2;
externcrater2d2_diesel;
#[macro_use]
externcraterocket;
externcraterocket_contrib;
#[macro_use]
externcrateserde_derive;
#[macro_use]
externcrateserde_json;
usedotenv::dotenv;
usestd::env;
useroutes::*;
usestd::process::Command;
moddb;
modmodels;
modroutes;
modschema;
fnrocket() -> rocket::Rocket {
    dotenv().ok();
   letdatabase_url = env::var("DATABASE_URL").expect("set DATABASE_URL");
   letpool = db::init_pool(database_url);
    rocket::ignite()
        .manage(pool)
        .mount(
           "/api/v1/",
            routes![get_all, new_user, find_user],
        )
}
fnmain() {
   let_output =ifcfg!(target_os ="windows") {
        Command::new("cmd")
            .args(&["/C","cd ui && npm start"])
            .spawn()
            .expect("Failed to start UI Application")
    }else{
        Command::new("sh")
            .arg("-c")
            .arg("cd ui && npm start")
            .spawn()
            .expect("Failed to start UI Application")
    };
    rocket().launch();
}

在我的项目中,我还添加了 Angular 前端,但用的还是我们的 Rust 后端来支持。

运行程序使用: cargo run

启动服务器

下面用 Insomnia 测试一下我们的服务器。

希望本文能对你有所帮助。祝好!

英文原文:

How to Build a REST API in Rust — A Step-by-Step Guide