使用Rust编写一个web todolist应用

使用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应用,有以下几个步骤

  1. 设置db schema
  2. 编写对应schema的rust struct
  3. 规划router,加入http endpoints
  4. 规划handlers
  5. 规划前后端的数据交互格式
  6. 写代码
  7. 测试

我们一步一步来,首先我们先创建一个应用

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提供的SerializeDeserialize,还有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"
相关推荐
晴子呀3 分钟前
Spring底层原理大致脉络
java·后端·spring
DreamByte4 分钟前
Python Tkinter小程序
开发语言·python·小程序
覆水难收呀13 分钟前
三、(JS)JS中常见的表单事件
开发语言·前端·javascript
阿华的代码王国17 分钟前
【JavaEE】多线程编程引入——认识Thread类
java·开发语言·数据结构·mysql·java-ee
繁依Fanyi23 分钟前
828 华为云征文|华为 Flexus 云服务器部署 RustDesk Server,打造自己的远程桌面服务器
运维·服务器·开发语言·人工智能·pytorch·华为·华为云
andrew_121925 分钟前
腾讯 IEG 游戏前沿技术 二面复盘
后端·sql·面试
shuxianshrng26 分钟前
鹰眼降尘系统怎么样
大数据·服务器·人工智能·数码相机·物联网
优思学院30 分钟前
优思学院|如何从零开始自己学习六西格玛?
大数据·运维·服务器·学习·六西格玛黑带·cssbb
FHKHH31 分钟前
计算机网络第二章:作业 1: Web 服务器
服务器·前端·计算机网络
Lill_bin36 分钟前
JVM内部结构解析
jvm·后端·spring cloud·微服务·云原生·ribbon