当前位置: 首页 > news >正文

Rust Rocket: 构建Restful服务项目实战

前言

这几天我的笔记系统开发工作进入了搬砖期,前端基于Yew,后端基于Rocket。关于Rocket搭建Restful服务,官方也有介绍,感觉很多细节不到位。因此我打算花2到3天的时间来整理一下,也算是对自己的一个交代。

对于有一定经验的开发者来说,他们可能已经熟悉了 Restful 开发中的基本 HTTP 方法,如 GET、POST、PUT 和 DELETE。然而,从项目实战的角度来说,这些方法的细节处理是不容忽视的。在项目开发中,我们必须关注文件夹结构的组织、参数的获取、返回值的处理和日志处理等方面的问题。这些问题的解决对于一个项目的质量至关重要,任何偷工减料或者不恰当的行为都会对项目造成不良影响。

在本篇文章中,我将会从实战的角度出发,帮助大家全面了解如何处理这些细节问题。

介绍

Rocket 是一个强大的 Rust 网络框架,可帮助开发者快速、安全地构建灵活易用的 web 应用程序,并提供类型安全性。在本实战文章中,我将使用 Rocket 框架搭建 Restful 服务,实现对笔记数据的 GET、POST、PUT 和 DELETE 操作,并使用 Postgres 数据库进行数据持久化。

准备工作

安装Rust

Rust安装非常简单,命令行如下:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

需要注意的是,安装 Rust 的命令中有一个与人机交互的环节,需要选择安装模式。通常情况下,直接按回车键选择默认选项即可。
但是,如果您希望在Dockerfile中来安装rust,比如,创建一个基于 CentOS 的 Rust 环境的 Docker 镜像,就必须跳过人机交互的环节,让命令行自动执行默认选项。
如何通过命令行来自动执行默认选项呢,可能会对不熟悉 Linux 命令的人来说有些困难。不过,不用担心,这个坑我已经踩过去了,在 Dockerfile 中,您可以使用下面的命令来实现。

run curl -s --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | bash -s -- -y

(您可以在Docker hub上搜到官方制作的Rust的镜像,但是这个镜像是基于Ubuntu的。我曾经就遇到一个Rust项目运行于Centos7,依赖glibc 2.17。但是Rust的 Ubuntu镜像中的glibc的版本是2.35,为此做了一个Centos7的Docker镜像来编译Rust项目)

创建项目

创建项目很简单,就下面一行命令。但是从实战的角度,要多说两句。

cargo new my-app

我们是先创建git仓库,再初始化项目代码呢,还是先初始化项目代码,再来设置git仓库?这里可能会小纠结一下。
两种情况都可以,在我们分析这两种情况之前,我们来仔细看一下这个命令行到底为我们创建了什么?

my-app是文件夹名称,也是项目的名称,它会自动初始化到my-app/Cargo.toml中,代码如下:

[package]
name = "my-app"
version = "0.1.0"
edition = "2021"# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html[dependencies]

创建的文件结构如下:

bash-3.2$ cargo new my-app
bash-3.2$ tree ./my-app
./my-app
├── Cargo.toml
└── src└── main.rs1 directory, 2 files

情况1. 先创建git,再创建项目

因此,如果我们先创建git,再初始化项目代码,那么我么创建项目的真正命令行如下:

git clone git@gitee.com:xxx/my-app
cd my-app
cargo new my-app
git add .
git commit . -m "init commit"
git push

得到的项目文件夹结构如下:

bash-3.2$ tree ./my-app
./my-app
└── my-app├── Cargo.toml└── src└── main.rs2 directories, 2 files

从上面的目录结构来看,我们的开发目录是./my-app/my-app,比项目的目录多了一层。这样的好处是,可以把项目中和开发无关的文件放在项目的根目录./my-app中,比如,部署脚本,需求文档,项目相关的会议记录等;把和Rust开发相关的文件放在./my-app/my-app中。

情况2. 先创建项目,再创建git

如果我们先创建git,再初始化项目代码,那么我们创建项目的真正命令行如下:

cargo new my-app
cd my-app
git init
git add .
git commit . -m "init commit"
git remote add origin git@gitee.com:xxx/my-app.git
git push -u origin "master"

得到的项目文件夹结构如下:

bash-3.2$ tree ./my-app
./my-app
├── Cargo.toml
└── src└── main.rs1 directory, 2 files

从上面的目录结构看,我们的项目目录和开发目录是同一个目录。这样做的好处在于整个项目的目录结构比较直观,方便理解。

上面从实战的角度介绍了项目的初始化方式,大家可以根据自己项目的特点和诉求进行调整。

Restful基础

项目创建好了,从实战的角度,按理说我们应该讲实现了。但具体实现什么呢?面对这个问题,我想还是先稍微介绍一下Restful。

RESTful(Representational State Transfer)是一种软件架构风格,它基于 HTTP 协议的设计理念,旨在提供一种简单、轻量级、可扩展和易于理解的方式来构建和访问 Web 服务。说Restful基于HTTP协议的设计理念,主要是因为它基于HTTP的方法: GET(获取资源)、POST(创建资源)、PUT(更新资源)、DELETE(删除资源)来完成服务接口的构建。

在Restful服务中,资源(Resources)是一个很重要的设计概念。RESTful 服务将数据和功能封装为资源,并通过统一的资源标识符(URI)来访问这些资源。我们上面提到的GET,POST,PUT和DELETE都是围绕资源来设计的。在项目实战中,对资源的识别是很重要的一个设计环节。

Restful服务通过Json和Xml数据格式来和客户端交换数据,这一点,在Javascript应用横行天下的今天尤为重要。从项目实战的角度,这个特点意味着它可以和几乎所有的客户端进行数据交互。我认为这是Restful成为现代 Web 开发的常用的架构风格的重要原因。

实战开始

实战是各种概念和经验交汇后的结果,因此,这里的实战多少带了我的一些个人色彩在里面。我尽量将“为什么要这么做”的原因也分享给大家,大家可以结合自己的项目实际情况来进行裁剪。(之所以说是尽量,也是因为这里面的水太深,有的地方我也是“知其然”不知其“所以然”。)

实战的内容包括:项目的目录结构,路由参数处理,路由返回值处理,日志。

项目的目录结构

笔记系统的后端api目录结构如下:

bash-3.2$ tree ./note_book_api/
./note_book_api/
├── LICENSE
├── app
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── error.log
│   ├── src
│   │   ├── fairings.rs
│   │   ├── main.rs
│   │   ├── models.rs
│   │   ├── routes.rs
│   │   ├── services
│   │   │   ├── mod.rs
│   │   │   └── note_book.rs
│   │   └── utils.rs
│   └── test.sh
└── db_scripts├── create_container.sh└── init└── init_db.sh5 directories, 14 files

从这个目录结构,大家可以看出这个结构这里采用的是第一种情况的项目管理方式,即git仓库目录中包含开发目录。在git仓库目录中,包含开发目录app,和数据库相关的脚本目录db_scripts

项目的开发目录app,从代码设计上分为

  1. routes:用于定义路由;
  2. fairings:用于定义rocket的中间件,之所以命名为fairings,是因为Rocket的中间件都要实现rocket::fairing::Fairing trait;
  3. models:用于定义与业务相关的数据模型;
  4. services:用于实现业务逻辑的服务模块;
  5. utils:用于定义各种功能代码,例如与IO相关的功能;

项目的数据库相关的脚本目录db_scripts

  1. init_db.sh用于初始化数据库结构。这个脚本应该具有幂等性,即保证项目在多次业务迭代中,数据库相关的结构能够持续更新。
  2. create_container.sh用于初始化数据库的docker容器。这个脚本应该具有幂等性,用于持续更新数据库容器。

路由参数处理

路由参数的处理可以归纳为以下几种情况

  1. 从post方法中获取数据
  2. 从url上获取数据
  3. 从query上获取数据

路由参数解析是Rocket框架的重要功能之一,它充分展现了Rocket框架的强大和灵活性。通过Rocket框架,我们能够轻松地解析和提取路由中的参数,无论是路径参数还是查询参数。Rocket框架的参数解析功能,为我们构建功能丰富的Web应用提供了强有力的支持。

从post方法中获取数据

#[post("/api/notes", format = "application/json", data = "<note>")]
pub async fn post_notes(note: Json<Note>) -> Result<String, MyStatus> {...
}

上面这段代码,实际上包含了两个概念,路由和handler。第一个参数"/api/nores"必须是字符串常量,format是序列化的数据格式,包括"application/json",data对应的参数名称。Json<Note>用于将http中data中的数据反序列化为对应的对象。
就上面的代码而言,可以考虑Note结构体通过#[derive(Default)]来实现default trait,以确保当客户端传过来的数据在反序列化时,如果出现了字段不匹配的情况,系统不会报错。
这里我说考虑,是因为这个报错不是一个单纯的错误。它关系到数据逻辑的严谨性和逻辑容错性问题。严谨性越高,容错性就越低,所以,从实战的角度来说,这是一个架构设计的权衡点。

format除了"json",还有下面几种类型:

“any” - MediaType::Any
“binary” - MediaType::Binary
“bytes” - MediaType::Bytes
“html” - MediaType::HTML
“plain” - MediaType::Plain
“text” - MediaType::Text
“json” - MediaType::JSON
“msgpack” - MediaType::MsgPack
“form” - MediaType::Form
“js” - MediaType::JavaScript
“css” - MediaType::CSS
“multipart” - MediaType::FormData
“xml” - MediaType::XML
“pdf” - MediaType::PDF
“markdown” - MediaType::Markdown
“md” - MediaType::Markdown

从url上获取数据

#[get("/api/note/<id>")]
pub async fn get_note(id: String) -> Result<Json<Note>, MyStatus> {...
}

通过<id>,将url上的参数映射到参数上。

从query上获取数据

#[get("/api/note?<id>")]
pub async fn get_note1(id: String) -> Result<Json<Note>, MyStatus> {...
}

通过<id>,将query上的参数映射到参数上。

路由返回值(Response)处理

Rocket支持路由返回值包括: String, str, File, Option和Result。但是从项目的实战的角度来说,我个人偏向于使用Result来作为路由的返回值。

#[post("/api/notes", format = "application/json", data = "<note>")]
pub async fn post_notes(note: Json<Note>) -> Result<Json<Res<String>>, MyStatus> {Ok(Json(Res::new(services::insert_or_update_note(&note.into_inner()).await?)))
}

我个人认为使用Result有以下两个好处

  1. 统一错误类型,有利于让内部代码的错误处理变得更加简洁
  2. 统一返回值的基本结构,有利于客户端的调用

首先说一下让内部代码的错误处理变得更加简洁。
Result<Json<Res<String>>, MyStatus>意味着,在这个项目的代码中,我们把任何函数的返回值都定义成Result<T, MyStatus>,或者实现impl From<E> for MyStatus。这种处理方式使得代码的错误处理变得异常的简洁。例如下面的代码:

pub async fn insert_or_update_note(note: &Note) -> Result<String, MyStatus> {...if note.has_id() {let _ = client.execute("update notes set title=$2, content=$3 where id=$1",&[&id, &note.title, &note.content],).await.map_err(MyError::from)?;} else {...}Ok(id)
}

在上面的代码中,直接通过map_err(MyError::from)?完成了execute函数在执行时的错误处理,从而省去了if ... else ...或者match的冗繁写法。我在已经感受到了Rust类型的一等公民地位中专门讨论了类型转的问题。

关于统一返回值的结构,有利于客户端的数据读取设计。
这里我定义了范型Res<T>,这个结构使Javascript客户端能够以统一的方式来解析数据,从而简化客户端的代码。

#[derive(Debug, Deserialize, Serialize)]
#[serde(crate = "rocket::serde")]
pub struct Res<T>{pub data: T
} 

日志处理

对于后端的api来说,日志功能尤其重要,其重要程度怎么形容都不过分。目前,对于笔记系统来说,部署规模都还很低,因此,我才用的是simplelog框架。
在Rocket中,日志通过Faring trait 来实现。Fairing trait 允许我们在应用程序启动过程中插入全局的中间件。
在我们的笔记系统设计中,我将日志中间件放在fairings.rs文件中。如果项目更加复杂可以将fairings.rs文件扩展成fairings文件夹。
farings.rs

#[rocket::async_trait]
impl Fairing for LoggingMiddleWare {fn info(&self) -> Info {Info {name: "Error",kind: Kind::Response,}}async fn on_response<'r>(&self, _req: &'r Request<'_>, res: &mut Response<'r>) {info!("response:{:?}", res);}
}

在main.rs文件中,初始化logger,并将logger添加到rocket管道中。
main.rs

#[launch]
fn rocket() -> _ {CombinedLogger::init(vec![TermLogger::new(LevelFilter::Info,Config::default(),TerminalMode::Mixed,ColorChoice::Auto,),WriteLogger::new(LevelFilter::Info,Config::default(),File::create("error.log").unwrap(),),]).unwrap();...rocket::build().attach(LoggingMiddleWare).mount("/", all_routes)
}

总结

好了,关于构建Restful服务的项目实战暂时就到这里了。从实战的角度,我们讨论了项目的初始化,项目目录的创建,路由参数的处理,路由返回值处理和日志处理。但是从实战的角度,要讨论的内容却远不止于此。后面,我会结合项目的进展和个人的经验,再和大家分享更多的内容。希望能够得到大家的反馈和支持。

http://www.lryc.cn/news/182647.html

相关文章:

  • 苹果签名有多少种类之TF签名(TestFlight签名)是什么?优势是什么?什么场合需要应用到?
  • 如何将图片存到数据库(以mysql为例), 使用ORM Bee更加简单
  • 【“栈、队列”的应用】408数据结构代码
  • es的nested查询
  • <一>Qt斗地主游戏开发:开发环境搭建--VS2019+Qt5.15.2
  • python:进度条的使用(tqdm)
  • Java类型转换和类型提升
  • C# 读取 Excel xlsx 文件,显示在 DataGridView 中
  • Docker02基本管理
  • Scala第十章
  • 10.4 校招 实习 内推 面经
  • 从0开始深入理解并发、线程与等待通知机制(中)
  • UE5报错及解决办法
  • 怎么通过docker/portainer部署vue项目
  • 【面试经典150 | 矩阵】旋转图像
  • 机器人制作开源方案 | 家庭清扫拾物机器人
  • C++算法 —— 动态规划(8)01背包问题
  • ASUS华硕天选4笔记本FA507NU7735H_4050原装出厂Win11系统
  • 金蝶OA server_file 目录遍历漏洞
  • read_image错误
  • 文本分词排序
  • SQL与关系数据库基本操作
  • 【2023年11月第四版教材】第18章《项目绩效域》(第一部分)
  • Docker启动Mysql
  • QScrollArea样式
  • 【gitlab】git push -u origin master 报403
  • 第二篇:矩阵的翻转JavaScript
  • 代码随想录算法训练营第五十七天 | 动态规划 part 15 | 392.判断子序列、115.不同的子序列
  • 【国漫逆袭】人气榜,小医仙首次上榜,霍雨浩排名飙升,不良人热度下降
  • 国庆中秋特辑(七)Java软件工程师常见20道编程面试题