前言#
虽然说rust写web没有什么优势,但是来都来了,那顺便逛一下比较出名的Rust Web框架吧~
提前准备#
rust基础知识:坏蛋Dan:rust基础学习--final: 章节索引rust异步基础知识: 坏蛋Dan:rust基础学习索引--异步tokio简单入门:坏蛋Dan:rust基础学习--tokio入门
Rocket#
简介#

一个异步的web框架
话不多说,直接上手
hello world#
在开始之前,我们需要更新下我们当前rustup版本
rustup default stable 然后创建我们的项目
mkdir rocket_study
cd rocket_study
cargo init hello-rocket然后配置下根目录的Cargo.toml
[workspace]
members = [
"hello-rocket"
] 然后打开hello-rocket文件夹里的Cargo.toml
[dependencies]
rocket = "=0.5.0-rc.3"然后回到main.rs文件中
#[macro_use] extern crate rocket;
#[get("/")]
fn index() -> &'static str {
"Hello, world!"
}
#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![index])
} #[get("/")]这个是声明宏,别给它外表欺骗了,用于注册对应的路由。build:创建服务示例。mount:注册基础请求路径以及路由。routes!:是个属性宏,这个应该是用来搭配get这个声明宏的,用于建立联系。#[macro_use]:这个主要是显性的将rocket里的宏全部导入,这样我们就可以在全局任意地方使用rocket的宏了。
这样就可以跑了,是的,你没看错,main函数都被干没了,主要是被宏直接隐藏了。
然后直接cargo run -p hello-rocket

预览#
生命周期#
Rocket主任务是监听请求,然后分配请求给你实现的处理器,返回响应给客户端。
这里将监听到返回响应体的过程叫做生命周期。大体上可以分为以下四部分:
Routing:解析一个HTTP请求并转换成我们可以间接操控的原生结构。然后通过我们定义的路由属性匹配对应的处理器(handler)来执行Validation:在处理器执行之前,会先验证处理器所需要的类型是否符合,如果检验失败,Rocket会继续往下找到下一个匹配到的处理器,如果找不到直接进入错误处理器(error handler)。Processing:处理器里面执行的逻辑就是我们开发者主要实现的部分,以返回一个Response为结束标志。Response:将process返回的Response整理一下发送给客户端。
Routing#
Rocket应用的中心是围绕路由(routes)和处理器(handlers)的。一个路由由以下两部分组成:
- 用于匹配请求的一系列参数
- 一个用于处理请求和返回响应体的处理器
而处理器指的是一个简单的函数可以接收任意的参数和返回任意的响应。
参数可以匹配到的包含静态路径、动态路径、路径片段(segments)、表单、查询语句(query strings)、请求格式说明符(request format specifiers)、请求体数据(body data)。
Rocket将这些定义为“属性”,写法和其他语言中的装饰器类似。比如下面这样:
#[get("/world")] // <- route attribute
fn world() -> &'static str { // <- request handler
"hello, world!"
}一个路由属性搭配上一个路由处理器
其中#[get]指的是一个get类型的请求,/world则是静态路径。除了get,自然还有post等,比如#[post]、#[put]或者#[catch]用于服务自定义错误页面。我们会在之后的章节里更深入的了解到。
Mounting#
在派发请求之前,需要先找到派发的对象。这就需要我们去注册或者说是挂载对应的路由。
rocket::build()
.mount("/hello", routes![world])
.mount("/hi", routes![world]);/hello:请求匹配到的路径的基础路径(base path),比如我们注册了world(routes![world]),那么这个时候请求路径就得是/hello/world才能识别到对应的处理器。routes!:前面说过这是一个宏,后面接入一组函数名,比如routes![fn_a, fn_b, fn_c]。build:创建一个新的Rocket实例。
同一个路由可以在不同基础路径下注册。比如这里的/hello/world以及/hi/world。
Launching#
不过在开始创建实例之前,我们还需要launch,launch的作用是创建一个多线程异步服务器以及派发进来的请求给路由。
launch有两种方式:
第一个就是我们刚看到的#[launch],编译阶段它会展开成一个main函数搭配上异步的runtime然后开启服务。另外我们还需要提供返回的类型,一般设置_就行了,它会自动推断,如果你想,你也可以手动返回Rocket<Build>。
#[macro_use] extern crate rocket;
#[get("/world")]
fn world() -> &'static str {
"Hello, world!"
}
#[launch]
fn rocket() -> _ {
rocket::build().mount("/hello", routes![world])
}第二种方式就是使用#[rocket::main]路由属性。和#[launch]不同的是它允许我们自行开启服务。比如下面这样:
#[rocket::main]
async fn main() -> Result<(), rocket::Error> {
let _rocket = rocket::build()
.mount("/hello", routes![world])
.launch()
.await?;
Ok(())
}你可以用来获取launch的future,在这个过程中处理一些其它的事务。又或者你需要判断launch之后的错误场景做特殊处理,那么这个时候也是非常有用的。
Future and Async#
rocket使用Future来处理并发(concurrency)问题。一般情况下我们是更喜欢使用异步库而不是同步的。
Rocket中存在异步的场景如下:
- 路由和错误捕获器(
Error Catchers)可以是异步的函数,那么在这里面我们就可以等待第三方库比如tokio等或者直接使用Rocket给的Future。 - 部分
Rocket的traits,比如FromData和FromRequest都有返回Future的方法。 Data和DataStream、进来的请求携带的数据、Response以及Body、传输给客户端的响应体携带的数据都是基于tokio::io::AsyncRead的,而不是std::io::Read。
需要注意的是Rocket v0.5使用了tokio runtime,我们通过#[launch]或者#[rocket::main]来开启的服务都是tokio runtime的,当然你也可以自定义。
异步路由
来看个例子
use rocket::tokio::time::{sleep, Duration};
#[get("/delay/<seconds>")]
async fn delay(seconds: u64) -> String {
sleep(Duration::from_secs(seconds)).await;
format!("Waited for {} seconds", seconds)
}这里使用了tokio::time::sleep,是一个异步函数,所以需要.await。
Multitasking
Rust的Future是一组协作式多任务处理,一般情况下我们不应该去阻塞异步函数。不过我们会遇到一些万不得已的场景,比如相关api只有同步的,这种时候推荐使用tokio::task::spawn_blocking也就是提供一个wrapper包裹原来的方法。
use std::io;
use rocket::tokio::task::spawn_blocking;
#[get("/blocking_task")]
async fn blocking_task() -> io::Result<Vec<u8>> {
// In a real app, use rocket::fs::NamedFile or tokio::fs::File.
let vec = spawn_blocking(|| std::fs::read("data.txt")).await
.map_err(|e| io::Error::new(io::ErrorKind::Interrupted, e))??;
Ok(vec)
}Requests#
Methods#
我们前面已经接触过get了,实际上目前可以支持几乎所有的请求类型,比如post、put、delete、head、patch、options等。具体可以看这里:route in rocket - Rust
HEAD Requests
当你的get请求中带有Head,Rocket会自动处理HEAD请求。当然,你也可以自定义一个HEAD的路由,Rocket并不会干扰程序显性的(explicitly)处理HEAD请求。
Reinterpreting
由于html那边定义了表单提交类型只支持GET和POST请求,Rocket“不得已”重新定义了部分场景下的请求类型,比如POST请求的Content-Type为application/x-www-form-urlencoded,并且form的第一个字段带有_method以及值是一个符合标准HTTP请求的比如PUT,那么这个请求类型就会变成对应_method值的类型。官方给的例子中就有用到这一点(不过恕我吐槽,还隔这传统SSR呢。。。):https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/examples/todo/static/index.html.tera#L47
Dynamic Path#
动态路径比较常见的场景是动态id场景,比如:
#[get("/hello/<name>")]
fn hello(name: &str) -> String {
format!("Hello, {}!", name)
}这个路由会匹配所有/hello/为基础路径的路由,然后将它匹配到的动态路径作为参数传递给处理器。
语法是<${xxx}>。
你可以传N个动态类型即你的动态路径有多层,只要你这个类型实现了FromParam[11]这个trait即可,他们管这叫做:参数保护,其实也就是trait bound。。
比如:
#[get("/hello/<name>/<age>/<cool>")]
fn hello(name: &str, age: u8, cool: bool) -> String {
if cool {
format!("You're a cool {} year old, {}!", age, name)
} else {
format!("{}, we need to talk about your coolness.", name)
}
}Rocket默认给标准库里的一些常见类型以及Rocket自身的一些特殊类型实现了这个trait,具体可以看这里:https://api.rocket.rs/v0.5-rc/rocket/request/trait.FromParam.html
多个片段(segments)
你也可以通过<param..>的方式来匹配多个动态路径,这种类型的参数一般被叫做分段防护装置(segments guards),都必须先实现FromSegments这个trait。这个参数必须放到路径的最后面,不然会导致编译错误(可以参考python的*params和**params参数匹配)
来看个例子:
use std::path::PathBuf;
#[get("/page/<path..>")]
fn get_page(path: PathBuf) { /* ... */ }PathBuf实现了FromSegments这个trait,所以你不用担心/page或者/page//导致的解析失败,另外因为实现了FromSegments,我们也不用担心路径遍历攻击(path traversal attacks)。基于这一点,我们可以简单的实现一个安全的静态文件服务器:
use std::path::{Path, PathBuf};
use rocket::fs::NamedFile;
#[get("/<file..>")]
async fn files(file: PathBuf) -> Option<NamedFile> {
NamedFile::open(Path::new("static/").join(file)).await.ok()
}PS:如果你真的想要搞一个静态文件服务器,你可以使用FileServer,只需一行代码即可:
rocket.mount("/public", FileServer::from("static/"))Ignored Segments
如果你的路径是动态的,但是你又不想要其中几个参数,那么这个时候你可以直接使用_占位符<_>占位即可,如果是多个可以使用<_..>。
比如:
#[get("/foo/<_>/bar")]
fn foo_bar() -> &'static str {
"Foo _____ bar!"
}
#[get("/<_..>")]
fn everything() -> &'static str {
"Hey, you're here."
}Forwarding#
回到我们前面匹配多个动态路径的路由例子:
#[get("/hello/<name>/<age>/<cool>")]
fn hello(name: &str, age: u8, cool: bool) { /* ... */ }如果这里的cool不是一个布尔值呢?又或者age不是u8呢?我们前面提到的生命周期的里有一个validation,这个时候就是invalid,那么就不会走这个handler,而是继续往下匹配,直到遇到了下一个匹配的上的或者找到最后找不到了走err handler。
另外我们可以通过rank关键字调整同个路由下不同handler匹配的优先级。比如下面这个例子:
#[get("/user/<id>")]
fn user(id: usize) { /* ... */ }
#[get("/user/<id>", rank = 2)]
fn user_int(id: isize) { /* ... */ }
#[get("/user/<id>", rank = 3)]
fn user_str(id: &str) { /* ... */ }
#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![user, user_int, user_str])
}上面这个例子中首先匹配到的就是user这个处理器,如果不符合校验,接着匹配的是user_int,如果还不匹配则轮到user_str。
这个rank我们可以在终端看到,比如

Rocket的默认rank范围是-12 ~ -1之间,这一点后面会解释到。
如果你给处理器的动态参数类型设置为Result<T, E>或者Option<T>,那么这个时候这个处理器函数将不会再往下匹配,Ok拿到值,而Err则表示解析错误等。
如果你确实存在多个同名路径路由,那么这个时候这个rank是不能省略的,不然编译的时候会直接报错。

注意,这里是forward的场景下才会出现。什么意思呢?
实际上就是上面这种场景,虽然是动态的,但是路径都长得一样。
那么对于长得不一样但是也有可能相同的场景来说并且没有指定rank的情况下,这里就需要引入权重(优先权)这一点了。比如下面这个例子
#[get("/foo/<_>/bar")]
fn foo_bar() { }
#[get("/<_..>")]
fn everything() { }虽然长得不一样,但是肯定是在同一个forward中的。
那么哪个优先级高呢?
很明显是/foo/<_>/bar高,因为它比较“静”也就是static。
而_..则是完全的“动”,即dynamic。简单地说就是以/分割那边字面量多哪边优先级高一些。

这里就是我们上面提到的rank的默认范围,数越小优先级越高。而path的权重则是高于query的(这里指的是路由的不同分类)。
static:路径中全是静态的,即都是字面量,比如/a/b/cpartial:部分是动态的,比如/root/<_>/barwild:全是动态的,比如<_..>
这里简单了解下即可,正常情况下我们并不会遇到这类问题。
Request Guards#
简单地说就是类似trait bound,也就是需要实现FromRequest[13]这个trait。实现了FromRequest的类型就都叫做请求守卫。
rocket`内置给几乎所有基础类型以及部分`rocket`自己的类型实现了这个`trait来简单的看下源码和例子

可以看到实际上就是每个类型自身去实现校验的逻辑,而rocket会在执行处理器之前进行校验。
对于没有命名的类型参数来说,你可以手动指定它们为请求守卫,比如:
#[get("/<param>")]
fn index(param: isize, a: A, b: B, c: C) { /* ... */ }其中A, B, C三个类型都是请求守卫。 他们仨可以是任意类型,只要是实现了FromRequest即可。
Custom Guards
基于上面这一点,我们实际上可以实现自己想要的请求守卫,自定义校验逻辑,比如:
use rocket::request::{FromRequest, Outcome, Request};
struct ApiKey {
_type: String,
content: String,
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for ApiKey {
type Error = String;
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
if let Some(key) = request
.query_value::<Option<std::result::Result<String, rocket::form::Errors>>>("key")
.take()
{
dbg!(key);
Outcome::Success(Self {
_type: "md5".to_string(),
content: "aioscnhasdjasdjwj".to_string(),
})
} else {
Outcome::Failure((
rocket::http::Status { code: 400 },
String::from("wrong key"),
))
}
}
}
#[get("/sensitive")]
fn sensitive(key: ApiKey) -> &'static str { /* .. */
return "哈嘿嗨"
}
如果不传key就会报错

Guard Transparency
另外我们还可以基于守卫去实现访问权限的方式,rocket中如果需要实现这一块功能,那么就需要数据都通过FromRequest才行。如果某个类型是只能通过FromRequest实现创建的,那么我们就将这类型称之为"类型级证明"(type-level proof),证明当前的请求已经根据任意的策略进行了验证。而FromRequest则作为这个类型的见证者,rocket中称这一概念为守卫透明度(guard transparency)。
这一大段话就很绕了,我们来搞个具体的例子加深下理解。
fn health_records(user: &SuperUser) -> Records { /* ... */ }某个程序具有一个函数 health_records,该函数返回数据库中的所有健康记录。由于健康记录是敏感信息,因此只能由超级用户访问。超级用户请求保护对超级用户进行身份验证和授权,其 FromRequest 实现是可以构造超级用户的唯一方法。通过按如下方式声明health_records函数,可以保证在编译时防止违反健康记录的访问控制。
原因如下:
health_records函数需要&SuperUser这个trait。SuperUser的构造器只有一个FromRequest。- 只有
Rocket可以通过FromRequest来传递request。
那么这里就只能是通过FromRequest来访问request,而SuperUser又是可以唯一一个通过验证的,那么这个时候自然就只有这个SuperUser可以访问数据。
不过这里代价则是生命周期,你可以看到我们前面写的代码,一连串的生命周期参数。通过牺牲一点可维护性来强化安全问题。
Forwarding Guards
Request Guards和Forwarding可以作为一套组合拳,至于组合啥,我们来通过一个例子来知晓组合的是啥。
首先我们这里有两个Request guard:
User:一个常规的、被授权的用户,我们给它实现FromRequest用来确认用户的cookie并且当确认无误的时候返回用户的信息,如果没有用户可以授权,这个守卫将forward。AdminUser:一个用户被授权为管理员。我们给它实现FromRequest用来确认是否是超级管理员并且确认无误之后返回管理员信息。如果没有管理员可以授权,这个守卫也会forward。
use rocket::response::Redirect;
#[get("/login")]
fn login() -> Template { /* .. */ }
#[get("/admin")]
fn admin_panel(admin: AdminUser) -> &'static str {
"Hello, administrator. This is the admin panel!"
}
#[get("/admin", rank = 2)]
fn admin_panel_user(user: User) -> &'static str {
"Sorry, you must be an administrator to access this page."
}
#[get("/admin", rank = 3)]
fn admin_panel_redirect() -> Redirect {
Redirect::to(uri!(login))
}我们使用这俩组合成一套用户身份验证的逻辑,优先验证是否是管理员,次之才是用户,最后则是游客,直接跳登录页面。
这么写有函数重载那味儿了,代码会简洁非常多,不用在同一个处理器中用if去做判断,而是通过impl FromRequest去实现check。
这么一看确实优雅了很多。
Cookies#
Cookie这玩意儿在Web中是绕不开了,而Rocket自然也考虑过这一点,所以它内置了相关的请求守卫:CookieJar[15],它允许我们对cookies的增删改。既然它是一个请求守卫,那它自然是可以直接用来作为处理器参数的。
使用例子:
use rocket::http::CookieJar;
#[get("/")]
fn index(cookies: &CookieJar<'_>) -> Option<String> {
cookies.get("message").map(|crumb| format!("Message: {}", crumb.value()))
}Private Cookies
Cookie有个问题就是你发送给客户端的数据是暴露的,用户可以通过某些方法去得到对应的信息,而对于一些敏感的信息来说,被用户拿到了是非常危险的。所以自然就需要做处理,Rocket中提供了私有cookie,和其他cookie唯一的不同点就是它们是加密过的。不过Rocket默认不导入这块,我们需要手动去添加features。
## in Cargo.toml
rocket = { version = "=0.5.0-rc.3", features = ["secrets"] }这些方法的名字和之前的一样,只不过加上了_private的后缀,即get_private、set_private、add_private、remove_private等。
来看个例子:
use rocket::http::{Cookie, CookieJar};
use rocket::response::{Flash, Redirect};
/// Retrieve the user's ID, if any.
#[get("/user_id")]
fn user_id(cookies: &CookieJar<'_>) -> Option<String> {
cookies.get_private("user_id")
.map(|crumb| format!("User ID: {}", crumb.value()))
}
/// Remove the `user_id` cookie.
#[post("/logout")]
fn logout(cookies: &CookieJar<'_>) -> Flash<Redirect> {
cookies.remove_private(Cookie::named("user_id"));
Flash::success(Redirect::to("/"), "Successfully logged out.")
}Secret Key
秘钥这玩意儿相信大家都知道是干啥的,简单地说就是用于加密时的一个私有变量。Rocket中加密采用的是secret_key配置中指定的 256位的key参数。在debug模式下,这个key会自动生成,而生产环境则需要我们开发者来定义。
值也不一定是256位的base64,也有可能是hex字符串又或者32字节的切片。我们可以通过openssl等工具直接生成,我们本机连接远程服务器的方式之一就是通过它。
openssl rand -base64 32关于配置这一块,具体可以看:https://rocket.rs/v0.5-rc/guide/configuration
Format#
前面我们自己写的路由处理器获取到的数据或者返回的数据最终都变成了字符串,而真实开发中这并不是我们需要的,我们需要的是某些特定格式的,比如当header中的Content-Type为application/json时,拿到或者返回的格式我们希望是json的。
作为Web框架,Rocket自然也兼顾到了这一点。
我们可以在路由的路径参数中(从这里往后定义在#${method}[]里面的参数都叫做组件了)设置format组件来forward, 比如:
#[post("/user", format = "application/json", data = "<user>")]
fn new_user(user: User) { /* ... */ } 只有Content-Type是application/json时才会匹配成功。
format = "application/json"可以简写为format = "json",具体可见:ContentType in rocket::http - Rust。
这里有点需要注意:format组件对于支持负载(payload-supporting)的 POST/PUT/DELETE/CATCH四种method来说是验证Content-Type,而对于不支持的GET/HEAD/OPTIONS来说则是匹配Accept。
Body Data#
上面Format中的例子带有一个data = <user>的组件,实际上这个就是定义的Body Data。这个组件同样会作为处理器的参数传入处理器中。
来再看个例子:
#[post("/", data = "<input>")]
fn new(input: T) { /* .. */ }这个参数需要实现FromData[18]这个trait ,一般管这叫做数据守卫(data guard).
JSON
JSON<T>[19]可以将数据序列化成JSON结构,唯一的要求则是T需要实现serde[20]的Deserialize这个trait。
来看个例子:
use rocket::serde::{Deserialize, json::Json};
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
struct Task<'r> {
description: &'r str,
complete: bool
}
#[post("/todo", data = "<task>")]
fn new(task: Json<Task<'_>>) { /* .. */ }完整的例子可见:https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/examples/serialization/src/json.rs
**注意:**这里使用的#[serde(crate = "rocket::serde")]是用来指向rocket导出的serde这个包,由于孤岛规则的原因,我们这么写是存在损耗的,所以如果不想导致损耗的话,可以直接修改Cargo.toml让它直接和rocket同级
serde = { version = "1.0", features = ["derive"] }另外这里的支持json也需要修改Cargo.toml
rocket = { version = "=0.5.0-rc.3", features = ["json"] } 还有其他格式的,详情可见:rocket - Rust
Temporary Files
看名字就知道是和文件操作相关的,这个TempFile数据守卫将数据流写入一个临时文件中,这样使得文件操作变得轻松很多。
来看个例子
use rocket::fs::TempFile;
#[post("/upload", format = "plain", data = "<file>")]
async fn upload(mut file: TempFile<'_>) -> std::io::Result<()> {
file.persist_to(permanent_location).await
}Streaming
有的时候我们就是想自己操作进来的数据,那么这个时候我们就可以通过Data[21]这个类型来包裹参数,比如:
use rocket::tokio;
use rocket::data::{Data, ToByteUnit};
#[post("/debug", data = "<data>")]
async fn debug(data: Data<'_>) -> std::io::Result<()> {
// Stream at most 512KiB all of the body data to stdout.
data.open(512.kibibytes())
.stream_to(tokio::io::stdout())
.await?;
Ok(())
}最多接收512KB的字节流,注意这里为了避免遭受到DoS的攻击,需要声明接收的最大值,这里使用的是rocket自带的ToByteUnit[22]的trait用来限制
Forms#
前面我们用format限制了Content-Type等,但是我们的数据拿到手还是字符串类型的,这时就需要我们手动去实现这一类的功能,将数据转换成我们想要的格式,前面Body Data里JSON<T>就是其中一种方法,不过Rocket中还内置了另一种数据守卫Form[24]以及FromForm[25]这个trait,可以让我们转换成更多的类型。
我们先来看个例子
use rocket::form::Form;
#[derive(FromForm)]
struct Task<'r> {
complete: bool,
r#type: &'r str,
}
#[post("/todo", data = "<task>")]
fn new(task: Form<Task<'_>>) { /* .. */ }Form作为一个数据守卫,要求它的泛型T得实现FromForm这个trait,我们这里直接使用派生宏给Task实现FromForm。
FromForm可以用于给任何字段都实现了FromForm/FromFormField的数据机构实现。
注意这里依旧是会校验Content-Type的,也就是说即使我们不使用format组件依旧没问题,当不匹配的时候就会forward。
我们甚至可以用Option<T>或者Result<T, E>包裹参数,这样我们就可以让fail被捕获到了。
#[post("/todo", data = "<task>")]
fn new(task: Option<Form<Task<'_>>>) { /* .. */ }Multipart
Multipart表单会被无感知处理,也没有其余损耗。
例子:
use rocket::form::Form;
use rocket::fs::TempFile;
#[derive(FromForm)]
struct Upload<'r> {
save: bool,
file: TempFile<'r>,
}
#[post("/upload", data = "<upload>")]
fn upload_form(upload: Form<Upload<'_>>) { /* .. */ }这就行了,大多数Multipart都有被内置处理,我们并不需要手动去处理
Parsing Strategy
对于FromForm来说,它并不会做严格限制,限制啥呢?字段,我们学Rust基础内容的时候如果实例多了或者少了字段就会导致编译报错,不过这里这段代码是跑在runtime的,所以并不会有编译上的问题。Form<T>仅对当前类型的字段做处理,它不会去限制你的数据是否缺失字段或者多了字段,少了它自动帮你补全成默认值,多了它不理会。
当然,你如果不想要这么宽松,你可以使用Form<Strict<T>>[26]来严格限制,Strict<T>也是实现了FromForm的,所以任何可以使用Form的地方都可以使用Form<Strict<T>>。
use rocket::form::{Form, Strict};
#[post("/todo", data = "<task>")]
fn new(task: Form<Strict<Task<'_>>>) { /* .. */ }你也可以把它用于其中一个字段
#[derive(FromForm)]
struct Input {
required: Strict<bool>,
uses_default: bool
}
#[post("/", data = "<input>")]
fn new(input: Form<Input>) { /* .. */ }比如必填。
既然有严格,那自然就有宽松lenient[27] ,Form默认就是lenient的,所以请不要做Form<Lenient<T>>这种骚操作。
Defaults
前面我们说过字段缺失会有补充默认值的行为,那么这个默认值我们是可以自定义的。
#[derive(FromForm)]
struct MyForm {
// Set the default value to be `"hello"`.
//
// Note how an `&str` is automatically converted into a `String`.
#[field(default = "hello")]
greeting: String,
// Remove the default value of `false`, requiring all parses of `MyForm`
// to contain an `is_friendly` field.
#[field(default = None)]
is_friendly: bool,
}#[field(default = expr)] 当这里的default为None的时候则不会作为这个字段的默认值,而是采用FromForm提供的。
如果不是None,则会调用expr.into()将值作为字段的默认值,具体可看:FromForm in rocket - Rust。
Field Renaming
就如标题所说,这里还支持字段重命名,你可能觉得有些蛋疼,但实际上在一些场景下还是有用的,比如对于前端给的数据中给的字段是另一个名字,这个时候如果跟前端battle不过,你就可以用,当然,更建议是继续battle直到胜利,来看个例子:
#[derive(FromForm)]
struct External<'r> {
#[field(name = uncased("firstName"))]
#[field(name = "first_name")]
first_name: &'r str
}我们给first_name这个字段定义一个first-Name的大小写敏感名字,和一个firstName大小写敏感的名字。也就是说这里支持first-Name、firstName、FirstName或者FIRSTName。
Ad-Hoc Validation
除了设置默认值之外,我们还可以做验证,是的,针对于某个字段的校验。比如:
#[derive(FromForm)]
struct Person {
#[field(validate = range(21..))]
age: u16
}我们需要age > 21才行,所以这里给了个range(21..)。不过这里的range是rocket::form::validate::range[28]
除此之外,还可以使用validate模块里提供的其它函数,比如:
#[derive(FromForm)]
struct Password<'r> {
#[field(name = "password")]
value: &'r str,
#[field(validate = eq(self.value))]
#[field(validate = omits("no"))]
confirm: &'r str,
} 当然,这也是可以自定义的,不过需要借助Rocket::form::Result来实现,比如:
use rocket::time::Date;
use rocket::form::{self, Error};
#[derive(FromForm)]
struct CreditCard {
#[field(validate = luhn(self.cvv, &self.expiration))]
number: u64,
#[field(validate = range(..9999))]
cvv: u16,
expiration: Date,
}
fn luhn<'v>(number: &u64, cvv: u16, exp: &Date) -> form::Result<'v, ()> {
if !valid {
Err(Error::validation("invalid credit card number"))?;
}
Ok(())
} Wrapping Validators
如果很多地方都有用到同一种验证,那么我们最好是将这块逻辑抽取出来重复使用。Rocket也提供了这种功能
比如这个Age的验证在很多地方都有用到,那么这个时候就可以这么封装
#[derive(FromForm)]
#[field(validate = range(18..150))]
struct Age(u16);
#[derive(FromForm)]
struct Person {
#[field(validate = range(21..))]
age: Age
}别忘了给它加上derive(FromForm),毕竟实现FromForm的前提是所有子字段都需要实现FromForm。
Rocket 的表单支持允许您的应用程序使用任何级别的嵌套和集合来表达任何结构,从而超越任何其他 Web 框架提供的表达能力。
简单地说,就是通过.或者[]去访问嵌套结构等。比如:
food.bart[bar:foo].blam[0_0][1000]=some-value&another_field=another_val
|-------------------------------| name
|--| |--| |-----| |--| |-| |--| keys
|--| |--| |-| |-| |--| |-| |--| indices 上面的格式不是一个引用,而是一个例子,用来加深理解。
等号左边这个链式调用被.和[]分割成keys,然后keys可以通过:来分割为indices也就是常说的index。
而等号右边则是一段key2=value2&key2=value2。
注意如果.是跟在[]后面的,那么这里的.不是必要的,比如a[b].c和a[b]c是等效的。
Nesting
Form是可以嵌套的,这一点我们前面的Age就是,我们再来看个例子:
use rocket::form::FromForm;
#[derive(FromForm)]
struct MyForm<'r> {
owner: Person<'r>,
pet: Pet<'r>,
}
#[derive(FromForm)]
struct Person<'r> {
name: &'r str
}
#[derive(FromForm)]
struct Pet<'r> {
name: &'r str,
#[field(validate = eq(true))]
good_pet: bool,
} 上面的结构可能被编码成owner.name=Bob&pet.name=Sally&pet.good_pet=on这样,然后解析之后会变成下面这样
MyForm {
owner: Person {
name: "Bob".into()
},
pet: Pet {
name: "Sally".into(),
good_pet: true,
}
}Vectors
也有支持Vectors的,而Vec里的元素也是可以使用.或者[]来赋值。比如下面这样:
"numbers[]=1&numbers[]=2&numbers[]=3",
"numbers[a]=1&numbers[b]=2&numbers[c]=3",
"numbers[a]=1&numbers[b]=2&numbers[a]=3",
"numbers[]=1&numbers[b]=2&numbers[c]=3",
"numbers.0=1&numbers.1=2&numbers[c]=3",
"numbers=1&numbers=2&numbers=3",这上面几种都是等效的,可能有些不好理解。对于空[]来说,直接push到vector的最后面,比如上面第一行,而对于同一个key来说,会按顺序往后放,比如上面第三行来说,等效于[a, b, a] = [1, 2, 3]。
它们都会被解析为
MyForm {
numbers: vec![1 ,2, 3]
}其中"numbers=1&numbers=2&numbers=3"有些离谱,不过它等效于numbers[]=1&numbers[]=2&number3=[]。
不过对于实现了FromFormField的类型来说,如果已经存在对应的key了就不会再继续push了。
// These, on the other hand...
"numbers[0]=1&numbers[0]=2&numbers[]=3",
"numbers[]=1&numbers[b]=3&numbers[b]=2",
// ...parse as this struct:
MyForm {
numbers: vec![1, 3]
}仅保留第一个key的值。
Nesting in Vectors
既然是Vector,那自然可以放其它结构的数据类型,比如Struct,来看个例子
// These form strings...
assert_form_parses! { MyForm,
"name=Bob&pets[0].name=Sally&pets[0].good_pet=on",
"name=Bob&pets[sally].name=Sally&pets[sally].good_pet=yes",
// ...parse as this struct:
MyForm {
name: "Bob".into(),
pets: vec![Pet { name: "Sally".into(), good_pet: true }],
}这个就不用多说了,和前面分析的流程一样。
需要提一下下面的场景是会导致解析失败的:
// These, on the other hand, fail to parse:
"name=Bob&pets[0].name=Sally&pets[1].good_pet=on",
"name=Bob&pets[].name=Sally&pets[].good_pet=on",不允许使用空[]或者key丢失。
Nested Vectors
既然Vec也实现了FromForm,那么它自然也可以作为Vec的元素类型。
比如:
#[derive(FromForm)]
struct MyForm {
v: Vec<Vec<usize>>,
}然后我们来看下规则:
"v=1&v=2&v=3" => MyForm { v: vec![vec![1], vec![2], vec![3]] },
"v[][]=1&v[][]=2&v[][]=3" => MyForm { v: vec![vec![1], vec![2], vec![3]] },
"v[0][]=1&v[0][]=2&v[][]=3" => MyForm { v: vec![vec![1, 2], vec![3]] },
"v[][]=1&v[0][]=2&v[0][]=3" => MyForm { v: vec![vec![1], vec![2, 3]] },
"v[0][]=1&v[0][]=2&v[0][]=3" => MyForm { v: vec![vec![1, 2, 3]] },
"v[0][0]=1&v[0][0]=2&v[0][]=3" => MyForm { v: vec![vec![1, 3]] },
"v[0][0]=1&v[0][0]=2&v[0][0]=3" => MyForm { v: vec![vec![1]] },老实说这真的可读性极差极差,这就是灵活的代价。
Maps
一个表单还可以包含maps,比如:
use std::collections::HashMap;
#[derive(FromForm)]
struct MyForm {
ids: HashMap<String, usize>,
}和Vector不一样的是,对于Map来说,它的key是有意义的,同一个key永远指向同一个数据。
来看个例子:
// These form strings...
"ids[a]=1&ids[b]=2",
"ids[b]=2&ids[a]=1",
"ids[a]=1&ids[a]=2&ids[b]=2",
"ids.a=1&ids.b=2",
// ...parse as this struct:
MyForm {
ids: map! {
"a" => 1usize,
"b" => 2usize,
}
}由于map的key和value可以是任意值,所以这里你把key设置为usize,那么你赋值时的表现完全可以和Vector的一样,但尽量不要这么做。
// These form strings...
"ids[0]name=Bob&ids[0]age=3&ids[1]name=Sally&ids[1]age=10",
"ids[0]name=Bob&ids[1]age=10&ids[1]name=Sally&ids[0]age=3",
"ids[0]name=Bob&ids[1]name=Sally&ids[0]age=3&ids[1]age=10",
// ...which parse as this struct:
MyForm {
ids: map! {
0usize => Person { name: "Bob".into(), age: 3 },
1usize => Person { name: "Sally".into(), age: 10 },
}
} 由于key值需要比较,所以对于你设置的key类型来说,它需要实现Eq,另外还需要实现一个Hash,这个点和Rocket底层逻辑有关,这里就不分析了。
当然,别忘了实现FromForm。
这里还有个问题,由于key可以是任意值,那么这里是可以塞入一个struct的,而struct又有多个字段,如果要写就得把所有字段再加上,这就很蛋疼了,而Rocket官方显然也知道这个问题,所以针对这个问题他们又提供了一个方案(这可读性真的。。。。)
来看个例子:
#[derive(FromForm)]
struct MyForm {
m: HashMap<Person, Pet>,
}
#[derive(FromForm, PartialEq, Eq, Hash)]
struct Person {
name: String,
age: usize
}
#[derive(FromForm)]
struct Pet {
wags: bool
}而针对于这种,写法这是:
"m[k:alice]name=Alice&m[k:alice]age=30&m[v:alice].wags=no",
"m[k:alice]name=Alice&m[k:alice]age=30&m[alice].wags=no",
"m[k:123]name=Alice&m[k:123]age=30&m[123].wags=no",m[k:$key],其中k表示key,而$key可以是任意的,对于同一序列来说,$key一样的都算是同一个元素。
同理于m[v:$value]
另外如果不指明k:则表示是value。
上面的代码解析后相当于:
MyForm {
m: map! {
Person { name: "Alice".into(), age: 30 } => Pet { wags: false }
}
}Arbitrary Collections
这个就不多解释了,简单地说就是自由~,但是自由的代价既是可读性差
来看个类型:
use std::collections::{BTreeMap, HashMap};
#[derive(FromForm, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
struct Person {
name: String,
age: usize
}
HashMap<Vec<BTreeMap<Person, usize>>, HashMap<usize, Person>>
|-[k:$k1]-----------|------|------| |-[$k1]-----------------|
|---[$i]-------|------|------| |-[k:$j]*|
|-[k:$k2]|------| ~~[$j]~~|name*|
|-name*| ~~[$j]~~|age-*|
|-age*-|
|~~~~~~~~~~~~~~~|v:$k2*|根据上面的类型,我们可以搞个例子:
type Foo = HashMap<Vec<BTreeMap<Person, usize>>, HashMap<usize, Person>>;
// This (long, contrived) form string...
"[k:top_key][i][k:sub_key]name=Bobert&\
[k:top_key][i][k:sub_key]age=22&\
[k:top_key][i][sub_key]=1337&\
[top_key][7]name=Builder&\
[top_key][7]age=99",你现在可以很好的分析这一块的意思吗?如果可以,那你基本上就学会了,但是如果还没,那也不急,咱以后实战磨练。
再分析序列之前,我们先来分析下类型,是一个Map类型,然后它的key类型是一个Vector,value类型则是Map,它的key是usize,而value是struct。而key里的元素则是BTree也是一个Map,而这个Map的key则是struct,value是usize。
很乱是吧,不过也不是不能理解。
然后我们来分析下序列。
top_key指的是Vector,也就是最外层Map的key。而i是Vector里的元素,是BtreeMap。sub_key则是BtreeMap里的key,也就是Persion。
而后面没有k:则表示是同一个数据的value,也就是Persion。
Context
Contextual[29]是其它form guard的代理(proxy),记录所有表单提交的值和提供关联到对应字段名的错误。Contextual一般是用于表单的渲染回填,即上次填写的内容再次打开表单的时候可以看到,另外就是提供错误这一点了。
我们可以使用Form<Contextual<'_, T>>作为数据守卫,T则需要实现FromForm。context字段包含表单的上下文Context[30]。
来看下用法:
use rocket::form::{Form, Contextual};
#[post("/submit", data = "<form>")]
fn submit(form: Form<Contextual<'_, T>>) {
if let Some(ref value) = form.value {
// The form parsed successfully. `value` is the `T`.
}
// We can retrieve raw field values and errors.
let raw_id_value = form.context.field_value("id");
let id_errors = form.context.field_errors("id");
}错误收集是支持嵌套写法的,即a.b.c这种。
另外Context会被序列化成一个map,所以它可以用于渲染在templates中,不过需要使用Serialize类型。具体可见:https://api.rocket.rs/v0.5-rc/rocket/form/struct.Context.html。而例子可见:https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/examples/forms
Query Strings#
前面我们分析的都属于path,那么这个Query是个啥呢?Query声明和path参数类似,但是处理方式则是和常规URL编码表单字段类似。
来看张图:

img_path_and_query
除了实现的trait不同之外。。。。
不过不用急,我们等会接触到例子就知道了
Static Parameters
和静态路径一样,也是强匹配,直接来看下例子:
#[get("/?hello&cat=♥")]
fn cats() -> &'static str {
"Hello, kittens!"
}
// The following GET requests match `cats`. `%E2%99%A5` is encoded `♥`.
"/?cat=%E2%99%A5&hello"
"/?hello&cat=%E2%99%A5"
"/?dogs=amazing&hello&there&cat=%E2%99%A5"注意是contains即是子集就行,并且不需要注意每个参数的顺序
可以看到实际上就是在匹配参数,专注点不同。
Dynamic Parameters
用法相信大家也都猜出来了,这里咱就直接上例子了
#[derive(Debug, PartialEq, FromFormField)]
enum Color {
Red,
Blue,
Green
}
#[derive(Debug, PartialEq, FromForm)]
struct Pet<'r> {
name: &'r str,
age: usize,
}
#[derive(Debug, PartialEq, FromForm)]
struct Person<'r> {
pet: Pet<'r>,
}
#[get("/?<name>&<color>&<person>&<other>")]
fn hello(name: &str, color: Vec<Color>, person: Person<'_>, other: Option<usize>) {
assert_eq!(name, "George");
assert_eq!(color, [Color::Red, Color::Green, Color::Green, Color::Blue]);
assert_eq!(other, None);
assert_eq!(person, Person {
pet: Pet { name: "Fi Fo Alex", age: 1 }
});
}
// A request with these query segments matches as above.
name=George&\
color=red&\
color=green&\
person.pet.name=Fi+Fo+Alex&\
color=green&\
person.pet.age=1&\
color=blue&\
extra=yes\注意默认也是不敏感 + 宽容的
Trailing Parameter
也就是收尾参数<xx..>,直接看例子:
use rocket::form::Form;
#[derive(FromForm)]
struct User<'r> {
name: &'r str,
active: bool,
}
#[get("/?hello&<id>&<user..>")]
fn user(id: usize, user: User<'_>) {
assert_eq!(id, 1337);
assert_eq!(user.name, "Bob Smith");
assert_eq!(user.active, true);
}
// A request with these query segments matches as above.
hello&\
name=Bob+Smith&\
id=1337&\
active=yes\这里收集的包括static和dynamic的,另外只能放到最后不然也会有编译错误
Error Catchers#
一般错误的出现大多来自以下场景:
- 守卫失败
- 响应失败
- 路由匹配失败
而catcher捕获器就是用来捕获这些错误的,包含了错误码和错误引起的地方或者区域。
捕获器和路由挺像的,不如说它也算是一种路由,但是比较特殊,专门用于处理出错误的场景。
它有以下一些和路由不同的点:
- 捕获器只设计错误的场景
- 捕获器使用
catch属性声明 - 捕获器通过
register()[33]而不是mount()[34]来注册 - 在捕获器被执行之前,任何的
cookies的修改都需要先处理完 - 错误处理器无法使用守卫
- 错误处理器返回的响应不能失败
- 处理器的作用域是路径的前缀
来看个例子:
use rocket::Request;
#[catch(404)]
fn not_found(req: &Request) { /* .. */ }表示捕获错误码为404的场景。
这里参数实际上除了&Request之外可能还有一个Status。返回的响应类型得是实现Responder的。比如String
#[catch(404)]
fn not_found(req: &Request) -> String {
format!("Sorry, '{}' is not a valid path.", req.uri())
}当然,不要忘了注册
fn main() {
rocket::build().register("/", catchers![not_found]);
}Scoping
前面说了捕获器的作用域是路由前缀,比如上面的例子中是/。
我们来看个例子:
#[catch(404)]
fn general_not_found() -> &'static str {
"General 404"
}
#[catch(404)]
fn foo_not_found() -> &'static str {
"Foo 404"
}
#[launch]
fn rocket() -> _ {
rocket::build()
.register("/", catchers![general_not_found])
.register("/foo", catchers![foo_not_found])
}这里如果你请求的是/foo,它只会走foo_not_found,而不会再触发general_not_found,所以作用域是严格的。
Default Catchers
默认捕获器会捕获所有类型的状态码。你可以用它来作为一个回调,用于处理公共业务,来看个例子:
use rocket::Request;
use rocket::http::Status;
#[catch(default)]
fn default_catcher(status: Status, request: &Request) { /* .. */ }
#[launch]
fn rocket() -> _ {
rocket::build().register("/", catchers![default_catcher])
}推荐是通过路由路径限制,而不是状态码,这样要处理的业务比较单一,作用域也比较小。当然,看你个人喜好。
Built-In Catcher
Rocket提供了内置的捕获器,返回的数据是HTML还是JSON取决于你说设置Header的Accept。具体可见:https://api.rocket.rs/v0.5-rc/rocket/catcher/struct.Catcher.html
Responses#
Request学的差不多了,我们来学下Response。
Responder#
我们在处理器中返回的内容最终会被rocket自动包装成一个标准的htttp responder[37],不过这是有代价的,我们的return type需要实现Responder这个trait。
Wrapping
有些情况下,我们的返回数据可能是一个多层嵌套的responder,即一个实现了Responder的类型包裹另外一个实现了Responder的类型,比如:
struct WrappingResponder<R>(R);具体的例子:
use rocket::http::Status;
use rocket::response::{content, status};
#[get("/")]
fn json() -> status::Custom<content::RawJson<&'static str>> {
status::Custom(Status::ImATeapot, content::RawJson("{ \"hi\": \"world\" }"))
}例子挺好懂的,就不多说了。
Errors
这里的Errors实际上有两种场景,一种是来自于业务,比如某些数据在业务逻辑中判断是无效的,那么这个时候就应该返回Error,但是状态一般都是200表示http响应正常。而另一种则是程序自己的错误,是开发的bug,这种情况一般是返回500,表示服务器内部出错,这种就是之前我们接触到的catcher来处理。
Status
我们可以利用Status[38]来forward,比如:
use rocket::http::Status;
#[get("/")]
fn just_fail() -> Status {
Status::NotAcceptable
}NotAcceptable对应的状态码是406,属于error status codes(范围是[400, 599]),它会直接forward到error catcher。其它可以跳catcher的还有以下的场景:

Custom Responder#
直接来看个例子:
use rocket::http::{Header, ContentType};
#[derive(Responder)]
#[response(status = 500, content_type = "json")]
struct MyResponder {
inner: OtherResponder,
header: ContentType,
more: Header<'static>,
#[response(ignore)]
unrelated: MyType,
}上面的代码在包裹的过程中会经历下面的流程:
- 将当前状态码设置成
500; - 返回数据类型设置成
application/json; - 插入
self.header和self.more; - 执行
self.inner返回组装好的response。
这里注意inner这个字段作为内部的responder,而其他的(除了使用#[response(ignore)]的)都会作为header插入到response。具体可见:https://api.rocket.rs/v0.5-rc/rocket/derive.Responder.html
一般情况下我们都是直接impl Responder来实现(具体可见:https://api.rocket.rs/v0.5-rc/rocket/response/trait.Responder.html),但是这里实际上还有另一种方式。
如果你的类型包裹了一个Responder,那么你的这个类型会自动实现Responder。
implementations#
Rocket内置给String、&str、File、Option以及Result这些类型实现了Responder。
Strings
给Strings和&str实现的Responder的body是sized以及Content-Type是text/plain的。
根据这点,相信大家自己都可以写一个,来看下源码的部分
use std::io::Cursor;
use rocket::request::Request;
use rocket::response::{self, Response, Responder};
use rocket::http::ContentType;
#[rocket::async_trait]
impl<'r> Responder<'r, 'static> for String {
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
Response::build()
.header(ContentType::Plain)
.sized_body(self.len(), Cursor::new(self))
.ok()
}
}这没什么好说的了,大家想要自己定义的话可以参照这个例子。
Option
由于Option和Result都要接收泛型作为数据类型,所以它们的T都需要实现Responder才行。
对于None的场景,会直接进入404的场景。来看个例子:
use rocket::fs::NamedFile;
#[get("/<file..>")]
async fn files(file: PathBuf) -> Option<NamedFile> {
NamedFile::open(Path::new("static/").join(file)).await.ok()
}成功直接返回文件,失败直接404。
Result
由于Result需要接受两个泛型T和E,所以他俩都得实现Responder。
还是fileserver,不过我们可以拓展下我们想要返回的内容:
use rocket::fs::NamedFile;
use rocket::response::status::NotFound;
#[get("/<file..>")]
async fn files(file: PathBuf) -> Result<NamedFile, NotFound<String>> {
let path = Path::new("static/").join(file);
NamedFile::open(&path).await.map_err(|e| NotFound(e.to_string()))
}Rocket Responders#
Rocket自己的一些类型也都实现了Responder,来看下最常用的几个:
- NamedFile:返回给客户端的文件流,会自动根据文件的拓展名来设置
Content-Type。 - Redirect:重定向。
- content:用于覆盖响应的Content-Type。
- status:用于覆盖响应的状态码。
- Flash:设置访问时删除的“闪存”cookie。
- Json:自动序列化值为JSON。
- MsgPack:自动序列化值为MessagePack。
- Template:使用Tera或者handlebars渲染一个动态的模板
Async Streams
stream响应器允许无线的异步stream。一个stream可以通过任何的异步Stream或者AsyncRead类型或者宏stream![42]来创建。Streams是一块实时单向通信(unidirectional real-time communication)。比如通过EventStream[43]实现的一个聊天室[44](Server-Sent Events即SSE)。
我们来看个例子,通过AsyncRead类型来创建的ReaderStream[45]:
use std::io;
use std::net::SocketAddr;
use rocket::tokio::net::TcpStream;
use rocket::response::stream::ReaderStream;
#[get("/stream")]
async fn stream() -> io::Result<ReaderStream![TcpStream]> {
let addr = SocketAddr::from(([127, 0, 0, 1], 9999));
let stream = TcpStream::connect(addr).await?;
Ok(ReaderStream::one(stream))
}再来看个使用宏TextStream[46]的例子:
use rocket::tokio::time::{Duration, interval};
use rocket::response::stream::TextStream;
/// Produce an infinite series of `"hello"`s, one per second.
#[get("/infinite-hellos")]
fn hello() -> TextStream![&'static str] {
TextStream! {
let mut interval = interval(Duration::from_secs(1));
loop {
yield "hello";
interval.tick().await;
}
}
}JSON
我们一般用这个来快速的返回一个JSON类型的数据,不过Json<T>的泛型需要实现前面说过的来自serde[47]的Serialize[48]才行。
例子:
use rocket::serde::{Serialize, json::Json};
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
struct Task { /* .. */ }
#[get("/todo")]
fn todo() -> Json<Task> {
Json(Task { /* .. */ })
}默认设置的Content-Type是JSON,如果序列化失败,错误的场景是500。
序列化的例子:https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/examples/serialization。
Templates#
直接来看个例子:
use rocket_dyn_templates::Template;
#[get("/")]
fn index() -> Template {
let context = /* object-like value */;
Template::render("index", &context)
}这里使用了rocket_dyn_templates的Template[50],渲染了一个名为index的模板。
注意这里的context需要实现Serialize。如果是字面量,我们可以直接使用context!这个宏。
use rocket_dyn_templates::Template;
#[get("/")]
fn index() -> Template {
Template::render("index", context! {
foo: 123,
})
}模板需要先注册才行,我们可以使用Template::fairing()来让Template自动注册所有它接触到的template:
use rocket_dyn_templates::Template;
#[launch]
fn rocket() -> _ {
rocket::build()
.mount("/", routes![/* .. */])
.attach(Template::fairing())
}前面说过这里可以使用两种模型:handlerbars和tera,用哪个取决于你在template_dir文件夹中模板的后缀是.hbs还是.tera。
至于如何配置,我们后面会接触到。
Live Reloading
局部热更新,在开发模式下修改模板不需要重启服务器。
具体可见:https://api.rocket.rs/v0.5-rc/rocket_dyn_templates/struct.Template.html
例子:https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/examples/templating
Typed URIs#
这个直接描述可能不太好理解,所以这里先来看例子:
#[get("/<id>/<name>?<age>")]
fn person(id: Option<usize>, name: &str, age: Option<u8>) { /* .. */ }
// with unnamed parameters, in route path declaration order
let mike = uri!(person(101, "Mike Smith", Some(28)));
assert_eq!(mike.to_string(), "/101/Mike%20Smith?age=28");
// with named parameters, order irrelevant
let mike = uri!(person(name = "Mike", id = 101, age = Some(28)));
assert_eq!(mike.to_string(), "/101/Mike?age=28");
let mike = uri!(person(id = 101, age = Some(28), name = "Mike"));
assert_eq!(mike.to_string(), "/101/Mike?age=28");
// with a specific mount-point
let mike = uri!("/api", person(id = 101, name = "Mike", age = Some(28)));
assert_eq!(mike.to_string(), "/api/101/Mike?age=28");
// with optional (defaultable) query parameters ignored
let mike = uri!(person(101, "Mike", _));
assert_eq!(mike.to_string(), "/101/Mike");
let mike = uri!(person(id = 101, name = "Mike", age = _));
assert_eq!(mike.to_string(), "/101/Mike");现在应该就很好理解了。
简单地说,我们通过uri!这个宏来创建路由的URI。它会返回一个Origin的结构,将路由插入结构中作为返回的值,这个值实现了UriDisplay确保了它是类型安全的等。
Origin实现了Into<Uri>,所以它可以通过.into()以及传入Redirect::into()等方法的方式转换成一个Uri。
Ignorables
前面说过动态的参数可以通过_占位符来实现忽略(仅query,path不支持),而在这里自然需要兼容处理,要求URI必须实现Ignorable.
Deriving UriDisplay
前面说过返回的值需要实现UriDisplay这个trait才行,所以如果我们需要自定义某些路由参数的时候就需要手动去实现才行。
对于path需要实现UriDisplayPath,而query的则是UriDisplayQuery。
比如:
use rocket::form::Form;
#[derive(FromForm, UriDisplayQuery)]
struct UserDetails<'r> {
age: Option<usize>,
nickname: &'r str,
}
#[post("/user/<id>?<details..>")]
fn add_user(id: usize, details: UserDetails) { /* .. */ }
let link = uri!(add_user(120, UserDetails { age: Some(20), nickname: "Bob".into() }));
assert_eq!(link.to_string(), "/user/120?age=20&nickname=Bob");它实际上是编译阶段自动帮我们实现UriDisplay<Path/Query>。
Typed URI Parts
前面的UriDisplay实际上实现了Part这个trait,用来作为marker type,作用自然是用来标记数据是Path还是Query。这么做自然是为了传参的安全,我们传参也可以直接一套代码就行而不用做区分。
Conversions
FromUriParam这个类型是用来在UriDisplay接收值之前转换传给uri!的值的。其中P自然就是Part,而S表示转换的目标类型。比如:
impl<'a, P: Part> FromUriParam<P, &'a str> for String {
type Target = &'a str;
}下面这些都是可以转的:
&T=>T&mut T=>TString=>&str&str=>&Path&str=>PathBufT=>Form<T>
专属于path的:
T=>Option<T>T=>Result<T, E>
专属于query的:
Option<T>=>Result<T, E>(for anyE)Result<T, E>=>Option<T>(for anyE)
另外是支持传递的,比如A => B, B => C,那么可以直接改成A => C。
use std::path::PathBuf;
#[get("/person/<id>/<details..>")]
fn person(id: usize, details: Option<PathBuf>) { /* .. */ }
uri!(person(id = 100, details = "a/b/c"));&str先变成PathBuf,然后又变成Option<PathBuf>。
具体可见:https://api.rocket.rs/v0.5-rc/rocket/http/uri/fmt/trait.FromUriParam.html
State#
一个web应用基本都需要维护一些状态,比如访客量、任务队列以及多数据库。Rocket提供了工具用于安全的管理状态。
Managed State#
Rocket提供的状态管理是按类型来的,每个类型最多只会拥有一个值。
上手很简单:
- 调用
manage,并且初始化。 - 给任意处理器添加
&State<T>,其中T是提供给manage的值的类型。
注意都得是Send和Sync的,得保证线程安全,因为Rocket会自动开多个线程。
Adding State
前面说的manage是一个方法,来自于Rocket实例。来看个例子:
use std::sync::atomic::AtomicUsize;
struct HitCount {
count: AtomicUsize
}
rocket::build().manage(HitCount { count: AtomicUsize::new(0) });我们直接传入了初始值。
另外还可以多次调用:
rocket::build()
.manage(HitCount { count: AtomicUsize::new(0) })
.manage(Config::from(user_input));Retrieving State
我们可以通过&State<T>获取到T类型对应的值。&State是一个用于管理状态的请求守卫。来看个例子:
use rocket::State;
#[get("/count")]
fn count(hit_count: &State<HitCount>) -> String {
let current_count = hit_count.count.load(Ordering::Relaxed);
format!("Number of visits: {}", current_count)
}另外如果你想要获取的值之前并没有注册到manage中,那么在服务器启动的时候会直接失败。而针对于runtime的数据来说,也就是动态类型,Rocket会使用sentinels来判断是否符合要求,不符合会触发500。
完整的例子:https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/examples/state
manage细节:https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html#method.manage
State type:https://api.rocket.rs/v0.5-rc/rocket/struct.State.html
Within Guards
由于&State自身是一个请求守卫,所以我们可以直接通过实现了Request::guard()[54]或者Rocket::state()[55]的方式获取到&State,不过使用Request::guard()的前提是实现了FromRequest。
例子:
use rocket::State;
use rocket::request::{self, Request, FromRequest};
use rocket::outcome::IntoOutcome;
struct Item<'r>(&'r str);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Item<'r> {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, ()> {
// Using `State` as a request guard. Use `inner()` to get an `'r`.
let outcome = request.guard::<&State<MyConfig>>().await
.map(|my_config| Item(&my_config.user_val));
// Or alternatively, using `Rocket::state()`:
let outcome = request.rocket().state::<MyConfig>()
.map(|my_config| Item(&my_config.user_val))
.or_forward(());
outcome
}
}Request-Local State#
根据前面manage方法来自于Rocket的实例我们可以知道前面的状态管理是在全局的,如果我们只想要在局部管理的话我们需要使用Request-local state,这种state随着请求的结束而被drop,注意这里的请求指的是一次http请求,包括了request、fairing、request guard、responder。
这种局部状态是会被缓存的,这对于一些场景来说是很有用的,比如同一个http请求过程中多次触发的请求守卫等。
来看个例子:
use rocket::request::{self, Request, FromRequest};
/// A global atomic counter for generating IDs.
static ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
/// A type that represents a request's ID.
struct RequestId(pub usize);
/// Returns the current request's ID, assigning one only as necessary.
#[rocket::async_trait]
impl<'r> FromRequest<'r> for &'r RequestId {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
// The closure passed to `local_cache` will be executed at most once per
// request: the first time the `RequestId` guard is used. If it is
// requested again, `local_cache` will return the same value.
request::Outcome::Success(request.local_cache(|| {
RequestId(ID_COUNTER.fetch_add(1, Ordering::Relaxed))
}))
}
}
#[get("/")]
fn id(id: &RequestId) -> String {
format!("This is request #{}.", id.0)
}每次请求都会返回同一个id,这么做可以省去参数传递,写起来比较方便。
具体可见:https://api.rocket.rs/v0.5-rc/rocket/request/trait.FromRequest.html#request-local-state
Databases#
Rocket通过rocket_db_pools内置了对数据库的支持,通过连接池的方式访问多个数据库。
至于如何配置我们之后会接触到。
下面简单三步实现连接数据库:
- 在这里选择一个支持的数据库驱动:rocket_db_pools - Rust。然后将
rocket_db_pools引入项目中:
[dependencies.rocket_db_pools]
version = "=0.1.0-rc.3"
features = ["sqlx_sqlite"]- 设置一个数据区的名字,下面的例子中数据库名字是
sqlite_logs。配置相关信息,其中数据库路径是必须的。
[default.databases.sqlite_logs]
url = "/path/to/database.sqlite"- 引入数据库,首先需要定义数据库驱动的类型以及它指向的数据库名字。在创建实例的时候需要初始化数据库驱动。最后可以通过路由守卫
Connection<T>来获取数据库实例
#[macro_use] extern crate rocket;
use rocket_db_pools::{Database, Connection};
use rocket_db_pools::sqlx::{self, Row};
#[derive(Database)]
#[database("sqlite_logs")]
struct Logs(sqlx::SqlitePool);
#[get("/<id>")]
async fn read(mut db: Connection<Logs>, id: i64) -> Option<String> {
sqlx::query("SELECT content FROM logs WHERE id = ?").bind(id)
.fetch_one(&mut *db).await
.and_then(|r| Ok(r.try_get(0)?))
.ok()
}
#[launch]
fn rocket() -> _ {
rocket::build().attach(Logs::init()).mount("/", routes![read])
}更多细节:https://api.rocket.rs/v0.5-rc/rocket_db_pools/index.html
Driver Features
rocket_db_pools默认只会导出必要的特性,如果需要拓展,我们得在Cargo.toml中引入:
[dependencies.sqlx]
version = "0.6"
default-features = false
features = ["macros", "offline", "migrate"]
[dependencies.rocket_db_pools]
version = "=0.1.0-rc.3"
features = ["sqlx_sqlite"]Synchronous ORMs
rocket_db_pools是不堵塞的,如果你需要同步堵塞的话,你可以使用rocket_sync_db_pools。
Examples
例子:https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/examples/databases
Fairings#
Fairings前面提过几次,但是都没说是个啥,实际上就是Rocket提供用来实现类似中间层(middleware)的方法。有了中间层,我们可以在一个http请求的生命周期中做很多事情。
Overview#
对于实现了Fairing这个trait的类型来说都叫fairing。
虽然和其它框架中的中间层很像,但是几个关键点还是有区别的:
Fairings无法直接对请求进行终结/响应。Fairings无法注入没有非request数据到一个请求中。Fairings可以阻止程序启动。Fairings可以修改程序配置。
实际上其它框架中的中间层和请求守卫/数据守卫更像一些。
根据上面几点,一个Fairings的作用更多的是和全局有关的,而不是局部。如果你的逻辑是局部的,建议使用请求守卫等。
Attaching
Fairings是需要注册的,需要通过Rocket实例的attching方法注册。
看下例子:
#[launch]
fn rocket() -> _ {
rocket::build()
.attach(req_fairing)
.attach(res_fairing)
}Fairings是按顺序触发的,所以你需要确保顺序是正确的。
除了singleton fairings之外fairing都是允许触发多次的。
callbacks
fairing可以在下面几个事件中注册回调:
Ignite(on_ignite):在Rocket实例构建的时候触发hook。一般用来校验和处理配置,或者处理全局状态。Liftoff(on_liftoff):在程序launch的瞬间触发。可以用来阻止Rocket实例launch。Request(on_request):当请求到达的时候触发。可以用来修改请求。Response(on_response):当一个响应已经准备好发送给客户端了的时候触发。可以用来修改响应。Shutdown(on_shutdown):在程序shutdown的时候触发。
细节可见:https://api.rocket.rs/v0.5-rc/rocket/fairing/trait.Fairing.html
implementing#
实现Fairing必须实现info这个方法,它会返回一个Info。这个Info会被用于Rocket指派fairing和触发回调。
Requirements
另外实现Fairing需要Send + Sync + 'static,也就是线程安全。
Examples
我们有好几个get/post请求,这里面有一些相同的操作,这里就可以抽出来放到全局。
use std::io::Cursor;
use std::sync::atomic::{AtomicUsize, Ordering};
use rocket::{Request, Data, Response};
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::{Method, ContentType, Status};
struct Counter {
get: AtomicUsize,
post: AtomicUsize,
}
#[rocket::async_trait]
impl Fairing for Counter {
// This is a request and response fairing named "GET/POST Counter".
fn info(&self) -> Info {
Info {
name: "GET/POST Counter",
kind: Kind::Request | Kind::Response
}
}
// Increment the counter for `GET` and `POST` requests.
async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) {
match request.method() {
Method::Get => self.get.fetch_add(1, Ordering::Relaxed),
Method::Post => self.post.fetch_add(1, Ordering::Relaxed),
_ => return
};
}
async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
// Don't change a successful user's response, ever.
if response.status() != Status::NotFound {
return
}
// Rewrite the response to return the current counts.
if request.method() == Method::Get && request.uri().path() == "/counts" {
let get_count = self.get.load(Ordering::Relaxed);
let post_count = self.post.load(Ordering::Relaxed);
let body = format!("Get: {}\nPost: {}", get_count, post_count);
response.set_status(Status::Ok);
response.set_header(ContentType::Plain);
response.set_sized_body(body.len(), Cursor::new(body));
}
}
}注册了on_request和on_response两个回调,用来插入请求值和修改响应值。
完整的例子可见于:https://api.rocket.rs/v0.5-rc/rocket/fairing/trait.Fairing.html#example
Ad-Hoc Fairings#
这里还提供了字面量的快捷方法,比如:
use rocket::fairing::AdHoc;
use rocket::http::Method;
rocket::build()
.attach(AdHoc::on_liftoff("Liftoff Printer", |_| Box::pin(async move {
println!("...annnddd we have liftoff!");
})))
.attach(AdHoc::on_request("Put Rewriter", |req, _| Box::pin(async move {
req.set_method(Method::Put);
})))
.attach(AdHoc::on_shutdown("Shutdown Printer", |_| Box::pin(async move {
println!("...shutdown has commenced!");
})));AdHoc是一个内置的实现了Fairing的结构体。
Testing#
Rocket还提供了单元/集成测试。
不过这就需要我们模拟请求了。
Local Dispatching#
Rocket有一个local模块,它包含了一个Client结构体可以用来创建LocalRequest。
使用例子:
- 创建一个
Rocket实例。
let rocket = rocket::build();- 使用这个实例构建客户端。
let client = Client::tracked(rocket).unwrap();- 使用客户端实例构建请求。
let req = client.get("/");- 获取响应
let response = req.dispatch();Validating Response#
上面第四步返回的response的类型是LocalResponse。我们可以用它来校验。
status: 返回http状态码。content_type: 返回Content-Type。headers: 返回响应的header,是一个map。into_string: 将响应体数据转换成字符串。into_bytes: 将响应体数据转换成Vec<u8>。into_json: 转换成JSON格式。into_msgpack: 转换成MessagePack格式。
来看个例子:
use rocket::http::{ContentType, Status};
let mut response = client.get(uri!(hello)).dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::Plain));
assert!(response.headers().get_one("X-Special").is_some());
assert_eq!(response.into_string().unwrap(), "Expected Body");Testing demo#
我们来搞个例子:
首先搭建下基础:
#[get("/")]
fn hello() -> &'static str {
"Hello, world!"
}
#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![hello])
}Setting Up
然后来创建测试环境
#[cfg(test)]
mod test {
use super::rocket;
use rocket::local::blocking::Client;
use rocket::http::Status;
#[test]
fn hello_world() {
/* .. */
}
}然后完善测试逻辑
Testing
#[test]
fn hello_world() {
let client = Client::tracked(rocket()).expect("valid rocket instance");
let mut response = client.get(uri!(super::hello)).dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.into_string().unwrap(), "Hello, world!");
}然后就可以直接cargo test了。
最终代码:
#[get("/")]
fn hello() -> &'static str {
"Hello, world!"
}
#[launch]
fn rocket() -> Rocket<Build> {
rocket::build().mount("/", routes![hello])
}
#[cfg(test)]
mod test {
use super::rocket;
use rocket::local::blocking::Client;
use rocket::http::Status;
#[test]
fn hello_world() {
let client = Client::tracked(rocket()).expect("valid rocket instance");
let mut response = client.get(uri!(super::hello)).dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.into_string().unwrap(), "Hello, world!");
}
}Asynchronous Testing#
上面的例子你应该注意到了blocking,是的,目前测试都是同步的,即使Rocket自身是异步的。
同步测试比较简单,但是在某些场景就不太好用了,比如最常见的并发场景,需要模拟多个请求。
而Rocket其实也提供了异步的api:rocket::local::asynchronous。例子:https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/examples/testing/src/async_required.rs。
Codegen Debug#
我们可以通过设置参数ROCKET_CODEGEN_DEBUG=1环境变量来查看更详细的错误信息。
比如:
note: emitting Rocket code generation debug output
--> examples/hello/src/main.rs:14:1
|
14 | #[get("/world")]
| ^^^^^^^^^^^^^^^^
|
= note:
impl world {
fn into_info(self) -> rocket::StaticRouteInfo {
fn monomorphized_function<'_b>(
__req: &'_b rocket::request::Request<'_>,
__data: rocket::data::Data,
) -> ::rocket::route::BoxFuture<'_b> {
::std::boxed::Box::pin(async move {
let ___responder = world();
::rocket::handler::Outcome::from(__req, ___responder)
})
}
::rocket::StaticRouteInfo {
name: "world",
method: ::rocket::http::Method::Get,
path: "/world",
handler: monomorphized_function,
format: ::std::option::Option::None,
rank: ::std::option::Option::None,
sentinels: sentinels![&'static str],
}
}
}Configuration#
Rocket系统配置是基于Figment这个crate的。
Overview#
Rocket的配置系统是基于Figment的 Provider的,它用于提供配置数据。Rocket的Config 、 Config::figment()、 Toml 以及 Json都是Providers的实例。多个provider可以合并成一个provider,不过需要实现Deserialize。
下面是一些基础配置:
| key | kind | description | debug/release default |
|---|---|---|---|
| address | IpAddr | IP服务地址 | 127.0.0.1 |
| port | u16 | 服务的端口 | 8000 |
| workers* | usize | 用于处理futures的线程 | cpu core count |
| max_blocking* | usize | 限制线程用于同步任务 | 512 |
| ident | string, false | 是否/如何通过 Server header.辨别 | "Rocket" |
| ip_header | string, false | IP header用于检测,获取通过 client's real IP. | "X-Real-IP" |
| keep_alive | u32 | Keep-alive 时间; 为 0是禁止. | 5 |
| log_level | LogLevel | 最高支持多少层log(off/normal/debug/critical) | normal/critical |
| cli_colors | bool | 是否在log中支持颜色和emoji. | true |
| secret_key | SecretKey | 密钥 | None |
| tls | TlsConfig | TLS 的配置,如果有. | None |
| limits | Limits | Streaming读取长度的限制 | Limits::default() |
| limits.$name | &str/uint | 读取$name的长度限制 | form = "32KiB" |
| ctrlc | bool | 是否支持ctrl + c终止程序 | true |
| shutdown* | Shutdown | 自定义终结程序方式 | Shutdown::default() |
*注意:这里的woekers、max_blocking以及shutdown.force在default provider中是只读的。
Profiles
配置文件可以通过 Profile设置任何的名字。开发模式下 Config 和 Config::figment()在开发模式下会自动设置为debug,而生产环境则是release,当然都是可以自定义的。
比如我们可以通过ROCKET_PROFILE来覆盖default provider的基础配置。
对于配置文件来说,这里有两个元(meta)配置文件:default和global,它们提供的值可以应用于所有配置文件。default的数据是兜底的,如果必要的数据没有配置就会直接拿default的,而global则是最顶级的,会覆盖任何的配置文件下同个配置项。
Default Provider#
Rocket默认的配置provider是 Config::figment()。在rocket::build()的时候会被使用。
配置文件的基础是key也就是配置项。
下面配置内容的优先级逐级升高:
- 通过
Config::default()配置出来的,是兜底数据。 Rocket.toml或者TOML文件路径配置在ROCKET_CONFIG的环境变量。ROCKET_前缀的环境变量。
Rocket.toml
根据前面的内容,我们可以通过三种方式来自定义配置文件。
通过Rocket.toml的不需要我们去自定义路径,Rocket会自动去判断当前文件夹里是否存在,如果没有就会去父目录里找,如果还找不到就继续去父目录里找,直到根目录。
通过相对路径配置的ROCKET_CONFIG的话同上。
而如果通过ROCKET_CONFIG配置的路径是绝对路径的话,就不需要去递归查找。
一个配置文件中可以包含多种配置文件,比如:
## defaults for _all_ profiles
[default]
address = "0.0.0.0"
limits = { form = "64 kB", json = "1 MiB" }
## set only when compiled in debug mode, i.e, `cargo build`
[debug]
port = 8000
## only the `json` key from `default` will be overridden; `form` will remain
limits = { json = "10MiB" }
## set only when the `nyc` profile is selected
[nyc]
port = 9001
## set only when compiled in release mode, i.e, `cargo build --release`
[release]
port = 9999
ip_header = false
secret_key = "hPrYyЭRiMyµ5sBB1π+CMæ1køFsåqKvBiQJxBVHQk="而下面这个则是一个设置了所有项的配置文件
[default]
address = "127.0.0.1"
port = 8000
workers = 16
max_blocking = 512
keep_alive = 5
ident = "Rocket"
ip_header = "X-Real-IP" # set to `false` to disable
log_level = "normal"
temp_dir = "/tmp"
cli_colors = true
secret_key = "hPrYyЭRiMyµ5sBB1π+CMæ1køFsåqKvBiQJxBVHQk="
[default.limits]
form = "64 kB"
json = "1 MiB"
msgpack = "2 MiB"
"file/jpg" = "5 MiB"
[default.tls]
certs = "path/to/cert-chain.pem"
key = "path/to/key.pem"
[default.shutdown]
ctrlc = true
signals = ["term", "hup"]
grace = 5
mercy = 5注意,如果不是必要的情况切勿去配置,用默认的即可。
Environment Variables
前面说过ROCKET_为前缀的配置项优先级是高于Rocket.toml的。
Rocket会读取所有以ROCKET_为前缀的环境变量,然后将_后面的内容作为配置项的名字。比如:
ROCKET_FLOAT=3.14
ROCKET_ARRAY=[1,"b",3.14]
ROCKET_STRING=Hello
ROCKET_STRING="Hello There"
ROCKET_KEEP_ALIVE=1
ROCKET_IDENT=Rocket
ROCKET_IDENT="Hello Rocket"
ROCKET_IDENT=false
ROCKET_TLS={certs="abc",key="foo/bar"}
ROCKET_LIMITS={form="64 KiB"}格式也是toml的。
Configuration Parameters#
Secret Key
这个前面私有cookies的那块介绍过了,这里就不多说了。
Limits
用于限制数据类型最大允许接收的量。这里需要传一个字段table,比如limits = { json = "10MiB" }带单位和不带都可以,默认是32KiB。
TLS
Rocket内置支持TLS的,不过需要导入:
[dependencies]
rocket = { version = "=0.5.0-rc.3", features = ["tls"] }可以通过tls.key 和 tls.certs两个配置项来配置,对于default provider来说,可以这样:
[default.tls]
key = "path/to/key.pem" # Path or bytes to DER-encoded ASN.1 PKCS#1/#8 or SEC1 key.
certs = "path/to/certs.pem" # Path or bytes to DER-encoded X.509 TLS cert chain.tls的配置也是传一个字典,它会被反序列化成TlsConfig结构体。
| key | required | type |
|---|---|---|
| key | yes | DER 编码的 ASN.1 PKCS#1/#8 或 SEC1 密钥的路径或字节。 |
| certs | yes | DER编码的X.509 TLS证书链的路径或字节。 |
| ciphers | no | 启用CipherSuite数组. |
| prefer_server_cipher_order | no | 布尔值表示是否首选服务器密码套件(prefer server cipher suites.) |
| mutual | no | 一个mutual TLS 的map配置 |
每一个cipherSuite变体都会被转换成字符串,比如CipherSuite::TLS_AES_256_GCM_SHA384 变成 TLS_AES_256_GCM_SHA384。
默认的配置如下:
[default.tls]
certs = "/ssl/cert.pem"
key = "/ssl/key.pem"
prefer_server_cipher_order = false
ciphers = [
"TLS_CHACHA20_POLY1305_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
]Mutual TLS
Rocket也支持双向TLS客户端验证的模块mtls。它提供一个请求守卫用于校验和获取客户端证书。
默认情况下,双向TLS是禁止的并且客户端证书是不必要的。如果想要支持,需要引入:
[dependencies]
rocket = { version = "=0.5.0-rc.3", features = ["mtls"] }然后配置tls.mutual
[default.tls.mutual]
ca_certs = "path/to/ca_certs.pem" # Path or bytes to DER-encoded X.509 TLS cert chain.
mandatory = truetls.mutual,ca_certs配置的是CA证书。
tls.mutual也是要求传一个字典,最终也是会被反序列化成MutualTls结构体:
| key | required | type |
|---|---|---|
| ca_certs | yes | DER编码的X.509 TLS证书链的路径或字节。 |
| mandatory | no | 控制客户端是否必须进行身份验证的布尔值。 |
当允许双向TLS时,mtls::Certificate请求守卫可以用来校验客户端证书。
use rocket::mtls::Certificate;
#[get("/auth")]
fn auth(cert: Certificate<'_>) {
// This handler only runs when a valid certificate was presented.
}例子:https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/examples/tls
Worker
worker配置项前面说过,用于处理并行(parallel)任务执行,注意是并行,并发这里不做限制。
补充下:并行可以理解为多个人处理多个事件,并发则是一个人处理多个事件。
这个配置项只能通过ROCKET_WORKERS环境变量或者Rocket.toml来配置,其它都只能使用默认的。
max_blocking参数限制异步runtime创建的线程的最大个数,和worker一样都不能配置默认的,不过和worker不一样的是,它空载(idling)的时候会终止。另外512的值一般不要去改动它,除非硬件不支持。
Extracting Values#
我们可以自行拓展通过Rocket::figment()暴露出来的配置,不过前提是实现了 Deserialize 。比如:
use rocket::serde::Deserialize;
#[launch]
fn rocket() -> _ {
let rocket = rocket::build();
let figment = rocket.figment();
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
struct Config {
port: u16,
custom: Vec<String>,
}
// extract the entire config any `Deserialize` value
let config: Config = figment.extract().expect("config");
// or a piece of it into any `Deserialize` value
let custom: Vec<String> = figment.extract_inner("custom").expect("custom");
rocket
}一般情况下,我们的配置项都是直接放到全局状态管理的,Rocket也考虑到了这一点,它提供了一个AdHoc fairing,这货我们前面接触过。它可以提取配置,输出错误以及存储到全局状态管理器中。比如:
use rocket::{State, fairing::AdHoc};
#[get("/custom")]
fn custom(config: &State<Config>) -> String {
config.custom.get(0).cloned().unwrap_or("default".into())
}
#[launch]
fn rocket() -> _ {
rocket::build()
.mount("/", routes![custom])
.attach(AdHoc::config::<Config>())
}Custom Providers#
我们可以通过rocket::custom()自定义provider,它替换了对 rocket::build()的调用。
自定义provider可以基于Config::figment()或者 Config::default()。
具体可见:https://docs.rs/figment/0.10/figment
另外需要注意,你可能需要直接用到figment和serde它们。rocket默认导出了它俩,所以直接引用即可。不过rocket导出的它俩可能是阉割版的,所以如果要用到一些特性,可能还得从包自身引入:
figment = { version = "0.10", features = ["env", "toml", "json"] }来看个自定义配置项的例子:
use rocket::data::{Limits, ToByteUnit};
#[launch]
fn rocket() -> _ {
let figment = rocket::Config::figment()
.merge(("port", 1111))
.merge(("limits", Limits::new().limit("json", 2.mebibytes())));
rocket::custom(figment).mount("/", routes![/* .. */])
}再来看个复杂一点的:
use rocket::serde::{Serialize, Deserialize};
use rocket::fairing::AdHoc;
use figment::{Figment, Profile, providers::{Format, Toml, Serialized, Env}};
#[derive(Debug, Deserialize, Serialize)]
#[serde(crate = "rocket::serde")]
struct Config {
app_value: usize,
/* and so on.. */
}
impl Default for Config {
fn default() -> Config {
Config { app_value: 3, }
}
}
#[launch]
fn rocket() -> _ {
let figment = Figment::from(rocket::Config::default())
.merge(Serialized::defaults(Config::default()))
.merge(Toml::file("App.toml").nested())
.merge(Env::prefixed("APP_").global())
.select(Profile::from_env_or("APP_PROFILE", "default"));
rocket::custom(figment)
.mount("/", routes![/* .. */])
.attach(AdHoc::config::<Config>())
}这里把能涉及到的配置文件都涉及到的,default的,global的,环境变量的。
实战(Pastebin)#
接下来来搞个简单的文件存储系统,返回文件存储的路径。
Pastebin指的是文件存储站点,可以上传文件。
Finished Product#
线上体验:https://paste.rs/
可以直接用curl来访问,比如:
curl --data-binary @test.txt https://paste.rs/我们需要先设计接口:
upload-#[post("/")]通过request的body获取原始数据并且返回文件存储的url。retrieve-#[get("/<id>")]返回上传的文件内容,通过id访问。
Getting Started#
先搭建环境
cargo new --bin rocket-pastebin
然后配置下根目录的Cargo.toml
[workspace]
members = [
"hello-rocket",
"rocket-pastebin"
]然后回到rocket-pastebin中引入rocket
[dependencies]
rocket = "=0.5.0-rc.3"然后到main.rs中:
#[macro_use] extern crate rocket;
#[launch]
fn rocket() -> _ {
rocket::build()
}然后试运行下cargo run -p rocket-pastebin,应该是正常运行了。
Design#
在开始实现之前,我们需要确定俩问题:
-
存储的文件放哪?一般情况下我们是需要单独买一个存储型服务器作为文件存储的地方,但是我们这个就只是一个
demo,直接就放到项目里就行了,那么新建一个和src同级的文件夹upload作为静态资源。 -
如何命名,常见的就是根据当前时间戳来命名,或者根据时间戳再进一步
hash处理。我们这里就简单的通过随机数创建id就行了。我们在src下创建一个paste_id.rs文件
use std::borrow::Cow;
use std::path::{Path, PathBuf};
use rand::{self, Rng};
/// A _probably_ unique paste ID.
pub struct PasteId<'a>(Cow<'a, str>);
impl PasteId<'_> {
/// Generate a _probably_ unique ID with `size` characters. For readability,
/// the characters used are from the sets [0-9], [A-Z], [a-z]. The
/// probability of a collision depends on the value of `size` and the number
/// of IDs generated thus far.
pub fn new(size: usize) -> PasteId<'static> {
const BASE62: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
let mut id = String::with_capacity(size);
let mut rng = rand::thread_rng();
for _ in 0..size {
id.push(BASE62[rng.gen::<usize>() % 62] as char);
}
PasteId(Cow::Owned(id))
}
/// Returns the path to the paste in `upload/` corresponding to this ID.
pub fn file_path(&self) -> PathBuf {
let root = concat!(env!("CARGO_MANIFEST_DIR"), "/", "upload");
Path::new(root).join(self.0.as_ref())
}
}随机生成一个数,然后创建PasteId实例自身存储id,到时候访问的时候直接从里面拿即可。
别忘了在toml中引入rand
[dependencies]
rocket = "=0.5.0-rc.3"
rand = "0.8"最后在main.rs中引入
mod paste_id;
use paste_id::PasteId;本来我们还需要一个index用来提供上传的入口,但是我们这里是基于curl的,所以就没必要了。
Retrieving Pastes#
我们先来实现通过id获取上传内容的部分
use std::path::Path;
use rocket::tokio::fs::File;
#[get("/<id>")]
async fn retrieve(id: &str) -> Option<File> {
let upload_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/", "upload");
let filename = Path::new(upload_dir).join(id);
File::open(&filename).await.ok()
}通过传入的id拼接存储目录,然后获取对应文件的内容,最后响应出去。
别忘了注册:
#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![index, retrieve])
}不过这里还有个问题:full path disclosure attack。如果我们的文件中有些文件是私有的,不允许外面访问的,或者说需要一定的权限才能访问,那么这个时候我们就有可能会被攻击。
那么这个时候我们就需要搞个请求守卫用来解决这个问题,回到paste_id.rs文件中,我们需要给Paste实现FromParam:
use rocket::request::FromParam;
/// Returns an instance of `PasteId` if the path segment is a valid ID.
/// Otherwise returns the invalid ID as the `Err` value.
impl<'a> FromParam<'a> for PasteId<'a> {
type Error = &'a str;
fn from_param(param: &'a str) -> Result<Self, Self::Error> {
param.chars().all(|c| c.is_ascii_alphanumeric())
.then(|| PasteId(param.into()))
.ok_or(param)
}
}这里面的代码并没有体现出解决方案,实际上我们需要加上的代码是black list也就是黑名单,用来过滤需要权限访问的文件。
然后别忘了修改路由,
use rocket::tokio::fs::File;
#[get("/<id>")]
async fn retrieve(id: PasteId<'_>) -> Option<File> {
File::open(id.file_path()).await.ok()
}这样看就舒服很多了。
Uploading#
首先,我们需要接收的肯定是数据流
Streaming Data
我们需要使用前面说过的 Data来做数据守卫,限制获取的量。
use rocket::data::{Data, ToByteUnit};
use rocket::http::uri::Absolute;
// In a real application, these would be retrieved dynamically from a config.
const ID_LENGTH: usize = 3;
const HOST: Absolute<'static> = uri!("http://localhost:8000");
#[post("/", data = "<paste>")]
async fn upload(paste: Data<'_>) -> std::io::Result<String> {
let id = PasteId::new(ID_LENGTH);
paste.open(128.kibibytes()).into_file(id.file_path()).await?;
Ok(uri!(HOST, retrieve(id)).to_string())
}限制为128K大小。然后通过uri!这个宏生成路由路径,不过根据我们前面学的知识,这里我们还需要给retrieve的参数id的类型PasteId实现UriDisplayPath才行:
#[derive(UriDisplayPath)]
pub struct PasteId<'a>(Cow<'a, str>);那么现在程序应该是可以跑了,我们运行下cargo run -p rocket-pastebin
然后我们需要借助curl来实现上传,关于curl如何在win10/win11使用的,我之前go的一篇文件中有说到,这里就不多说了,可以去翻看下。
我们现在rocket-pastebin根目录下创建一个test.txt文件,然后在里面随便写点东西
curl.exe --data-binary @test.txt http://localhost:8000上传完之后我们再访问下
curl http://localhost:8000/$filename
这样就可以了,虽然和粗糙。。。
完整代码可见:https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/examples/pastebin
Help#
又到了经典《I Need Help》(某褪色者口号)环节。
还是那句话,能去github上找/问就尽量去github上,不过提issue前务必确认没有相同的缺陷:https://github.com/SergioBenitez/Rocket/issues。
另外你还可以加入聊天:https://chat.mozilla.org/#/room/#rocket:mozilla.org
又或者: Kiwi IRC client
你还可以在这问问题:https://rocket.rs/v0.5-rc/guide/faq/
例子#
https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/examples
或者你也可以看源码(?):https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/examples
总结#
让我想想。。。

参考#
- ^Rocket https://rocket.rs/v0.5-rc/guide/introduction/
- ^hello world https://rocket.rs/v0.5-rc/guide/getting-started/#hello-world
- ^LifeCycle https://rocket.rs/v0.5-rc/guide/overview/#lifecycle
- ^Routing https://rocket.rs/v0.5-rc/guide/overview/#routing
- ^mounting https://rocket.rs/v0.5-rc/guide/overview/#mounting
- ^Launching https://rocket.rs/v0.5-rc/guide/overview/#launching
- ^Future and Async https://rocket.rs/v0.5-rc/guide/overview/#futures-and-async
- ^Requests https://rocket.rs/v0.5-rc/guide/requests/
- ^Methods https://rocket.rs/v0.5-rc/guide/requests/#methods
- ^Dynamic Paths https://rocket.rs/v0.5-rc/guide/requests/#dynamic-paths
- ^FromParam https://api.rocket.rs/v0.5-rc/rocket/request/trait.FromParam.html
- ^Forwarding https://rocket.rs/v0.5-rc/guide/requests/#forwarding
- ^FromRequest https://api.rocket.rs/v0.5-rc/rocket/request/trait.FromRequest.html
- ^Cookies https://rocket.rs/v0.5-rc/guide/requests/#cookies
- ^CookieJar https://api.rocket.rs/v0.5-rc/rocket/http/struct.CookieJar.html
- ^Format https://rocket.rs/v0.5-rc/guide/requests/#format
- ^Body Data https://rocket.rs/v0.5-rc/guide/requests/#body-data
- ^FromData https://api.rocket.rs/v0.5-rc/rocket/data/trait.FromData.html
- ^JSON https://api.rocket.rs/v0.5-rc/rocket/serde/json/struct.Json.html
- ^serde https://serde.rs/
- ^data https://api.rocket.rs/v0.5-rc/rocket/data/struct.Data.html
- ^ToByteUnit https://api.rocket.rs/v0.5-rc/rocket/data/trait.ToByteUnit.html
- ^Forms https://rocket.rs/v0.5-rc/guide/requests/#forms
- ^Form https://api.rocket.rs/v0.5-rc/rocket/form/struct.Form.html
- ^FromForm https://api.rocket.rs/v0.5-rc/rocket/form/trait.FromForm.html
- ^Form<Strict> https://api.rocket.rs/v0.5-rc/rocket/form/struct.Strict.html
- ^lenient https://api.rocket.rs/v0.5-rc/rocket/form/struct.Lenient.html
- ^Rocket::form::validate https://api.rocket.rs/v0.5-rc/rocket/form/validate/fn.range.html
- ^Contextual https://api.rocket.rs/v0.5-rc/rocket/form/struct.Contextual.html
- ^Context https://api.rocket.rs/v0.5-rc/rocket/form/struct.Context.html
- ^Query Strings https://rocket.rs/v0.5-rc/guide/requests/#query-strings
- ^Error Catchers https://rocket.rs/v0.5-rc/guide/requests/#error-catchers
- ^register() https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html#method.register
- ^mount() https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html#method.mount
- ^Responses https://rocket.rs/v0.5-rc/guide/responses/
- ^title-Responder https://rocket.rs/v0.5-rc/guide/responses/#responses
- ^trait-Responder https://api.rocket.rs/v0.5-rc/rocket/response/trait.Responder.html
- ^Status https://api.rocket.rs/v0.5-rc/rocket/response/status/
- ^custom responder https://rocket.rs/v0.5-rc/guide/responses/#custom-responders
- ^implementions https://rocket.rs/v0.5-rc/guide/responses/#implementations
- ^Rocket Responders https://rocket.rs/v0.5-rc/guide/responses/#rocket-responders
- ^stream! https://api.rocket.rs/v0.5-rc/rocket/response/stream/macro.stream.html
- ^EventStream https://api.rocket.rs/v0.5-rc/rocket/response/stream/struct.EventStream.html
- ^chat-demo https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/examples/chat
- ^ReaderStream https://api.rocket.rs/v0.5-rc/rocket/response/stream/struct.ReaderStream.html
- ^TextStream https://docs.serde.rs/serde/trait.Serialize.html
- ^serde https://serde.rs/
- ^Serialize https://docs.serde.rs/serde/trait.Serialize.html
- ^Templates https://rocket.rs/v0.5-rc/guide/responses/#templates
- ^Template https://api.rocket.rs/v0.5-rc/rocket_dyn_templates/struct.Template.html
- ^Typed URIs https://rocket.rs/v0.5-rc/guide/responses/#typed-uris
- ^State https://rocket.rs/v0.5-rc/guide/state/
- ^Managed State https://rocket.rs/v0.5-rc/guide/state/#managed-state
- ^Request::guard() https://api.rocket.rs/v0.5-rc/rocket/struct.Request.html#method.guard
- ^Rocket::state() https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html#method.state
- ^Request-Local State https://rocket.rs/v0.5-rc/guide/state/#request-local-state
- ^Databases https://rocket.rs/v0.5-rc/guide/state/#databases
- ^Fairings https://rocket.rs/v0.5-rc/guide/fairings/#fairings
- ^overview https://rocket.rs/v0.5-rc/guide/fairings/#overview
- ^implementing https://rocket.rs/v0.5-rc/guide/fairings/#implementing
- ^Ad-Hoc Fairings https://rocket.rs/v0.5-rc/guide/fairings/#ad-hoc-fairings
- ^testing https://rocket.rs/v0.5-rc/guide/testing/
- ^Local Dispatching https://rocket.rs/v0.5-rc/guide/testing/#local-dispatching
- ^validating response https://rocket.rs/v0.5-rc/guide/testing/#validating-responses
- ^Testing "Hello World" https://rocket.rs/v0.5-rc/guide/testing/#testing-hello-world
- ^Asynchronous Testing https://rocket.rs/v0.5-rc/guide/testing/#asynchronous-testing
- ^Codegen Debug https://rocket.rs/v0.5-rc/guide/testing/#codegen-debug
- ^Configuration https://rocket.rs/v0.5-rc/guide/configuration/#configuration
编辑于 2023-05-11 15:01・IP 属地广东
