- 作者:老汪软件技巧
- 发表时间: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. 开发配置(规划中...)