Rust 代码挑战系列 4 - 如何发送 Http 请求、如何处理表单数据
这是一个学习 Rust 的小系列文章,通过完成来自 shuttle 平台所举办的 2023 Christmas Code Hunt 里的每个小挑战,来学习 rust web 框架的使用,此为第四篇文章。
虽然 2023 的节日已过,没了节日氛围带来的沉浸感,但还是要有继续下去的动力哦。
本篇内的 Part 8 涉及 http 请求的发送,Part 11 涉及表单数据的处理。
前言
在使用 cch23-validator
测试自己的代码时,怎样可以看到请求体内容,来方便自行测试呢?
一个法子是添加一个中间件(middleware)来打印请求信息,另一个法子比较简单直接,就是看 cch23-validator
的源码,读者们可以自行进入源码查看所需任务的请求体。
Part 8
宝可梦(Pokémon)来到了圣诞老人的世界,作为一个创新者(innovator),圣诞老人想要抓住机会,让宝可梦们加入到送礼活动中。
Task 1
圣诞老人需要知道宝可梦的体重,以便合理地将他们带上雪橇。
# 示例输入输出
curl http://localhost:8000/8/weight/25
6
该任务中,我们需要请求宝可梦数据库接口获取对应 id 的宝可梦信息,现在看看如何发送请求,新增一个路由函数:
Router.route("/8/weight/:id", get(handler::d8_1))
路由函数实现:
#[derive(Deserialize)]
pub struct PokeWeight {
weight: f32,
}
pub async fn d8_1(Path(id): Path<f32>) -> String {
let body = reqwest::get(format!("https://pokeapi.co/api/v2/pokemon/{id}"))
.await
.unwrap()
.json::<PokeWeight>()
.await
.unwrap();
(body.weight / 10.0).to_string()
}
上述我们使用到了 reqwest
这个 crate 来发送 http 请求,因此需要安装它,并要开启 json
特性:
cargo add reqwest --features json
Task 2
知道了神奇宝贝的重量,圣诞老人需要我们计算它从 10 米高的烟囱(chimney)上掉下来时与地板碰撞产生的动量(momentum)(这样他就知道自己是需要爬下来还是可以把它丢下来),重力加速度是 g = 9.825 m/s²
,忽略空气阻力。
# 示例输入输出
curl http://localhost:8000/8/drop/25
84.10707461325713
新增路由函数:
Router.route("/8/drop/:id", get(handler::d8_2))
路由函数实现:
pub async fn d8_2(Path(id): Path<i32>) -> String {
let body = reqwest::get(format!("https://pokeapi.co/api/v2/pokemon/{id}"))
.await
.unwrap()
.json::<PokeWeight>()
.await
.unwrap();
// p = mv
// v = 根号(2gh)
let momentum: f32 = (body.weight / 10.0) * (2.0f32 * 9.825 * 10.0).sqrt();
momentum.to_string()
}
这个任务的最大最大难点,对于一个物理知识遗忘,各种公式遗忘的人来说,就是如何计算这个动量了,好在 gpt 的存在,笔者询问了它已知重量和高度求动量,勉强得到了计算公式。
Part 11
精灵们已经厌倦了只处理字符串、数字和字节,现在需要在电脑屏幕上放一些花哨的圣诞饰品(christmas ornaments)。
Task 1
请求某张图片并返回这张图:
# 示例输入输出
curl -I -X GET http://localhost:8000/11/assets/decoration.png
HTTP/1.1 200 OK
content-type: image/png
content-length: 787297
...
这个任务涉及到了静态文件的处理,我们会想到使用一个中间件来实现这个功能,那么 axum 里有对应的中间件吗?
在此之前,我们需要对 axum 的构成有一些了解,axum 的路由、中间件系统等都是使用了外部 crate,其中的中间件相关则使用了 tower 这个库。
tower 简单介绍起来就是:提供模块化和可复用的组件,用于构建健壮的网络服务端和客户端,它是协议无关的(protocol agnostic),基于"请求-响应"模式。
tower_http 则提供了和 http 相关的一些工具,其包含了很多用于构建 http 服务端和客户端的中间件,比如 Trace 日志追踪、跨域、限流等中间件,另外需要知道的是 tower_http 本身并没有网络请求能力,需要搭配满足特定条件的包使用。
简单了解后,现在看看 tower_http 所提供的静态文件托管中间件是如何使用的吧:
这次不是新增路由,而是新增一个 service:
use tower_http::services::ServeDir;
Router.nest_service("/11/assets", ServeDir::new("assets"))
我们需要安装 tower_http
并开启 fs
特性:
cargo add tower-http --features fs
上述中提到了 "service" 这个名词,这是来自 tower 的一个核心概念,感兴趣可阅读其文档深入了解。
我们使用了 "assets" 目录,所以需要再项目根目录下新增该目录,然后将图片文件放到这个目录下便可,本任务所需要的图片链接是 https://cch23.shuttleapp.rs/assets/decoration.png,可自行下载。
Task 2
# 示例输入输出
curl -X POST http://localhost:8000/11/red_pixels \
-H 'Content-Type: multipart/form-data' \
-F 'image=@decoration.png' # the image from Task 1
73034
这个任务中,我们接收一张图片,需要返回该图片中 "magical red" 的数量,其中当一个像素颜色满足 red > blue + green 时,便认为这是一个 magical red。
新增一个路由函数:
Router.route("/11/red_pixels", post(handler::d11_2))
该需求中我们需要处理到 multipart/form-data
类型的数据,先看下路由函数的最终实现:
use axum_extra::extract::Multipart;
use image::{EncodableLayout, GenericImageView};
pub async fn d11_2(mut form: Multipart) -> String {
while let Some(field) = form.next_field().await.unwrap() {
if field.name().unwrap() == "image" {
let data = field.bytes().await.unwrap();
let img = image::load_from_memory(&data.as_bytes()).unwrap();
let sum = img
.pixels()
.filter(|(_, _, color)| {
// red > blue + green
// WARNING: color[i] 是个 u8,不能直接两者相加相减,会导致运行时报错 attempt to add/subtract with overflow
return color[0] as u16 > (color[1] as u16 + color[2] as u16);
})
.count();
return sum.to_string();
}
}
return "0".to_string();
}
我们使用 Multipart
extractor 来提取表单数据,使用它需要开启 axum-extra
的 multipart
feature,请编辑 Cargo.toml
:
axum-extra = { version = "0.9.0", features = ["cookie", "multipart"] }
在读取图片像素信息时使用了 image
crate,需要安装一下:
cargo add image
在读取像素值时,代码中的 WARNING 注释里提到了,我们需要将类型转为 u16
或更大范围的值,而不能使用默认返回的 u8
,因为 u8
最大值是 255
,两个相加自然会有概率大于 255,不要忘了我们使用的是一个强类型语言!
结尾
我们似乎已经安装了很多 crate,和 javascript 的包使用不同,笔者总感觉使用 rust 的每个 crate 都是一次挑战,使用起来总不是那么的简单直观,这或许就是强类型语言和 rust 的语言特性所带来的吧,但还是很有趣的。