虽然说rust
写web
没有什么优势,但是来都来了,那顺便逛一下比较出名的Rust Web
框架吧~
rust
基础知识:坏蛋Dan:rust基础学习--final: 章节索引rust
异步基础知识: 坏蛋Dan:rust基础学习索引--异步tokio
简单入门:坏蛋Dan:rust基础学习--tokio入门一个异步的web
框架
话不多说,直接上手
在开始之前,我们需要更新下我们当前rustup
版本
然后创建我们的项目
然后配置下根目录的Cargo.toml
然后打开hello-rocket
文件夹里的Cargo.toml
然后回到main.rs
文件中
#[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
整理一下发送给客户端。Rocket
应用的中心是围绕路由(routes
)和处理器(handlers
)的。一个路由由以下两部分组成:
而处理器指的是一个简单的函数可以接收任意的参数和返回任意的响应。
参数可以匹配到的包含静态路径、动态路径、路径片段(segments
)、表单、查询语句(query strings
)、请求格式说明符(request format specifiers
)、请求体数据(body data
)。
Rocket
将这些定义为“属性”,写法和其他语言中的装饰器类似。比如下面这样:
一个路由属性搭配上一个路由处理器
其中#[get]
指的是一个get
类型的请求,/world
则是静态路径。除了get
,自然还有post
等,比如#[post]
、#[put]
或者#[catch]
用于服务自定义错误页面。我们会在之后的章节里更深入的了解到。
在派发请求之前,需要先找到派发的对象。这就需要我们去注册或者说是挂载对应的路由。
/hello
:请求匹配到的路径的基础路径(base path
),比如我们注册了world
(routes![world]
),那么这个时候请求路径就得是/hello/world
才能识别到对应的处理器。routes!
:前面说过这是一个宏,后面接入一组函数名,比如routes![fn_a, fn_b, fn_c]
。build
:创建一个新的Rocket
实例。同一个路由可以在不同基础路径下注册。比如这里的/hello/world
以及/hi/world
。
不过在开始创建实例之前,我们还需要launch
,launch
的作用是创建一个多线程异步服务器以及派发进来的请求给路由。
launch
有两种方式:
第一个就是我们刚看到的#[launch]
,编译阶段它会展开成一个main
函数搭配上异步的runtime
然后开启服务。另外我们还需要提供返回的类型,一般设置_
就行了,它会自动推断,如果你想,你也可以手动返回Rocket<Build>
。
第二种方式就是使用#[rocket::main]
路由属性。和#[launch]
不同的是它允许我们自行开启服务。比如下面这样:
你可以用来获取launch
的future
,在这个过程中处理一些其它的事务。又或者你需要判断launch
之后的错误场景做特殊处理,那么这个时候也是非常有用的。
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
的,当然你也可以自定义。
异步路由
来看个例子
这里使用了tokio::time::sleep
,是一个异步函数,所以需要.await
。
Multitasking
Rust
的Future
是一组协作式多任务处理,一般情况下我们不应该去阻塞异步函数。不过我们会遇到一些万不得已的场景,比如相关api
只有同步的,这种时候推荐使用tokio::task::spawn_blocking
也就是提供一个wrapper
包裹原来的方法。
我们前面已经接触过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
动态路径比较常见的场景是动态id
场景,比如:
这个路由会匹配所有/hello/
为基础路径的路由,然后将它匹配到的动态路径作为参数传递给处理器。
语法是<${xxx}>
。
你可以传N
个动态类型即你的动态路径有多层,只要你这个类型实现了FromParam
[11]这个trait
即可,他们管这叫做:参数保护,其实也就是trait bound
。。
比如:
Rocket
默认给标准库里的一些常见类型以及Rocket
自身的一些特殊类型实现了这个trait
,具体可以看这里:https://api.rocket.rs/v0.5-rc/rocket/request/trait.FromParam.html
多个片段(segments)
你也可以通过<param..>
的方式来匹配多个动态路径,这种类型的参数一般被叫做分段防护装置(segments guards
),都必须先实现FromSegments
这个trait
。这个参数必须放到路径的最后面,不然会导致编译错误(可以参考python
的*params
和**params
参数匹配)
来看个例子:
PathBuf
实现了FromSegments
这个trait
,所以你不用担心/page
或者/page//
导致的解析失败,另外因为实现了FromSegments
,我们也不用担心路径遍历攻击(path traversal attacks
)。基于这一点,我们可以简单的实现一个安全的静态文件服务器:
PS:如果你真的想要搞一个静态文件服务器,你可以使用FileServer
,只需一行代码即可:
Ignored Segments
如果你的路径是动态的,但是你又不想要其中几个参数,那么这个时候你可以直接使用_
占位符<_>
占位即可,如果是多个可以使用<_..>
。
比如:
回到我们前面匹配多个动态路径的路由例子:
如果这里的cool
不是一个布尔值呢?又或者age
不是u8
呢?我们前面提到的生命周期的里有一个validation
,这个时候就是invalid
,那么就不会走这个handler
,而是继续往下匹配,直到遇到了下一个匹配的上的或者找到最后找不到了走err handler
。
另外我们可以通过rank
关键字调整同个路由下不同handler
匹配的优先级。比如下面这个例子:
上面这个例子中首先匹配到的就是user
这个处理器,如果不符合校验,接着匹配的是user_int
,如果还不匹配则轮到user_str
。
这个rank
我们可以在终端看到,比如
Rocket
的默认rank
范围是-12 ~ -1
之间,这一点后面会解释到。
如果你给处理器的动态参数类型设置为Result<T, E>
或者Option<T>
,那么这个时候这个处理器函数将不会再往下匹配,Ok
拿到值,而Err
则表示解析错误等。
如果你确实存在多个同名路径路由,那么这个时候这个rank
是不能省略的,不然编译的时候会直接报错。
注意,这里是forward
的场景下才会出现。什么意思呢?
实际上就是上面这种场景,虽然是动态的,但是路径都长得一样。
那么对于长得不一样但是也有可能相同的场景来说并且没有指定rank
的情况下,这里就需要引入权重(优先权)这一点了。比如下面这个例子
虽然长得不一样,但是肯定是在同一个forward
中的。
那么哪个优先级高呢?
很明显是/foo/<_>/bar
高,因为它比较“静”也就是static
。
而_..
则是完全的“动”,即dynamic
。简单地说就是以/
分割那边字面量多哪边优先级高一些。
这里就是我们上面提到的rank
的默认范围,数越小优先级越高。而path
的权重则是高于query
的(这里指的是路由的不同分类)。
static
:路径中全是静态的,即都是字面量,比如/a/b/c
partial
:部分是动态的,比如/root/<_>/bar
wild
:全是动态的,比如<_..>
这里简单了解下即可,正常情况下我们并不会遇到这类问题。
简单地说就是类似trait bound
,也就是需要实现FromRequest
[13]这个trait
。实现了FromRequest
的类型就都叫做请求守卫。
来简单的看下源码和例子
可以看到实际上就是每个类型自身去实现校验的逻辑,而rocket
会在执行处理器之前进行校验。
对于没有命名的类型参数来说,你可以手动指定它们为请求守卫,比如:
其中A, B, C
三个类型都是请求守卫。 他们仨可以是任意类型,只要是实现了FromRequest
即可。
Custom Guards
基于上面这一点,我们实际上可以实现自己想要的请求守卫,自定义校验逻辑,比如:
如果不传key
就会报错
Guard Transparency
另外我们还可以基于守卫去实现访问权限的方式,rocket
中如果需要实现这一块功能,那么就需要数据都通过FromRequest
才行。如果某个类型是只能通过FromRequest
实现创建的,那么我们就将这类型称之为"类型级证明"(type-level proof
),证明当前的请求已经根据任意的策略进行了验证。而FromRequest
则作为这个类型的见证者,rocket
中称这一概念为守卫透明度(guard transparency
)。
这一大段话就很绕了,我们来搞个具体的例子加深下理解。
某个程序具有一个函数 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
。我们使用这俩组合成一套用户身份验证的逻辑,优先验证是否是管理员,次之才是用户,最后则是游客,直接跳登录页面。
这么写有函数重载那味儿了,代码会简洁非常多,不用在同一个处理器中用if
去做判断,而是通过impl FromRequest
去实现check
。
这么一看确实优雅了很多。
Cookie
这玩意儿在Web
中是绕不开了,而Rocket
自然也考虑过这一点,所以它内置了相关的请求守卫:CookieJar
[15],它允许我们对cookies
的增删改。既然它是一个请求守卫,那它自然是可以直接用来作为处理器参数的。
使用例子:
Private Cookies
Cookie
有个问题就是你发送给客户端的数据是暴露的,用户可以通过某些方法去得到对应的信息,而对于一些敏感的信息来说,被用户拿到了是非常危险的。所以自然就需要做处理,Rocket
中提供了私有cookie
,和其他cookie
唯一的不同点就是它们是加密过的。不过Rocket
默认不导入这块,我们需要手动去添加features
。
这些方法的名字和之前的一样,只不过加上了_private
的后缀,即get_private、set_private、add_private、remove_private
等。
来看个例子:
Secret Key
秘钥这玩意儿相信大家都知道是干啥的,简单地说就是用于加密时的一个私有变量。Rocket
中加密采用的是secret_key
配置中指定的 256
位的key
参数。在debug
模式下,这个key
会自动生成,而生产环境则需要我们开发者来定义。
值也不一定是256
位的base64
,也有可能是hex
字符串又或者32
字节的切片。我们可以通过openssl
等工具直接生成,我们本机连接远程服务器的方式之一就是通过它。
关于配置这一块,具体可以看:https://rocket.rs/v0.5-rc/guide/configuration
前面我们自己写的路由处理器获取到的数据或者返回的数据最终都变成了字符串,而真实开发中这并不是我们需要的,我们需要的是某些特定格式的,比如当header
中的Content-Type
为application/json
时,拿到或者返回的格式我们希望是json
的。
作为Web
框架,Rocket
自然也兼顾到了这一点。
我们可以在路由的路径参数中(从这里往后定义在#${method}[]
里面的参数都叫做组件了)设置format
组件来forward
, 比如:
只有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
。
上面Format
中的例子带有一个data = <user>
的组件,实际上这个就是定义的Body Data
。这个组件同样会作为处理器的参数传入处理器中。
来再看个例子:
这个参数需要实现FromData
[18]这个trait
,一般管这叫做数据守卫(data guard
).
JSON
JSON<T>
[19]可以将数据序列化成JSON
结构,唯一的要求则是T
需要实现serde
[20]的Deserialize
这个trait
。
来看个例子:
完整的例子可见:https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/examples/serialization/src/json.rs
**注意:**这里使用的#[serde(crate = "rocket::serde")]
是用来指向rocket
导出的serde
这个包,由于孤岛规则的原因,我们这么写是存在损耗的,所以如果不想导致损耗的话,可以直接修改Cargo.toml
让它直接和rocket
同级
另外这里的支持json
也需要修改Cargo.toml
还有其他格式的,详情可见:rocket - Rust
Temporary Files
看名字就知道是和文件操作相关的,这个TempFile
数据守卫将数据流写入一个临时文件中,这样使得文件操作变得轻松很多。
来看个例子
Streaming
有的时候我们就是想自己操作进来的数据,那么这个时候我们就可以通过Data
[21]这个类型来包裹参数,比如:
最多接收512KB
的字节流,注意这里为了避免遭受到DoS
的攻击,需要声明接收的最大值,这里使用的是rocket
自带的ToByteUnit
[22]的trait
用来限制
前面我们用format
限制了Content-Type
等,但是我们的数据拿到手还是字符串类型的,这时就需要我们手动去实现这一类的功能,将数据转换成我们想要的格式,前面Body Data
里JSON<T>
就是其中一种方法,不过Rocket
中还内置了另一种数据守卫Form
[24]以及FromForm
[25]这个trait
,可以让我们转换成更多的类型。
我们先来看个例子
Form
作为一个数据守卫,要求它的泛型T
得实现FromForm
这个trait
,我们这里直接使用派生宏给Task
实现FromForm
。
FromForm
可以用于给任何字段都实现了FromForm/FromFormField
的数据机构实现。
注意这里依旧是会校验Content-Type
的,也就是说即使我们不使用format
组件依旧没问题,当不匹配的时候就会forward
。
我们甚至可以用Option<T>
或者Result<T, E>
包裹参数,这样我们就可以让fail
被捕获到了。
Multipart
Multipart
表单会被无感知处理,也没有其余损耗。
例子:
这就行了,大多数Multipart
都有被内置处理,我们并不需要手动去处理
Parsing Strategy
对于FromForm
来说,它并不会做严格限制,限制啥呢?字段,我们学Rust
基础内容的时候如果实例多了或者少了字段就会导致编译报错,不过这里这段代码是跑在runtime
的,所以并不会有编译上的问题。Form<T>
仅对当前类型的字段做处理,它不会去限制你的数据是否缺失字段或者多了字段,少了它自动帮你补全成默认值,多了它不理会。
当然,你如果不想要这么宽松,你可以使用Form<Strict<T>>
[26]来严格限制,Strict<T>
也是实现了FromForm
的,所以任何可以使用Form
的地方都可以使用Form<Strict<T>>
。
你也可以把它用于其中一个字段
比如必填。
既然有严格,那自然就有宽松lenient
[27] ,Form
默认就是lenient
的,所以请不要做Form<Lenient<T>>
这种骚操作。
Defaults
前面我们说过字段缺失会有补充默认值的行为,那么这个默认值我们是可以自定义的。
#[field(default = expr)]
当这里的default为None的时候则不会作为这个字段的默认值,而是采用FromForm提供的。
如果不是None
,则会调用expr.into()
将值作为字段的默认值,具体可看:FromForm in rocket - Rust。
Field Renaming
就如标题所说,这里还支持字段重命名,你可能觉得有些蛋疼,但实际上在一些场景下还是有用的,比如对于前端给的数据中给的字段是另一个名字,这个时候如果跟前端battle
不过,你就可以用,当然,更建议是继续battle
直到胜利,来看个例子:
我们给first_name
这个字段定义一个first-Name
的大小写敏感名字,和一个firstName
大小写敏感的名字。也就是说这里支持first-Name
、firstName
、FirstName
或者FIRSTName
。
Ad-Hoc Validation
除了设置默认值之外,我们还可以做验证,是的,针对于某个字段的校验。比如:
我们需要age > 21
才行,所以这里给了个range(21..)
。不过这里的range
是rocket::form::validate::range
[28]
除此之外,还可以使用validate
模块里提供的其它函数,比如:
当然,这也是可以自定义的,不过需要借助Rocket::form::Result
来实现,比如:
Wrapping Validators
如果很多地方都有用到同一种验证,那么我们最好是将这块逻辑抽取出来重复使用。Rocket
也提供了这种功能
比如这个Age
的验证在很多地方都有用到,那么这个时候就可以这么封装
别忘了给它加上derive(FromForm)
,毕竟实现FromForm
的前提是所有子字段都需要实现FromForm
。
Rocket 的表单支持允许您的应用程序使用任何级别的嵌套和集合来表达任何结构,从而超越任何其他 Web 框架提供的表达能力。
简单地说,就是通过.
或者[]
去访问嵌套结构等。比如:
上面的格式不是一个引用,而是一个例子,用来加深理解。
等号左边这个链式调用被.
和[]
分割成keys
,然后keys
可以通过:
来分割为indices
也就是常说的index
。
而等号右边则是一段key2=value2&key2=value2
。
注意如果.
是跟在[]
后面的,那么这里的.
不是必要的,比如a[b].c
和a[b]c
是等效的。
Nesting
Form
是可以嵌套的,这一点我们前面的Age
就是,我们再来看个例子:
上面的结构可能被编码成owner.name=Bob&pet.name=Sally&pet.good_pet=on
这样,然后解析之后会变成下面这样
Vectors
也有支持Vectors
的,而Vec
里的元素也是可以使用.
或者[]
来赋值。比如下面这样:
这上面几种都是等效的,可能有些不好理解。对于空[]
来说,直接push
到vector
的最后面,比如上面第一行,而对于同一个key
来说,会按顺序往后放,比如上面第三行来说,等效于[a, b, a]
= [1, 2, 3]
。
它们都会被解析为
其中"numbers=1&numbers=2&numbers=3"
有些离谱,不过它等效于numbers[]=1&numbers[]=2&number3=[]
。
不过对于实现了FromFormField
的类型来说,如果已经存在对应的key
了就不会再继续push
了。
仅保留第一个key
的值。
Nesting in Vectors
既然是Vector
,那自然可以放其它结构的数据类型,比如Struct
,来看个例子
这个就不用多说了,和前面分析的流程一样。
需要提一下下面的场景是会导致解析失败的:
不允许使用空[]
或者key
丢失。
Nested Vectors
既然Vec
也实现了FromForm
,那么它自然也可以作为Vec
的元素类型。
比如:
然后我们来看下规则:
老实说这真的可读性极差极差,这就是灵活的代价。
Maps
一个表单还可以包含maps
,比如:
和Vector
不一样的是,对于Map
来说,它的key
是有意义的,同一个key
永远指向同一个数据。
来看个例子:
由于map
的key
和value
可以是任意值,所以这里你把key
设置为usize
,那么你赋值时的表现完全可以和Vector
的一样,但尽量不要这么做。
由于key
值需要比较,所以对于你设置的key
类型来说,它需要实现Eq
,另外还需要实现一个Hash
,这个点和Rocket
底层逻辑有关,这里就不分析了。
当然,别忘了实现FromForm
。
这里还有个问题,由于key
可以是任意值,那么这里是可以塞入一个struct
的,而struct
又有多个字段,如果要写就得把所有字段再加上,这就很蛋疼了,而Rocket
官方显然也知道这个问题,所以针对这个问题他们又提供了一个方案(这可读性真的。。。。)
来看个例子:
而针对于这种,写法这是:
m[k:$key]
,其中k
表示key
,而$key
可以是任意的,对于同一序列来说,$key
一样的都算是同一个元素。
同理于m[v:$value]
另外如果不指明k:
则表示是value
。
上面的代码解析后相当于:
Arbitrary Collections
这个就不多解释了,简单地说就是自由~,但是自由的代价既是可读性差
来看个类型:
根据上面的类型,我们可以搞个例子:
你现在可以很好的分析这一块的意思吗?如果可以,那你基本上就学会了,但是如果还没,那也不急,咱以后实战磨练。
再分析序列之前,我们先来分析下类型,是一个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]。
来看下用法:
错误收集是支持嵌套写法的,即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
前面我们分析的都属于path
,那么这个Query
是个啥呢?Query
声明和path
参数类似,但是处理方式则是和常规URL
编码表单字段类似。
来看张图:
img_path_and_query
除了实现的trait
不同之外。。。。
不过不用急,我们等会接触到例子就知道了
Static Parameters
和静态路径一样,也是强匹配,直接来看下例子:
注意是contains
即是子集就行,并且不需要注意每个参数的顺序
可以看到实际上就是在匹配参数,专注点不同。
Dynamic Parameters
用法相信大家也都猜出来了,这里咱就直接上例子了
注意默认也是不敏感 + 宽容的
Trailing Parameter
也就是收尾参数<xx..>
,直接看例子:
这里收集的包括static
和dynamic
的,另外只能放到最后不然也会有编译错误
一般错误的出现大多来自以下场景:
而catcher
捕获器就是用来捕获这些错误的,包含了错误码和错误引起的地方或者区域。
捕获器和路由挺像的,不如说它也算是一种路由,但是比较特殊,专门用于处理出错误的场景。
它有以下一些和路由不同的点:
catch
属性声明register()
[33]而不是mount()
[34]来注册cookies
的修改都需要先处理完来看个例子:
表示捕获错误码为404
的场景。
这里参数实际上除了&Request
之外可能还有一个Status
。返回的响应类型得是实现Responder
的。比如String
当然,不要忘了注册
Scoping
前面说了捕获器的作用域是路由前缀,比如上面的例子中是/
。
我们来看个例子:
这里如果你请求的是/foo
,它只会走foo_not_found
,而不会再触发general_not_found
,所以作用域是严格的。
Default Catchers
默认捕获器会捕获所有类型的状态码。你可以用它来作为一个回调,用于处理公共业务,来看个例子:
推荐是通过路由路径限制,而不是状态码,这样要处理的业务比较单一,作用域也比较小。当然,看你个人喜好。
Built-In Catcher
Rocket
提供了内置的捕获器,返回的数据是HTML
还是JSON
取决于你说设置Header
的Accept
。具体可见:https://api.rocket.rs/v0.5-rc/rocket/catcher/struct.Catcher.html
Request
学的差不多了,我们来学下Response
。
我们在处理器中返回的内容最终会被rocket
自动包装成一个标准的htttp responder
[37],不过这是有代价的,我们的return type
需要实现Responder
这个trait
。
Wrapping
有些情况下,我们的返回数据可能是一个多层嵌套的responder
,即一个实现了Responder
的类型包裹另外一个实现了Responder
的类型,比如:
具体的例子:
例子挺好懂的,就不多说了。
Errors
这里的Errors
实际上有两种场景,一种是来自于业务,比如某些数据在业务逻辑中判断是无效的,那么这个时候就应该返回Error
,但是状态一般都是200
表示http
响应正常。而另一种则是程序自己的错误,是开发的bug
,这种情况一般是返回500
,表示服务器内部出错,这种就是之前我们接触到的catcher
来处理。
Status
我们可以利用Status
[38]来forward
,比如:
NotAcceptable
对应的状态码是406
,属于error status codes
(范围是[400, 599]
),它会直接forward
到error catcher
。其它可以跳catcher
的还有以下的场景:
直接来看个例子:
上面的代码在包裹的过程中会经历下面的流程:
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
。
Rocket
内置给String、&str、File、Option
以及Result
这些类型实现了Responder
。
Strings
给Strings
和&str
实现的Responder
的body
是sized
以及Content-Type
是text/plain
的。
根据这点,相信大家自己都可以写一个,来看下源码的部分
这没什么好说的了,大家想要自己定义的话可以参照这个例子。
Option
由于Option
和Result
都要接收泛型作为数据类型,所以它们的T
都需要实现Responder
才行。
对于None
的场景,会直接进入404
的场景。来看个例子:
成功直接返回文件,失败直接404。
Result
由于Result
需要接受两个泛型T
和E
,所以他俩都得实现Responder
。
还是fileserver
,不过我们可以拓展下我们想要返回的内容:
Rocket
自己的一些类型也都实现了Responder
,来看下最常用的几个:
Content-Type
。Async Streams
stream
响应器允许无线的异步stream
。一个stream
可以通过任何的异步Stream
或者AsyncRead
类型或者宏stream!
[42]来创建。Streams
是一块实时单向通信(unidirectional real-time communication
)。比如通过EventStream
[43]实现的一个聊天室[44](Server-Sent Events
即SSE
)。
我们来看个例子,通过AsyncRead
类型来创建的ReaderStream
[45]:
再来看个使用宏TextStream
[46]的例子:
JSON
我们一般用这个来快速的返回一个JSON
类型的数据,不过Json<T>
的泛型需要实现前面说过的来自serde
[47]的Serialize
[48]才行。
例子:
默认设置的Content-Type
是JSON
,如果序列化失败,错误的场景是500
。
序列化的例子:https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/examples/serialization。
直接来看个例子:
这里使用了rocket_dyn_templates
的Template
[50],渲染了一个名为index
的模板。
注意这里的context
需要实现Serialize
。如果是字面量,我们可以直接使用context!
这个宏。
模板需要先注册才行,我们可以使用Template::fairing()
来让Template
自动注册所有它接触到的template
:
前面说过这里可以使用两种模型: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
这个直接描述可能不太好理解,所以这里先来看例子:
现在应该就很好理解了。
简单地说,我们通过uri!这个宏来创建路由的URI
。它会返回一个Origin的结构,将路由插入结构中作为返回的值,这个值实现了UriDisplay确保了它是类型安全的等。
Origin
实现了Into<Uri>
,所以它可以通过.into()
以及传入Redirect::into()等方法的方式转换成一个Uri。
Ignorables
前面说过动态的参数可以通过_
占位符来实现忽略(仅query
,path
不支持),而在这里自然需要兼容处理,要求URI
必须实现Ignorable.
Deriving UriDisplay
前面说过返回的值需要实现UriDisplay
这个trait
才行,所以如果我们需要自定义某些路由参数的时候就需要手动去实现才行。
对于path
需要实现UriDisplayPath,而query
的则是UriDisplayQuery。
比如:
它实际上是编译阶段自动帮我们实现UriDisplay<Path/Query>
。
Typed URI Parts
前面的UriDisplay
实际上实现了Part这个trait
,用来作为marker type
,作用自然是用来标记数据是Path
还是Query
。这么做自然是为了传参的安全,我们传参也可以直接一套代码就行而不用做区分。
Conversions
FromUriParam这个类型是用来在UriDisplay
接收值之前转换传给uri!
的值的。其中P
自然就是Part
,而S
表示转换的目标类型。比如:
下面这些都是可以转的:
&T
=> T
&mut T
=> T
String
=> &str
&str
=> &Path
&str
=> PathBuf
T
=> Form<T>
专属于path
的:
T
=> Option<T>
T
=> Result<T, E>
专属于query
的:
Option<T>
=> Result<T, E>
(for any E
)Result<T, E>
=> Option<T>
(for any E
)另外是支持传递的,比如A => B, B => C
,那么可以直接改成A => C
。
&str
先变成PathBuf
,然后又变成Option<PathBuf>
。
具体可见:https://api.rocket.rs/v0.5-rc/rocket/http/uri/fmt/trait.FromUriParam.html
一个web
应用基本都需要维护一些状态,比如访客量、任务队列以及多数据库。Rocket
提供了工具用于安全的管理状态。
Rocket
提供的状态管理是按类型来的,每个类型最多只会拥有一个值。
上手很简单:
manage
,并且初始化。&State<T>
,其中T
是提供给manage
的值的类型。注意都得是Send
和Sync
的,得保证线程安全,因为Rocket
会自动开多个线程。
Adding State
前面说的manage
是一个方法,来自于Rocket
实例。来看个例子:
我们直接传入了初始值。
另外还可以多次调用:
Retrieving State
我们可以通过&State<T>
获取到T
类型对应的值。&State是一个用于管理状态的请求守卫。来看个例子:
另外如果你想要获取的值之前并没有注册到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
。
例子:
根据前面manage
方法来自于Rocket
的实例我们可以知道前面的状态管理是在全局的,如果我们只想要在局部管理的话我们需要使用Request-local state
,这种state
随着请求的结束而被drop
,注意这里的请求指的是一次http
请求,包括了request、fairing、request guard、responder
。
这种局部状态是会被缓存的,这对于一些场景来说是很有用的,比如同一个http
请求过程中多次触发的请求守卫等。
来看个例子:
每次请求都会返回同一个id
,这么做可以省去参数传递,写起来比较方便。
具体可见:https://api.rocket.rs/v0.5-rc/rocket/request/trait.FromRequest.html#request-local-state
Rocket
通过rocket_db_pools
内置了对数据库的支持,通过连接池的方式访问多个数据库。
至于如何配置我们之后会接触到。
下面简单三步实现连接数据库:
rocket_db_pools
引入项目中:sqlite_logs
。配置相关信息,其中数据库路径是必须的。Connection<T>
来获取数据库实例更多细节:https://api.rocket.rs/v0.5-rc/rocket_db_pools/index.html
Driver Features
rocket_db_pools
默认只会导出必要的特性,如果需要拓展,我们得在Cargo.toml
中引入:
Synchronous ORMs
rocket_db_pools
是不堵塞的,如果你需要同步堵塞的话,你可以使用rocket_sync_db_pools。
Examples
例子:https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/examples/databases
Fairings
前面提过几次,但是都没说是个啥,实际上就是Rocket
提供用来实现类似中间层(middleware
)的方法。有了中间层,我们可以在一个http
请求的生命周期中做很多事情。
对于实现了Fairing这个trait
的类型来说都叫fairing
。
虽然和其它框架中的中间层很像,但是几个关键点还是有区别的:
Fairings
无法直接对请求进行终结/响应。Fairings
无法注入没有非request
数据到一个请求中。Fairings
可以阻止程序启动。Fairings
可以修改程序配置。实际上其它框架中的中间层和请求守卫/数据守卫更像一些。
根据上面几点,一个Fairings
的作用更多的是和全局有关的,而不是局部。如果你的逻辑是局部的,建议使用请求守卫等。
Attaching
Fairings
是需要注册的,需要通过Rocket
实例的attching方法注册。
看下例子:
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
实现Fairing
必须实现info这个方法,它会返回一个Info。这个Info
会被用于Rocket
指派fairing
和触发回调。
Requirements
另外实现Fairing
需要Send + Sync + 'static
,也就是线程安全。
Examples
我们有好几个get/post
请求,这里面有一些相同的操作,这里就可以抽出来放到全局。
注册了on_request
和on_response
两个回调,用来插入请求值和修改响应值。
完整的例子可见于:https://api.rocket.rs/v0.5-rc/rocket/fairing/trait.Fairing.html#example
这里还提供了字面量的快捷方法,比如:
AdHoc是一个内置的实现了Fairing
的结构体。
Rocket
还提供了单元/集成测试。
不过这就需要我们模拟请求了。
Rocket
有一个local模块,它包含了一个Client结构体可以用来创建LocalRequest。
使用例子:
Rocket
实例。上面第四步返回的response
的类型是LocalResponse。我们可以用它来校验。
status
: 返回http
状态码。content_type
: 返回Content-Type
。headers
: 返回响应的header
,是一个map
。into_string
: 将响应体数据转换成字符串。into_bytes
: 将响应体数据转换成Vec<u8>
。into_json
: 转换成JSON
格式。into_msgpack
: 转换成MessagePack
格式。来看个例子:
我们来搞个例子:
首先搭建下基础:
Setting Up
然后来创建测试环境
然后完善测试逻辑
Testing
然后就可以直接cargo test
了。
最终代码:
上面的例子你应该注意到了blocking
,是的,目前测试都是同步的,即使Rocket
自身是异步的。
同步测试比较简单,但是在某些场景就不太好用了,比如最常见的并发场景,需要模拟多个请求。
而Rocket
其实也提供了异步的api
:rocket::local::asynchronous。例子:https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/examples/testing/src/async_required.rs。
我们可以通过设置参数ROCKET_CODEGEN_DEBUG=1
环境变量来查看更详细的错误信息。
比如:
Rocket
系统配置是基于Figment这个crate
的。
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
则是最顶级的,会覆盖任何的配置文件下同个配置项。
Rocket
默认的配置provider
是 Config::figment()
。在rocket::build()
的时候会被使用。
配置文件的基础是key
也就是配置项。
下面配置内容的优先级逐级升高:
Config::default()
配置出来的,是兜底数据。Rocket.toml
或者TOML
文件路径配置在ROCKET_CONFIG
的环境变量。ROCKET_
前缀的环境变量。Rocket.toml
根据前面的内容,我们可以通过三种方式来自定义配置文件。
通过Rocket.toml
的不需要我们去自定义路径,Rocket
会自动去判断当前文件夹里是否存在,如果没有就会去父目录里找,如果还找不到就继续去父目录里找,直到根目录。
通过相对路径配置的ROCKET_CONFIG
的话同上。
而如果通过ROCKET_CONFIG
配置的路径是绝对路径的话,就不需要去递归查找。
一个配置文件中可以包含多种配置文件,比如:
而下面这个则是一个设置了所有项的配置文件
注意,如果不是必要的情况切勿去配置,用默认的即可。
Environment Variables
前面说过ROCKET_
为前缀的配置项优先级是高于Rocket.toml
的。
Rocket
会读取所有以ROCKET_
为前缀的环境变量,然后将_
后面的内容作为配置项的名字。比如:
格式也是toml
的。
Secret Key
这个前面私有cookies
的那块介绍过了,这里就不多说了。
Limits
用于限制数据类型最大允许接收的量。这里需要传一个字段table
,比如limits = { json = "10MiB" }
带单位和不带都可以,默认是32KiB
。
TLS
Rocket
内置支持TLS
的,不过需要导入:
可以通过tls.key
和 tls.certs
两个配置项来配置,对于default provider
来说,可以这样:
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
。
默认的配置如下:
Mutual TLS
Rocket
也支持双向TLS
客户端验证的模块mtls
。它提供一个请求守卫用于校验和获取客户端证书。
默认情况下,双向TLS
是禁止的并且客户端证书是不必要的。如果想要支持,需要引入:
然后配置tls.mutual
tls.mutual,ca_certs
配置的是CA
证书。
tls.mutual
也是要求传一个字典,最终也是会被反序列化成MutualTls
结构体:
key | required | type |
---|---|---|
ca_certs | yes | DER编码的X.509 TLS证书链的路径或字节。 |
mandatory | no | 控制客户端是否必须进行身份验证的布尔值。 |
当允许双向TLS
时,mtls::Certificate
请求守卫可以用来校验客户端证书。
例子: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
的值一般不要去改动它,除非硬件不支持。
我们可以自行拓展通过Rocket::figment()
暴露出来的配置,不过前提是实现了 Deserialize
。比如:
一般情况下,我们的配置项都是直接放到全局状态管理的,Rocket
也考虑到了这一点,它提供了一个AdHoc fairing
,这货我们前面接触过。它可以提取配置,输出错误以及存储到全局状态管理器中。比如:
我们可以通过rocket::custom()
自定义provider
,它替换了对 rocket::build()
的调用。
自定义provider
可以基于Config::figment()
或者 Config::default()
。
具体可见:https://docs.rs/figment/0.10/figment
另外需要注意,你可能需要直接用到figment
和serde
它们。rocket
默认导出了它俩,所以直接引用即可。不过rocket
导出的它俩可能是阉割版的,所以如果要用到一些特性,可能还得从包自身引入:
来看个自定义配置项的例子:
再来看个复杂一点的:
这里把能涉及到的配置文件都涉及到的,default
的,global
的,环境变量的。
接下来来搞个简单的文件存储系统,返回文件存储的路径。
Pastebin指的是文件存储站点,可以上传文件。
线上体验:https://paste.rs/
可以直接用curl
来访问,比如:
我们需要先设计接口:
upload
- #[post("/")]
通过request
的body
获取原始数据并且返回文件存储的url
。retrieve
- #[get("/<id>")]
返回上传的文件内容,通过id
访问。先搭建环境
然后配置下根目录的Cargo.toml
然后回到rocket-pastebin
中引入rocket
然后到main.rs
中:
然后试运行下cargo run -p rocket-pastebin
,应该是正常运行了。
在开始实现之前,我们需要确定俩问题:
demo
,直接就放到项目里就行了,那么新建一个和src
同级的文件夹upload
作为静态资源。hash
处理。我们这里就简单的通过随机数创建id
就行了。我们在src
下创建一个paste_id.rs
文件随机生成一个数,然后创建PasteId
实例自身存储id
,到时候访问的时候直接从里面拿即可。
别忘了在toml
中引入rand
最后在main.rs
中引入
本来我们还需要一个index
用来提供上传的入口,但是我们这里是基于curl
的,所以就没必要了。
我们先来实现通过id
获取上传内容的部分
通过传入的id
拼接存储目录,然后获取对应文件的内容,最后响应出去。
别忘了注册:
不过这里还有个问题:full path disclosure attack。如果我们的文件中有些文件是私有的,不允许外面访问的,或者说需要一定的权限才能访问,那么这个时候我们就有可能会被攻击。
那么这个时候我们就需要搞个请求守卫用来解决这个问题,回到paste_id.rs
文件中,我们需要给Paste
实现FromParam
:
这里面的代码并没有体现出解决方案,实际上我们需要加上的代码是black list
也就是黑名单,用来过滤需要权限访问的文件。
然后别忘了修改路由,
这样看就舒服很多了。
首先,我们需要接收的肯定是数据流
Streaming Data
我们需要使用前面说过的 Data
来做数据守卫,限制获取的量。
限制为128K
大小。然后通过uri!
这个宏生成路由路径,不过根据我们前面学的知识,这里我们还需要给retrieve
的参数id
的类型PasteId
实现UriDisplayPath
才行:
那么现在程序应该是可以跑了,我们运行下cargo run -p rocket-pastebin
然后我们需要借助curl
来实现上传,关于curl
如何在win10/win11
使用的,我之前go
的一篇文件中有说到,这里就不多说了,可以去翻看下。
我们现在rocket-pastebin
根目录下创建一个test.txt
文件,然后在里面随便写点东西
上传完之后我们再访问下
这样就可以了,虽然和粗糙。。。
完整代码可见:https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/examples/pastebin
又到了经典《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
让我想想。。。
编辑于 2023-05-11 15:01・IP 属地广东