• 作者:老汪软件技巧
  • 发表时间:2024-10-12 10:03
  • 浏览量:

随着这几年Rust社区Server领域的生态发展,2024年我觉得是将Rust应用在Web服务器一个比较成熟的时机。本文就是基于这个契机记录下学习Axum服务器框架搭建的经验。

后端语言的选择上,一般不想搞Python、Java或者JS环境的都会选择Go。在服务器领域Rust拥有很多Go的优点:

还有很多其他语言都不具备的吸引人的特性:

让我们开始吧!

目前文章还未完成规划内容,后续持续补充

准备

学习任何一个新东西都要记得,不要给自己增加包袱和限制。其实,条条大路通罗马。一个最佳的实践就是跟着社区经过时间选择出的资源,选一个符合你学习习惯的跟随学习,过程中多参考其他资料多提问。

Keep Simple! 不把事情复杂化,选择最简单的。

1. 安装Rust环境

无需创造更多的教程,跟着官方的引导安装好rust。入门 - Rust 程序设计语言 ()

2. 配置编辑器VSCode

Rust的开发体验总体上是一种轻巧的感觉,有很多编辑器可以选择,这里以VSCode为例。

在插件商店搜索并安装下面几个扩展:

rust-analyzer

rust插件已经被废弃,而官方招安了 rust-analyzer 插件并把它扶正了。插件本身提供代码补全、定义跳转、引用查找、类型文档提示、语法检查、代码格式化、错误提示和建议等等功能,开箱即用,只需要一个就行,个人感觉比Go的插件好用。

REST Client (非必须)

做web开发的小伙伴都可以用到的rest客户端。他通过在文件中编辑rest请求,点击发送并查看结果。虽然,没有Postman的功能强大,但是简单易用覆盖大部分场景。

Database Client(非必须)

如果你没有购买安装 Navicat,那么这个插件将会是一个vscode一站式不用换乘的数据库客户端好工具。安装后在侧边栏可直接创建数据库连接,多个连接以文件目录的形式分类。可以对数据库进行新建、查询、插入等一系列基操,覆盖常规的使用场景。

3. 服务端技术选型分析

服务端框架

选择 axum,axum 是现在最火热的服务器框架。很多人选它的理由也应该是他小巧而拓展性强,基于 tokio、tower 和 hyper 带来了强大完整的生态,更是因为它专注于人体工程学带来的舒适的开发体验。

数据库连接

选择sqlx,它不是一个ORM库。虽然ORM库已经在其他成熟后端框架中验证有价值,但是好像与rust的语言调性不是很搭,目前的ORM库我觉得都达不到工程化和人体工程学的要求。sqlx 的 api 使用简单,效率高,结合sqlbuilder 我觉得在中小型的项目中我觉得游刃有余。

拓展:在一个rust 服务器构建模板 rust on nails中使用了 cornucopia 这个库,通过sql反向生成代码,看起来很不错的一个尝试。

开始搭建

注意:文章内容都可以在模板项目 rust_project_templates (github) 内的 rust-on-bubbles 中找到完整代码。

Rust on Bubbles 意为气泡上斑斓多彩的锈迹,表示rust给我们带来的丰富的可能性。

1. 创建项目

执行命令:

cargo new your-project-name

验证一下,默认项目会打印 Hello, world!:

cd your-project-name

cargo run

拓展一下

情况一:创建一个没有 git 本地的新项目 local-project

cargo new local-project --vcs none

情况二:初始化一个刚 Clone 下来的空仓库

# cd to your project

cargo init

2. 构建目录结构

社区里的示例项目基本都是一个单项目,但是对于工程实际的服务端来说,一个单项目会让事情变得更加复杂。同时,避免搞出更复杂的三层架构,我们将按照服务对象的不同,将代码简单分成两个仓库:

这里要用到 cargo 项目管理的 workspace 功能。

首先,在项目根据目录创建crates目录用来放置多个项目。

其次,在其中创建二进制项目server和库项目db。

cargo new crates/server

cargo new crates/db --lib

项目不一定非要放到 crates 目录里,也可以直接放根目录下,只是这么做井然有序一些,你根据自己喜好组织。

最后,删除根目录下的src目录,并修改根目录下的Cargo.toml为:

[workspace]
members = [
    "crates/*",
]
resolver = "2"

运行cargo run验证一下,输出 Hello, world!

拓展:需要了解 Cargo.toml 更多的配置,可查看官方文档 清单格式 - Cargo 手册 中文版 ()

3. 初始化 server 项目

依次执行下面添加依赖包:

cd crates/server

cargo add axum

cargo add axum-macros

cargo add serde -F derive

cargo add tokio -F full

cargo build

提示:添加了依赖库后运行一下build是一个好习惯,可以让后面编写代码时代码提示处于已准备好的状态,避免因为依赖找不到导致代码报错,制造出大量的panic。

这些包只是目前会用到的,在开发中需要用什么包再添加什么包,feature(可选功能) 也是需要用到了再去 Cargo.toml 中手动添加,做到 如无必要勿增实体 即可。安装后Cargo.toml是这样:

[package]
name = "server"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.7.7"
axum-macros = "0.4.2"
serde = { version = "1.0.210", features = ["derive"] }
tokio = { version = "1.40.0", features = ["full"] }

将main.rs内容替换为下面的代码:

use axum::{response::Html, routing::get, Router};
use tokio::net::TcpListener;
#[tokio::main]
async fn main() {
    // ## build application
    let app = Router::new()
        .route("/", get(Html("

Hello World!

"))) .route("/hello", get(|| async { "Hello Rust on Bubble!" })); // ## run app with hyper, listening globally on port 3000 let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app).await.unwrap(); }

运行cargo run启动服务器,在浏览器输入localhost:3000和localhost:3000/hello查看结果。

4. 实现用户api

首先,需要先定义业务对象,让我们创建models模块。

Cargo 定义每一个文件都是一个模块,每一个文件夹也是一个模块。文件模块中的导出是通过pub关键字,文件夹需要mod.rs文件来指定需要向外部pub哪些文件模块,从而形成命名空间的层级结构。

在server/src目录下创建models目录,在其中创建mod.rs和user.rs。

自项目开始至项目完成__搭建开发环境步骤

use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Clone)]
pub struct User {
    pub name: String,
    pub email: String,
}

pub mod user;

然后,定义路由处理器,让我们创建handlers模块。

在server/src目录下创建handlers目录,在其中创建mod.rs和user.rs。

use axum::{
    http::StatusCode,
    Json,
};
use  crate::models::user::User;
#[axum_macros::debug_handler]
pub async fn get_user() -> Result, StatusCode> {
    Ok(Json(User {
        name: "alpha".to_string(),
        email: "a@a.com".to_string(),
    }))
}

pub mod user;

最后在main.rs中配置路由,代码如下:

use axum::{routing::get, Router};
use handlers::user::get_user;
use tokio::net::TcpListener;
mod handlers;
mod models;
#[tokio::main]
async fn main() {
    let user_router = Router::new().route("/", get(get_user));
    // ## build application
    let app = Router::new().nest("/user", user_router);
    // ## run app with hyper, listening globally on port 3000
    let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

运行cargo run启动服务器,在浏览器输入localhost:3000/user查看结果。

后续可继续按照这个模式添加更多的路由和handler。

5. 初始化 db 项目

初始化db项目前需要准备好你的数据库,sqlx 支持 mysql、postgresql、sqllite,以 postgresql(简称pgsql) 为例。

在 db 项目中创建 src 的同级目录 migrations,用来放置初始化数据库的 sql 文件,在其中创建一个 db.sql 文件。

假设你已经在开发环境上安装好了 pgsql 和 vscode 的 Database Client 插件,那么只需要在 vscode 的侧边栏上选择 Database 面板中创建你的 pgsql 的连接。

然后,在创建的数据库连接上点击 “+”号创建数据库,如下图:

就会在左侧创建一个临时的初始化 sql 文件,我们不使用这个临时文件,只是复制其第一行的连接信息到前面创建的 db.sql 文件中第一行。(在不使用 Database Client 插件 的情况下,这行连接信息不是必须的)

然后,在下面添加如下的 sql 代码:

-- Active: 6666666666666@@127.0.0.1@5432
CREATE DATABASE your_db_name
CREATE TABLE users (
  id serial PRIMARY KEY,
  name VARCHAR(50) UNIQUE NOT NULL,
  email VARCHAR(255) UNIQUE NOT NULL
);
INSERT INTO
    "users" ("id", "name", "email")
VALUES (1, 'user1', 'user1@mail.com,'),
    (2, 'user2', 'user2@mail.com,'),
    (3, 'user3', 'user3@mail.com,');

如果你已经安装了 Database Client 插件,那么就可以直接点击 sql 行上的 Run 一次执行完所有的 sql,完成数据库创建。同时,你可以使用其他任何熟悉的方式执行 sql。

接着我们开始 db 库的初始化,依次执行下面添加依赖包:

cd crates/db

cargo add sqlx -F runtime-tokio,tls-native-tls,derive,macros,postgres

cargo add serde -F derive

cargo build

安装后Cargo.toml是这样:

[package]
name = "db"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0.210", features = ["derive"] }
sqlx = { version = "0.8.2", features = ["runtime-tokio", "tls-native-tls", "derive", "macros", "postgres"] }

⚠️ 严重注意:由于依赖 ring 这个库会导致任何更改都会触发项目的重新编译,sqlx 的 features 选择 result-tls 等依赖于 ring 库的功能就会导致 wactch 整个项目并不会进行增量编译迅速重启,而是要等待全部重新编译。由于非常的耗时,它会让你修改代码后想要马上运行起服务进行调试成为不可能。

将 lib.rs 中的代码替换为:

use sqlx::{postgres::PgPoolOptions, Error, PgPool};
use std::time::Duration;
#[derive(Clone)]
pub struct DbState {
    pub pool: PgPool,
}
pub async fn connc_db() -> Result {
    PgPoolOptions::new()
    .max_connections(5)
    .acquire_timeout(Duration::from_secs(3))
    .connect("postgres://your_db_username:your_db_password@localhost/your_db_name")
    .await
}

记得修改代码中的连接字符串为你自己的!!!

然后,执行下面命令添加db的依赖到server:

cd crates/server

cargo add db

cargo build

下面这一行会被加入到server/Cargo.toml中。

db = { version = "0.1.0", path = "../db" }

最后,在 server/src/main.rs 中加入如下代码:

use db::connc_db;
#[tokio::main]
async fn main() {
    // ## init DB pool
    let _pool = connc_db().await.expect("Can't connect to database");
    //...
}

运行cargo run启动服务器,服务成功启动代表数据库连接成功。

启动服务失败会打印下面的报错信息,这个时候你需要需要检查你的数据库连接和数据库连接字符串

Can't connect to database

6. 配置环境变量

前面直接硬编码数据库连接字符串到了代码中,在实际工程实践中这样做是很不合理的。我们应该通过环境变量来配置数据库的连接字符串。

执行下面命令添加环境变量依赖库:

cd crates/db

cargo add dotenvy

cargo add dotenvy_macro

cargo build

首先,在根目录下创建.env文件,将你的连接字符串设置为环境变量DATABASE_URL。

DATABASE_URL=postgres://your_db_username:your_db_password@localhost/your_db_name

然后,修改 lib.rs 的代码为下面这样:

use dotenvy::dotenv;
use sqlx::{postgres::PgPoolOptions, Error, PgPool};
use std::{env, time::Duration};
#[derive(Clone)]
pub struct DbState {
    pub pool: PgPool,
}
pub async fn connc_db() -> Result {
    dotenv().expect(".env not found!");
    let db_connection_str: String = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    PgPoolOptions::new()
        .max_connections(5)
        .acquire_timeout(Duration::from_secs(3))
        .connect(&db_connection_str)
        .await
}

运行cargo run启动服务器,服务成功启动代表数据库连接成功。

7. 实现数据库查询

在 db/src 中新建文件user.rs

use serde::{Deserialize, Serialize};
use sqlx::PgPool;
#[derive(sqlx::FromRow, Deserialize, Serialize, Clone)]
pub struct User {
    pub id: i32,
    pub name: String,
    pub email: String,
}
pub async fn query_user(pool: PgPool) -> Option {
    sqlx::query_as::<_, User>("SELECT * FROM users")
        .fetch_optional(&pool)
        .await
        .expect("Query error!")
}

在 lib.rs 中公开

pub mod user;

在 server 中创建 app-state.rs 定义应用的状态

use db::DbState;
#[derive(Clone)]
pub struct AppState {
    pub db: DbState,
}

在 server 的 main.rs 中将数据库状态加入app状态中。

use app_state::AppState;
use axum::{routing::get, Router};
use db::{connc_db, DbState};
use handlers::user::get_user;
use tokio::net::TcpListener;
mod app_state;
mod handlers;
mod models;
#[tokio::main]
async fn main() {
    // ## init DB pool
    let pool = connc_db().await.expect("Can't connect to database");
    // ## init app state
    let state: AppState = AppState {
        db: DbState { pool: pool },
    };
    let user_router = Router::new().route("/", get(get_user));
    // ## build application
    let app = Router::new().nest("/user", user_router).with_state(state);
    // ## run app with hyper, listening globally on port 3000
    let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

在 server 的 handlers/user.rs 的添加对 db 函数 query_user 的调用

use crate::{app_state::AppState, models::user::User};
use axum::{extract::State, http::StatusCode, Json};
use db::user::{query_user, UserEntity};
#[axum_macros::debug_handler]
pub async fn get_user(State(state): State) -> Result, StatusCode> {
    let acc: Option = query_user(state.db.pool).await;
    if let Some(acc) = acc {
        return Ok(Json(User {
            name: acc.name,
            email: acc.email,
        }));
    }
    Err(StatusCode::INTERNAL_SERVER_ERROR)
}

运行cargo run启动服务器,在浏览器输入localhost:3000/user就可以查看查询结果。

8. 错误处理(规划中...)9. 日志记录(规划中...)10. Web页面

在现在的前端生态下,服务器的模板页面就拿来搞搞首页、博客、文档就好了。用来做web应用还是要差很多,所以这一块可以直接使用纯前端项目与后端分离部署,也可以自己试试搞服务端渲染。不在这里做模板化html的介绍了。

11. 开发配置(规划中...)