使用rust编写一个web服务不如使用java提供的spring boot一样简单,需要手工去添加依赖,目前rust web生态已趋近成熟,可以尝试进行web开发。
本次开发的服务使用的依赖有
- axum:一个专注于生态和模块化的web应用开发框架
- serde:rust中数据的泛用性序列化/反序列化库
- tokio:异步运行时库
- tower:为server/client提供模块化和可重用的库
- tower-http:专为HTTP协议提供的模块化和可重用的库
- tracing:日志库
- tracing-subscriber:给tracing日志库提供工具和组合消费者的方法,这个可以提供给axum使用
- bb8:连接池,基于tokio
- bb8_postgres:连接池,专为postgres提供
做一个简单的Web应用,有以下几个步骤
- 设置db schema
- 编写对应schema的rust struct
- 规划router,加入http endpoints
- 规划handlers
- 规划前后端的数据交互格式
- 写代码
- 测试
我们一步一步来,首先我们先创建一个应用
bash
cargo new todolist
然后,我们添加依赖,这里我们使用cargo add 添加
bash
cargo add axum serde tokio tower tower-http tracing tracing-subscriber bb8 bb8-postgres clap --features serde/derive,tokio/rt-multi-thread,tower-http/fs,tower-http/trace,clap/derive
这样的话,就不用添加版本了。
这里我们建一个简单的数据库
sql
create database todolist;
create table todo (
id serial primary key,
description varchar(512) not null,
completed bool not null
);
然后我们正式进入我们的代码部分:
定义postgresql连接,这里我使用了clip库,从命令行传入数据连接参数
rust
// 定义传入参数模型
#[derive(Parser, Debug)]
#[command(version, about, long_about=None)]
struct Args {
#[arg(short='H', long)]
host: String,
#[arg(short, long)]
user: String,
#[arg(short, long)]
password: String,
#[arg(short, long)]
dbname: String,
}
// 主体部分,建立postgreSQL的数据库连接
let args = Args::parse();
let connection_str = format!("host={} user={} password={} dbname={}", args.host,args.user,args.password,args.dbname);
let manager = PostgresConnectionManager::new_from_stringlike(connection_str, NoTls).unwrap();
let pool = Pool::builder().build(manager).await.unwrap();
这里,我使用了axum中的AppState来管理全局所要使用的变量,在axum中使用Router::new()
提供的with_state
,值得注意的是,这里的struct必须实现Clone trait。
rust
#[derive(Clone)]
struct MyAppState {
dbpool: Pool<PostgresConnectionManager<NoTls>>,
}
接下来,我们定义初始化日志模块
rust
tracing_subscriber::fmt::init();
一行代码就能搞定。
然后我们定义几个model,注意这里面实现的trait,serde提供的Serialize
,Deserialize
,还有Debug
, Clone
。
rust
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Todo {
id: i32,
description: String,
completed: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct CreateTodo {
description: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct UpdateTodo {
id: i32,
description: Option<String>,
completed: Option<bool>
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct ResultWrapper<T> {
code: u32,
message: String,
data: Option<T>,
}
接下来我们定义handler,分别是获取todo数据列表,新建数据列表和删除数据列表
这里要求返回的结果必须实现IntoResponse,否则无法在axum的Route中注册,可以使用axum提供的Json Struct包括数据和结果,这样就能将数据正常转换为Respone。
State则在axum中进行注册,可以直接在参数列表中传入,这里bb8提供的Pool,不用考虑所有权,不使用clone,直接进行get使用。
返回的结果为一个tuple,第一元素为状态码,第二个为参数。
rust
async fn todo_list(State(app_state): State<MyAppState>) -> impl IntoResponse {
match app_state.dbpool.get().await {
Ok(db) => match db.query("SELECT * FROM todo", &[]).await {
Ok(rows) => {
let data: Vec<Todo> = rows.into_iter().map(|i| {
Todo {
id: i.get(0),
description: i.get(1),
completed: i.get(2)
}
}).collect();
(StatusCode::OK, Json(ResultWrapper{code: 0, message: "ok".to_string(), data: Some(data)}))
}, Err(e) => {
(StatusCode::INTERNAL_SERVER_ERROR, Json(ResultWrapper{code: 500, message: e.to_string(), data: None}))
}
}, Err(e) => {
(StatusCode::INTERNAL_SERVER_ERROR, Json(ResultWrapper{code: 500, message: e.to_string(), data: None}))
}
}
}
async fn todo_delete(State(pool): State<MyAppState>, Json(id): Json<i32>) -> impl IntoResponse {
match pool.dbpool.get().await {
Ok(db) => match db.execute("DELETE FROM todo WHERE id = $1", &[&id]).await {
Ok(r) => {
tracing::info!("todo list id {} had been deleted", id);
(
StatusCode::OK,
Json(ResultWrapper {
code: 0,
message: "ok".to_string(),
data: Some(r),
}),
)
}
Err(e) => (
StatusCode::BAD_REQUEST,
Json(ResultWrapper {
code: 500,
message: e.to_string(),
data: None,
}),
),
},
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ResultWrapper {
code: 500,
message: e.to_string(),
data: None,
}),
),
}
}
async fn todo_create(
State(pool): State<MyAppState>,
Json(input): Json<CreateTodo>,
) -> impl IntoResponse {
match pool.dbpool.get().await {
Ok(db) => {
match db
.query(
"INSERT INTO todo (description, completed) VALUES ($1, FALSE) RETURNING id",
&[&input.description],
)
.await
{
Ok(rows) => {
if let Some(row) = rows.get(0) {
let id: i32 = row.get(0);
(
StatusCode::OK,
Json(ResultWrapper {
code: 0,
message: "ok".to_string(),
data: Some(id),
}),
)
} else {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ResultWrapper {
code: 400,
message: "no return data".to_string(),
data: None,
}),
)
}
}
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ResultWrapper {
code: 500,
message: e.to_string(),
data: None,
}),
),
}
}
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ResultWrapper {
code: 500,
message: e.to_string(),
data: None,
}),
),
}
}
我们看一下,axum的主体部分,即路由注册和端口注册启动这个环节
这里面有:
- 注册路由
- 添加app_state
- 增加日志部分
- 启动web server服务
rust
let app = Router::new()
.route("/", post(todo_create))
.route("/", delete(todo_delete))
.route("/", get(todo_list))
.with_state(my_state)
.layer(TraceLayer::new_for_http());
let listener = tokio::net::TcpListener::bind("127.0.0.1:8889")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
这样,整体一个简单的todolist webserver就已完成,这里面还有一个update部分没有编写,不过仿照上面的handler也可以编写出来。
下面是整体代码
rust
use axum::{
extract::State,
http::StatusCode,
response::IntoResponse,
routing::{delete, get, post},
Json, Router,
};
use bb8::Pool;
use bb8_postgres::{tokio_postgres::NoTls, PostgresConnectionManager};
use clap::Parser;
use serde::{Deserialize, Serialize};
use tower_http::trace::TraceLayer;
#[derive(Parser, Debug)]
#[command(version, about, long_about=None)]
struct Args {
#[arg(short = 'H', long)]
host: String,
#[arg(short, long)]
user: String,
#[arg(short, long)]
password: String,
#[arg(short, long)]
dbname: String,
}
#[derive(Clone)]
struct MyAppState {
dbpool: Pool<PostgresConnectionManager<NoTls>>,
}
#[tokio::main]
async fn main() {
let args = Args::parse();
let connection_str = format!(
"host={} user={} password={} dbname={}",
args.host, args.user, args.password, args.dbname
);
let manager = PostgresConnectionManager::new_from_stringlike(connection_str, NoTls).unwrap();
let pool = Pool::builder().build(manager).await.unwrap();
let my_state = MyAppState { dbpool: pool };
tracing_subscriber::fmt::init();
let app = Router::new()
.route("/", post(todo_create))
.route("/", delete(todo_delete))
.route("/", get(todo_list))
.with_state(my_state)
.layer(TraceLayer::new_for_http());
let listener = tokio::net::TcpListener::bind("127.0.0.1:8889")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Todo {
id: i32,
description: String,
completed: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct CreateTodo {
description: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct UpdateTodo {
id: i32,
description: Option<String>,
completed: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct ResultWrapper<T> {
code: u32,
message: String,
data: Option<T>,
}
async fn todo_list(State(app_state): State<MyAppState>) -> impl IntoResponse {
match app_state.dbpool.get().await {
Ok(db) => match db.query("SELECT * FROM todo", &[]).await {
Ok(rows) => {
let data: Vec<Todo> = rows
.into_iter()
.map(|i| Todo {
id: i.get(0),
description: i.get(1),
completed: i.get(2),
})
.collect();
(
StatusCode::OK,
Json(ResultWrapper {
code: 0,
message: "ok".to_string(),
data: Some(data),
}),
)
}
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ResultWrapper {
code: 500,
message: e.to_string(),
data: None,
}),
),
},
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ResultWrapper {
code: 500,
message: e.to_string(),
data: None,
}),
),
}
}
async fn todo_delete(State(pool): State<MyAppState>, Json(id): Json<i32>) -> impl IntoResponse {
match pool.dbpool.get().await {
Ok(db) => match db.execute("DELETE FROM todo WHERE id = $1", &[&id]).await {
Ok(r) => {
tracing::info!("todo list id {} had been deleted", id);
(
StatusCode::OK,
Json(ResultWrapper {
code: 0,
message: "ok".to_string(),
data: Some(r),
}),
)
}
Err(e) => (
StatusCode::BAD_REQUEST,
Json(ResultWrapper {
code: 500,
message: e.to_string(),
data: None,
}),
),
},
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ResultWrapper {
code: 500,
message: e.to_string(),
data: None,
}),
),
}
}
async fn todo_create(
State(pool): State<MyAppState>,
Json(input): Json<CreateTodo>,
) -> impl IntoResponse {
match pool.dbpool.get().await {
Ok(db) => {
match db
.query(
"INSERT INTO todo (description, completed) VALUES ($1, FALSE) RETURNING id",
&[&input.description],
)
.await
{
Ok(rows) => {
if let Some(row) = rows.get(0) {
let id: i32 = row.get(0);
(
StatusCode::OK,
Json(ResultWrapper {
code: 0,
message: "ok".to_string(),
data: Some(id),
}),
)
} else {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ResultWrapper {
code: 400,
message: "no return data".to_string(),
data: None,
}),
)
}
}
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ResultWrapper {
code: 500,
message: e.to_string(),
data: None,
}),
),
}
}
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ResultWrapper {
code: 500,
message: e.to_string(),
data: None,
}),
),
}
}
依赖的版本为
toml
[dependencies]
axum = "0.7.5"
bb8 = "0.8.5"
bb8-postgres = "0.8.1"
clap = { version = "4.5.13", features = ["derive"] }
serde = { version = "1.0.204", features = ["derive"] }
tokio = { version = "1.39.2", features = ["rt-multi-thread"] }
tower = "0.4.13"
tower-http = { version = "0.5.2", features = ["fs", "trace"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"