使用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
- 规划前后端的数据交互格式
- 写代码
- 测试
我们一步一步来,首先我们先创建一个应用
cargo new todolist
然后,我们添加依赖,这里我们使用cargo add 添加
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
这样的话,就不用添加版本了。
这里我们建一个简单的数据库
create database todolist;create table todo (id serial primary key,description varchar(512) not null,completed bool not null
);
然后我们正式进入我们的代码部分:
定义postgresql连接,这里我使用了clip库,从命令行传入数据连接参数
// 定义传入参数模型
#[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。
#[derive(Clone)]
struct MyAppState {dbpool: Pool<PostgresConnectionManager<NoTls>>,
}
接下来,我们定义初始化日志模块
tracing_subscriber::fmt::init();
一行代码就能搞定。
然后我们定义几个model,注意这里面实现的trait,serde提供的Serialize
,Deserialize
,还有Debug
, Clone
。
#[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,第一元素为状态码,第二个为参数。
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服务
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也可以编写出来。
下面是整体代码
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,}),),}
}
依赖的版本为
[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"