Rust 代码挑战系列 - 4

这是一个学习 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-extramultipart 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 的语言特性所带来的吧,但还是很有趣的。