RustWeb开发入门

前言

学学用Rocket框架搞一搞web开发。


Optional和Result

先补一下之前漏掉的重要内容,Rust的错误处理。

主动报错panic

相当于PHP下面的exit函数,调用之后会打印错误信息,然后退出程序:

1
2
3
4
5
6
7
8
9
10
fn main() {
for n in 1..=100 {
match n {
1 => println!("One!!!0x{:x}", n),
2 | 4 | 6 | 8 => println!("Two!!!0x{:x}", n),
20..=30 => println!("Three!!!0x{:x}", n),
_ => panic!("No!"),
}
}
}

结果:

1
2
3
4
5
PS D:\Code\Rust\study> .\index.exe
One!!!0x1
Two!!!0x2
thread 'main' panicked at .\index.rs:9:18:
No!

空值检查Optional

大部分语言以空值为对象调用函数都会发生一个空指针错误,在Java语言下一般会事先通过判断值是否为null的方式(如!=或者Optional)来规避这种错误,而在Rust语言下,这种情况可以用Option加上match匹配的方式来解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn get_data(i: u8) -> Option<String>{
if i < 3 {
Some(i.to_string())
}else {
None
}
}

fn check(data: Option<String>) {
match data {
Some(inner) => println!("Number: {}.", inner),
None => println!("None."),
}
}

fn main() {
for n in 1..=3 {
let res: Option<String> = get_data(n);
check(res);
}
}

Option里面只有两种类型,代表有数值的Some和代表空值的None。

错误检查Result

除了空指针问题,代码运行时还经常会遇到错误,比如字符串转换为数字的格式错误,此时就可以用Result:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::num::ParseIntError;

fn get_number(s: &str) -> Result<i32, ParseIntError> {
let n = match s.parse::<i32>() {
Ok(n) => n,
Err(e) => return Err(e),
};
Ok(n + 1)
}

fn print(res: Result<i32, ParseIntError>) {
match res {
Ok(n) => println!("n is {}", n),
Err(e) => println!("Error: {}", e),
}
}

fn main() {
print(get_number("01234"));
print(get_number("no"));
}

Result里面同样也只有两种类型,代表执行正常的Ok和代表执行异常的Err,如果执行异常,可以在match中使用return语句提前跳出函数返回结果。

?

也是提前返回的功能,遇到问题时会返回一个Err:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::num::ParseIntError;

fn get_number(s1: &str, s2: &str) -> Result<i32, ParseIntError> {
let n1 = s1.parse::<i32>()?;
let n2 = s2.parse::<i32>()?;
Ok(n1 + n2)
}

fn print(res: Result<i32, ParseIntError>) {
match res {
Ok(n) => println!("n is {}", n),
Err(e) => println!("Error: {}", e),
}
}

fn main() {
print(get_number("01234", "43210"));
print(get_number("no", "1"));
}

环境搭建

开始学习Rocker Web开发,首先要用包管理工具cargo创建一个新的项目:

1
cargo new rocket-web --bin

注意项目名不要与rocket一致,然后安装rocket依赖:

1
cargo add rocket

Hello World

直接复制黏贴到main.rs里面测试一下:

1
2
3
4
5
6
7
8
9
10
11
#[macro_use] extern crate rocket;

#[get("/")]
fn index() -> &'static str {
"Hello, world!"
}

#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![index])
}

然后编译:

1
cargo build

第一次编译要把依赖都编译了,还挺久的,然后运行:

1
.\target\debug\rocket-web.exe

或者用cargo run命令也可以,Hello World运行成功,还有一些日志记录:

1
2
3
4
5
6
7
8
GET / text/html:
>> Matched: (index) GET /
>> Outcome: Success(200 OK)
>> Response succeeded.
GET /favicon.ico image/avif:
>> No matching routes for GET /favicon.ico image/avif.
>> No 404 catcher registered. Using Rocket default.
>> Response succeeded.

JSON

先在项目里引入一下序列化、反序列化和JSON:

1
use rocket::serde::{Deserialize, Serialize, json::Json};

但是出现问题:

1
unresolved import `rocket::serde::json`

serde是Rust序列化、反序列化框架,根据rocket的官方文档的描述,想要使用rocket的serde框架,需要修改配置文件Cargo.toml,将dependencies下面的内容改为:

1
2
3
rocket = { version = "0.5.1", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

然后再次build就没问题了,根据官方文档,features为条件编译,可选择项目需要的依赖。

解决了依赖引入问题,然后定义一个结构体和一个接收JSON格式数据的路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#[macro_use] extern crate rocket;
use rocket::serde::{Deserialize, Serialize, json::Json};

#[derive(Debug, Deserialize, Serialize)]
#[serde(crate = "rocket::serde")]
struct User<'a> {
uid: u8,
username: &'a str,
password: &'a str,
login_status: bool
}

#[get("/")]
fn index() -> &'static str {
"Hello, world!"
}

#[post("/login", data = "<user>")]
fn login(user: Json<User>) -> Json<User> {
println!("Received user: {:?}", user);
Json(User {
uid: user.uid,
username: user.username,
password: user.password,
login_status: user.login_status,
})
}

#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![index])
.mount("/", routes![login])
}

接受一个JSON数据,然后原样返回。

Cookies

有登录自然就要有保存登陆状态的功能,在Rocket框架中可以靠加密cookie的方式来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#[macro_use] extern crate rocket;
use rocket::serde::{Deserialize, Serialize, json::Json};
use rocket::http::CookieJar;
use serde_json;

#[derive(Debug, Deserialize, Serialize)]
#[serde(crate = "rocket::serde")]
struct User<'a> {
uid: u8,
username: &'a str,
password: &'a str,
status: bool
}

#[get("/")]
fn index(cookies: &CookieJar<>) -> Json<bool> {
let login_status: bool;
login_status = match cookies.get_private("user") {
Some(cookie) => match serde_json::from_str::<User>(cookie.value()) {
Ok(user) => user.username == "admin",
Err(_) => false
},
None => false
};
Json(login_status)
}

#[post("/login", data = "<input_user>")]
fn login<'a>(cookies: &CookieJar<'a>, input_user: Json<User<'a>>) -> Json<User<'a>> {
println!("Received user: {:?}", input_user);
let user = User {
uid: input_user.uid,
username: input_user.username,
password: input_user.password,
status: true,
};
match serde_json::to_string(&user) {
Ok(json_str) => cookies.add_private(("user", json_str)),
Err(e) => println!("Error: {}", e),
}
Json(user)
}

#[get("/logout")]
fn logout<'a>(cookies: &CookieJar<'_>) -> Json<bool> {
cookies.remove_private("user");
Json(true)
}

#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![index])
.mount("/", routes![login])
.mount("/", routes![logout])
}

文件上传

他的FileName总是不带后缀名,麻烦:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#[post("/upload", data = "<upload_file>")]
async fn upload<'a>(cookies: &CookieJar<'a>, upload_file: Form<Upload<'a>>) -> Status {
if !check_login(cookies) {
return Status::Unauthorized
}
let upload_struct: Upload<'a>= upload_file.into_inner();
let mut temp_file: TempFile = upload_struct.file;
let saved_name: &str;
match temp_file.raw_name() {
Some(file_name) => saved_name = file_name.dangerous_unsafe_unsanitized_raw().as_str(),
None => return Status::InternalServerError
};
match temp_file.persist_to(Path::new("C:/test").join(saved_name)).await {
Ok(_) => println!("Upload Successfully"),
Err(e) => println!("Error: {}", e),
};
println!("Title: {}", upload_struct.category);
Status::Accepted
}

文件下载

由于需要输出文件,所以返回类型不太一样:

1
2
3
4
5
6
7
8
#[post("/download", data = "<download>")]
async fn download<'a>(cookies: &CookieJar<'a>, download: Json<Download<'a>>) -> Result<NamedFile, Status> {
if !check_login(cookies) {
return Err(Status::Unauthorized)
}
println!("Received download request: {:?}", download);
NamedFile::open(Path::new("C:/test").join(download.category).join(download.file_name)).await.map_err(|_| Status::NotFound)
}

官方文档-错误管理

官方文档-cargo

Rust 学习之 Some、Option、Result

Rust Rocket简单入门

Rocket JSON

官方文档-features


RustWeb开发入门
http://yoursite.com/2024/10/15/RustWeb开发入门/
作者
Aluvion
发布于
2024年10月15日
许可协议