Rust 代码挑战系列 1 - 使用 axum 编写 http 服务器
这是一个学习 Rust 的小系列文章,通过完成来自 shuttle 平台所举办的 2023 Christmas Code Hunt 里的每个小挑战,来学习 rust web 框架的使用。
你可以选择任意的 web 框架来完成挑战,笔者选用的是之前文章中提及的 axum 框架实现 http 服务,版本选用目前最新的 0.7,rust 使用的是 1.74.0 版本。
在每个任务中,主要记录了笔者的实现思路和代码改进过程,而非直接给出最终结果,旨在更多的了解 rust 和 axum 的使用。 为了精简内容,笔者不会对问题做过多描述,想要了解完整描述可自行查看对应问题的原页面。
在开始前,若对 axum
不熟悉,建议先阅读其文档,以便对该框架有一些基本认识。
本篇内涉及了 -1、1、4 这三部分的挑战内容。
Part -1
-1 是一个热身部分,用来搭建一下开发环境并实现一个 hello world 请求的处理。
Task 1
# 输入输出示例
curl -I -X GET http://localhost:8000/
HTTP/1.1 200 OK
...
我们先来自行初始化一个项目:
# 新建项目
cargo new cch2023
添加依赖,编辑 Cargo.toml
:
[dependencies]
axum = "0.7.2"
tokio = { version = "1.35.1", features = ["full"] }
更新 src/main.rs
:
use axum::{routing::get, Router};
#[tokio::main]
async fn main() {
let router = Router::new().route("/", get(demo));
let listener = tokio::net::TcpListener::bind("127.0.0.1:8000")
.await
.unwrap();
axum::serve(listener, router).await.unwrap();
}
async fn demo() -> &'static str {
"hi"
}
使用 cargo run
运行程序,然后发一个请求测试一下(无法使用 curl
的话,可以在 vscode 里安装 Thunder Client 插件发送请求):
curl http://localhost:8000
# hi%
Task 2
# 输入输出示例
curl -I -X GET http://localhost:8000/-1/error
HTTP/1.1 500 Internal Server Error
...
在 axum
里返回错误码很简单,直接返回对应的状态码即可:
use axum::{http::StatusCode};
async fn main() {
let router = Router::new()
.route("/-1/error", get(mock_error));
// ...
}
async fn mock_error() -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}
那么 -1 部分的热身任务就完成了。
Part 1
正式开始挑战啦。
Task 1
寒冷的北极大陆上,圣诞老人用来派发礼物的包包管理系统出现了点故障,我们需要帮助他进行校准。
实现一个 GET 接口 /1/<num1>/<num2>
,返回 (num1 XOR num2) POW 3
的结果,其中 XOR
是异或运算,POW
是指数运算。
# 输入输出示例
curl http://localhost:8000/1/4/8
1728
在 axum
的 Router
文档里可以看到所有支持的路径写法,我们将使用 /:num1/:num2
:
use axum::{extract::Path};
async fn main() {
let router = Router::new()
.route("/1/:num1/:num2", get(d1));
// ...
}
async fn d1(path: Path<(i32, i32)>) -> String {
(path.0 .0 ^ path.0 .1).pow(3).to_string()
}
上述中使用 Path
extractor 来提取路径中的参数,然后进行数学运算,返回字符串内容即可。
Task 2
包包系统校准好了,现在需要再校准下雪橇管理系统。现在 <num>
不只有两个,将这些 <num>
和上述一样进行异或运算,然后进行指数运算。
# 输入输出示例 1
curl http://localhost:8000/1/10
1000
## 输入输出示例 2
curl http://localhost:8000/1/4/5/8/10
27
这个任务里,路径的参数个数是不固定的,axum
则为我们提供了 wildcards 写法:/assets/*path
,来看看怎么使用,:
async fn main() {
let router = Router::new()
// 该路径会包含前面的 /1/:num1/:num2,故此处将其替换,路径冲突会报错
.route("/1/:nums", get(d1));
// ...
}
async fn d1(Path(nums): Path<String>) -> String {
let nums = nums
.split('/')
.filter(|x| !x.is_empty())
.map(|x| i32::from_str_radix(x, 10).unwrap())
.collect::<Vec<_>>();
let mut res = 0;
for num in nums {
res = res ^ num;
}
res = res.pow(3);
res.to_string()
}
上述中当我们使用了通配符后,便需要自行解析路径参数,将参数都转为数字然后进行计算返回。
小知识 - Turbofish
上述代码的 xx.collect::<Vec<_>>()
使用到了一种叫做 turbofish 的写法 ::<T>
,用来协助编译器进行类型推断。我们看下面的例子,为何这里 rust 不能自动推断出目标类型是个 Vec<&str>
,而是编译报错 “type annotations needed”:
fn col_demo(data: &'static str) {
let items = data.split('/').collect(); // compiler error
}
写成下面的某一种形式就正常了:
let items = data.split('/').collect::<Vec<_>>();
let items: Vec<_> = data.split('/').collect();
通过查看 collect
的函数签名可知其返回值是一个实现了 FromIterator
trait 的类型,而从 FromIterator 的文档可知有很多类型都实现了这个 trait,比如 Vec
、VecDeque
,因此如果不手动标注具体类型,编译器无法知道我们需要的是哪个类型:
let items = data.split('/').collect::<Vec<_>>(); // ok
let items = data.split('/').collect::<VecDeque<_>>(); // ok
其中 Vec
元素的类型使用了 _
表示让编译器自动推断,不需要手动指定,是因为此处的类型编译器是可以自动推断的。
回到 Task 2 中,我们的代码可以进一步改进,将其中的 for 循环使用函数风格替换掉,可以一条语句写完整个逻辑了:
async fn d1(Path(nums): Path<String>) -> String {
nums.split('/')
.filter(|x| !x.is_empty())
.map(|x| i32::from_str_radix(x, 10).unwrap())
.fold(0, |acc, cur| acc ^ cur)
.pow(3)
.to_string()
}
这一部分的问题解决让我们了解到 rust 的 iterator 的多种工具函数,它可以几乎代替 for 循环的写法,具体可以查阅 iterator 文档查看所有的函数。
Part 4
怎么从 Part 1 突然就到 Part 4 了呢?因为 shuttle 的挑战是基于日期的,而非递增序号哦。
在柔和的北极光的照耀下,圣诞老人正在训练他的驯鹿团队,一只顽皮的小精灵不小心把绿茶打翻洒在了计算机上,驯鹿们的数据错乱了,现在我们需要帮助恢复这些数据。
Task 1
计算出某一组驯鹿组合的综合力量值:
# 输入输出示例
curl -X POST http://localhost:8000/4/strength \
-H 'Content-Type: application/json' \
-d '[
{ "name": "Dasher", "strength": 5 },
{ "name": "Dancer", "strength": 6 },
{ "name": "Prancer", "strength": 4 },
{ "name": "Vixen", "strength": 7 }
]'
22
该任务里涉及到了 json 格式的请求体的处理,新增一个路由:
Router::new().route("/4/strength", post(d4_1));
实现路由函数:
use axum::Json;
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct Reindeer {
name: String,
strength: i32,
}
async fn d4_1(Json(body): Json<Vec<Reindeer>>) -> String {
// 使用 iterator 函数式编程写法
body.iter().map(|x| x.strength).sum::<i32>().to_string()
}
上述中用到了 serde
包处理序列化,我们需要安装它并开启 derive
特性:
cargo add serde --features derive
小提问
在上述的 Reindeer
struct 定义中,能否将 name
定义为 &'static str
而非 String
呢?
不行,原因可从 Json<T>
的 trait 实现看出,T
需要满足 DeserializeOwned
,该 trait 要求在反序列化时不会借用到外部数据,使用 &'static str
则会借用到数据引用。
Task 2
现在每个驯鹿有了更多的属性,我们需要返回一份统计数据:
# 输入输出示例
curl -X POST http://localhost:8000/4/contest \
-H 'Content-Type: application/json' \
-d '[
{
"name": "Dasher",
"strength": 5,
"speed": 50.4,
"height": 80,
"antler_width": 36,
"snow_magic_power": 9001,
"favorite_food": "hay",
"cAnD13s_3ATeN-yesT3rdAy": 2
},
{
"name": "Dancer",
"strength": 6,
"speed": 48.2,
"height": 65,
"antler_width": 37,
"snow_magic_power": 4004,
"favorite_food": "grass",
"cAnD13s_3ATeN-yesT3rdAy": 5
}
]'
{
"fastest": "Speeding past the finish line with a strength of 5 is Dasher",
"tallest": "Dasher is standing tall with his 36 cm wide antlers",
"magician": "Dasher could blast you away with a snow magic power of 9001",
"consumer": "Dancer ate lots of candies, but also some grass"
}
这个任务里,我们要根据响应格式来返回内容,其中在计算 "consumer"
里谁吃的糖果最多时用到的是 "cAnD13s_3ATeN-yesT3rdAy"
字段,这个字段名需要额外处理一下。
新增一个路由:
Router::new().route("/4/contest", post(d4_2));
实现路由函数:
#[derive(Debug, Deserialize)]
struct DetailedReindeer {
name: String,
strength: i32,
speed: f32,
height: i32,
antler_width: i32,
snow_magic_power: i32,
favorite_food: String,
// 使用 rename 来“修正”奇怪的命名。更多属性可查阅文档 https://serde.rs/attributes.html
#[serde(rename(deserialize = "cAnD13s_3ATeN-yesT3rdAy"))]
candy: i32,
}
#[derive(Debug, Serialize)]
struct ReindeerResponse {
fastest: String,
tallest: String,
magician: String,
consumer: String,
}
async fn d4_2(Json(body): Json<Vec<DetailedReindeer>>) -> Json<ReindeerResponse> {
let fastest = body.iter().max_by(|a, b| a.speed.total_cmp(&b.speed));
let tallest = body.iter().max_by_key(|a| a.height);
let magician = body.iter().max_by_key(|a| a.snow_magic_power);
let consumer = body.iter().max_by_key(|a| a.candy);
let res = ReindeerResponse {
fastest: format!(
"Speeding past the finish line with a strength of {} is {}",
fastest.unwrap().strength,
fastest.unwrap().name
),
tallest: format!(
"{} is standing tall with his {} cm wide antlers",
tallest.unwrap().name,
tallest.unwrap().antler_width
),
magician: format!(
"{} could blast you away with a snow magic power of {}",
magician.unwrap().name,
magician.unwrap().snow_magic_power
),
consumer: format!(
"{} ate lots of candies, but also some {}",
consumer.unwrap().name,
consumer.unwrap().favorite_food
),
};
return Json::from(res);
}
在查找最大值时,我们使用了 iterator.max_by_key()
,其中查找 fastest
使用的是 iterator.max_by()
,因为 max_by_key
要求返回的元素内容满足 Ord
trait,rust 中的 integer 类型都实现了该 trait,但是 float 类型(f32、f64)并未实现,似乎是因为在 IEEE 754 二进制浮点数运算标准里,浮点数还包括了 -0/+0、Infinity、NaN 等一些特殊值,而 integer 并没有,感兴趣可与在此帖子内了解更多。
小结
本节完,若要有所收获,还需自己动起手来先自己尝试实现哦。