Salvo 是一个基于 Rust 语言构建的异步、高性能、功能丰富的 Web 服务器框架。它设计简洁、模块化,易于使用和扩展,并提供了强大的路由、中间件、数据提取、WebSocket、TLS (支持 OpenSSL/Rustls 证书热重载)、HTTP/3、反向代理、请求超时控制等支持。
此示例演示了如何使用 Salvo 的
concurrency-limiter
中间件来限制可以同时处理的并发请求数量。这对于控制资源密集型操作(如文件上传)的负载非常有用。
此示例设置了两个文件上传端点:
/limited
:此路径应用了 max_concurrency(1)
中间件,意味着一次只允许处理一个上传请求。后续的请求将排队等待,直到当前请求完成。/unlimit
:此路径没有并发限制,可以同时处理任意数量的上传请求。两个端点都使用同一个 upload
处理器,该处理器模拟了一个耗时 10 秒的操作(tokio::time::sleep
),以清晰地展示并发限制的效果。index
处理器提供一个包含两个上传表单的 HTML 页面,分别指向这两个端点。
Cargo.toml
: 定义项目依赖,并通过 features = ["concurrency-limiter"]
启用了 Salvo 的并发限制器功能。src/main.rs
: 包含主要的应用程序逻辑:
index
处理器以提供 HTML 表单。upload
处理器来处理文件上传并模拟延迟。/limited
路径应用 max_concurrency(1)
中间件。temp
用于存储上传的文件。0.0.0.0:5800
。此示例展示了如何使用 Salvo 的 CSRF (跨站请求伪造) 保护功能,并将 CSRF token 存储在会话 (Session) 中。它演示了多种不同的加密/签名算法来保护 token。
为了实现 CSRF 保护,该示例依赖于 Salvo 的 session
功能来存储每个客户端的 CSRF token。主要步骤包括:
SessionHandler
,使用 MemoryStore
(内存存储) 和一个密钥来管理会话数据。FormFinder
指定从表单的哪个字段 (csrf_token
) 提取 token。bcrypt_session_csrf
, hmac_session_csrf
等)。get_page
) 中,通过 depot.csrf_token()
获取(或生成新的)CSRF token,并将其嵌入到返回的 HTML 表单中。post_page
) 中,Salvo 的 CSRF 中间件会自动验证请求中提交的 token 是否与会话中存储的匹配。如果验证通过,处理器逻辑才会执行;否则,将返回错误。处理器随后会再次生成新的 token 用于下一次请求。/bcrypt/
, /hmac/
, /aes_gcm/
, /ccp/
),每个路径应用对应算法的 CSRF 中间件。所有这些路径都共享 get_page
和 post_page
处理器。此示例的核心在于演示如何结合会话管理来实现有状态的 CSRF 保护,并提供了多种安全级别的算法选项。
Cargo.toml
: 引入 salvo
依赖,并启用 csrf
和 session
特性。同时需要 serde
用于表单数据的序列化/反序列化。src/main.rs
:
home
, get_page
, post_page
处理器。SessionHandler
和各种基于 Session 的 Csrf
中间件。get_page
中生成并显示 CSRF token。post_page
中解析表单数据(包含 csrf_token
),CSRF 中间件隐式执行验证。此示例演示了如何使用 Salvo 和 Quinn (一个 QUIC 协议实现) 来创建一个 WebTransport 服务器。WebTransport 是一种基于 HTTP/3 的现代双向通信协议,提供低延迟的数据传输,适用于实时应用。
该示例实现了一个简单的 echo 服务器,可以通过 WebTransport 连接:
QuinnListener
来处理 QUIC 连接,并通过 join
与一个标准的 TcpListener
(使用 Rustls 处理 TLS) 结合,允许服务器同时处理 HTTP/1.1 或 HTTP/2(用于提供静态文件和可能的备用连接)和 HTTP/3 (用于 WebTransport)。connect
处理器通过 req.web_transport_mut().await
获取 WebTransport 会话。session.open_bi
) 向客户端发送消息。session.accept_datagram()
接收不可靠的数据报,并在前面加上 "Response: " 后 echo 回去。session.accept_uni()
接收客户端发起的单向流,读取所有数据,然后通过服务器端打开的对应单向流 (session.open_uni
) 将数据分块 echo 回去。session.accept_bi()
接收客户端发起的双向流,将其分割为读写两半,然后同样将接收到的数据分块 echo 回去。StaticDir
提供了一个简单的 HTML/JS 客户端页面 (static/client.html
, static/client.js
),用户可以通过浏览器访问此页面来与 WebTransport 服务器交互。此示例展示了 Salvo 处理高级网络协议 (如 WebTransport) 的能力,以及如何管理其不同的通信模式(数据报、单向/双向流)。
Cargo.toml
:
salvo
的 quinn
(提供 WebTransport 和 HTTP/3 支持) 和 serve-static
(提供静态文件服务) 特性。anyhow
(错误处理), bytes
(数据处理), futures-util
(异步流处理) 等辅助库。src/main.rs
:
connect
处理器,核心的 WebTransport 会话处理逻辑。echo_stream
和 send_chunked
辅助函数处理流数据。RustlsConfig
加载证书。QuinnListener
和 TcpListener
,并将它们绑定到端口 5800。/counter
路径指向 connect
处理器,并将其他路径用于提供静态文件。certs/
: 包含用于本地测试的自签名证书 (cert.pem
) 和私钥 (key.pem
)。static/
:
client.html
: 提供给用户的 WebTransport 客户端界面。client.js
, client.css
: 客户端的 JavaScript 逻辑和样式。此示例演示了 Salvo 如何捕获和处理来自处理器 (Handler) 的不同类型的错误,包括标准库错误、
anyhow::Error
、eyre::Report
以及自定义错误类型。
Salvo 处理器可以返回 Result<T, E>
。如果返回 Ok(T)
,则正常处理 T
(通常 T
实现了 Writer
trait 来写入响应)。如果返回 Err(E)
,Salvo 会尝试处理这个错误 E
:
salvo::Error
, std::io::Error
等)有内置的处理逻辑,通常会转换成合适的 HTTP 状态码和响应体。Writer
Trait 处理: 如果错误类型 E
实现了 salvo::Writer
trait,Salvo 会调用其 write
方法,允许你完全自定义错误对应的 HTTP 响应。示例中的 CustomError
就是这样处理的,它实现了 Writer
trait,将响应状态码设置为 500 并写入自定义消息 "custom error"。anyhow
和 eyre
: 当启用 salvo
的 anyhow
或 eyre
特性时,Salvo 会自动为 anyhow::Error
和 eyre::Report
提供 Writer
实现。这意味着你可以直接在处理器中返回 Result<(), anyhow::Error>
或 Result<(), eyre::Result>
,Salvo 会捕获这些错误并生成一个通常包含错误信息的 500 Internal Server Error 响应。此示例通过三个不同的处理器展示了这几种情况:
handle_anyhow
: 返回 anyhow::anyhow!("handled anyhow error")
。handle_eyre
: 返回 eyre::Report::msg("handled eyre error")
。handle_custom
: 返回自定义的 Err(CustomError)
.访问对应的路径 (/anyhow
, /eyre
, /custom
) 会触发相应的错误,并观察 Salvo 如何根据错误类型生成响应。
Cargo.toml
:
salvo
的 anyhow
和 eyre
特性以集成这两种错误处理库。anyhow
和 eyre
依赖。src/main.rs
:
CustomError
结构体。CustomError
实现 Writer
trait来自定义错误响应。handle_anyhow
, handle_eyre
, handle_custom
三个处理器,分别返回不同类型的错误。此示例演示了如何让 Salvo 服务器同时监听多个 TCP 地址和端口。
通过 TcpListener
的 join()
方法,可以将多个监听器配置组合在一起。当调用 bind().await
时,Salvo 会尝试绑定所有指定的地址和端口。服务器随后会接受来自任何一个监听器的连接。
在这个具体的例子中:
TcpListener::new("0.0.0.0:5800")
创建一个监听 5800 端口的配置。.join(TcpListener::new("0.0.0.0:5801"))
将监听 5801 端口的配置加入。bind().await
实际执行绑定操作,使服务器同时在 5800 和 5801 端口上接受连接。访问 http://localhost:5800
或 http://localhost:5801
都会路由到同一个 hello
处理器,并得到 "Hello World" 响应。
Cargo.toml
: 包含基本的 salvo
和 tokio
依赖。src/main.rs
:
hello
处理器。TcpListener::new(...).join(...)
配置多端口监听。bind().await
绑定监听器。此示例展示了如何使用 Salvo 的
oapi
(OpenAPI) 功能来构建一个带有交互式 API 文档的 TODO List RESTful API。
核心特性和步骤:
Cargo.toml
中启用 salvo
的 oapi
特性。Todo
结构体,并使用 #[derive(Serialize, Deserialize, ToSchema)]
让它能被序列化/反序列化,并自动生成 OpenAPI Schema。可以使用 #[salvo(schema(example = ...))]
提供示例值。#[endpoint]
宏标记处理 HTTP 请求的函数。这个宏会自动处理请求解析、响应序列化,并为 OpenAPI 文档收集元数据。oapi::extract::*
中的提取器(如 QueryParam
, JsonBody
, PathParam
)来声明和获取请求参数/体。这些提取器会自动将参数信息添加到 OpenAPI 文档。#[endpoint]
中添加 tags(...)
, status_codes(...)
, parameters(...)
等属性来丰富 OpenAPI 文档信息。list_todos
, create_todo
, update_todo
, delete_todo
,它们操作一个内存中的 STORE
(使用 tokio::sync::Mutex<Vec<Todo>>
) 来模拟数据库。OpenApi
实例,提供 API 的标题和版本。.merge_router(&router)
将路由信息和从 #[endpoint]
宏收集到的元数据合并到 OpenAPI 文档对象中。OpenApi
文档对象转换为一个路由,通常提供 JSON 格式的文档 (doc.into_router("/api-doc/openapi.json")
)。SwaggerUi
, Scalar
, RapiDoc
, ReDoc
。它们都指向之前生成的 JSON 文档路径。访问 /swagger-ui
, /scalar
, /rapidoc
, 或 /redoc
可以查看和交互式地测试 TODO API。
Cargo.toml
: 启用 salvo
的 oapi
特性,并包含 serde
(数据序列化) 和 tokio
(异步运行时) 依赖。src/main.rs
:
Todo
数据结构和内存 STORE
。#[endpoint]
定义 CRUD 操作的处理器 (list_todos
, create_todo
, update_todo
, delete_todo
)。Router
注册业务 API 路径。OpenApi
实例并合并路由信息。index
处理器提供一个简单的 HTML 页面链接到各个文档 UI。此示例演示了如何在 Salvo 应用中集成 MongoDB 数据库,实现基本的 CRUD (创建、读取、更新、删除 - 此示例主要展示创建和读取) 操作。
主要步骤和概念:
Cargo.toml
中添加 mongodb
crate。main
函数中,使用 mongodb::Client::with_uri_str
连接到 MongoDB 实例。连接 URI 通常从环境变量读取或使用默认值。std::sync::OnceLock
将 Client
实例存储为全局静态变量 MONGODB_CLIENT
。提供了一个辅助函数 get_mongodb_client()
来获取它。User
结构体,并使用 #[derive(Debug, Deserialize, Serialize)]
使其可以与 BSON (MongoDB 的文档格式) 进行相互转换。注意 _id
字段通常是 Option<ObjectId>
类型。create_username_index
函数,在启动时为 users
集合的 username
字段创建一个唯一索引,以确保用户名的唯一性并提高查询性能。add_user
:
User
数据 (req.parse_json::<User>()
)。client.database(DB_NAME).collection::<Document>(COLL_NAME)
)。注意这里使用了 Document
类型,也可以直接用 User
类型 если BSON 结构匹配。User
数据转换为 BSON Document
(doc!{ ... }
)。coll_users.insert_one()
将文档插入数据库。get_users
:
User
类型的集合 (collection::<User>(COLL_NAME)
)。coll_users.find(None, None)
查询所有文档,返回一个 Cursor
。try_stream_ext::TryStreamExt
(通过 cursor.try_next().await?
) 迭代 Cursor
,将结果收集到 Vec<User>
中。res.render(Json(vec_users))
)。get_user
:
username
(req.param::<String>("username")
)。coll_users.find_one(doc! { "username": &username }, None)
按用户名查询单个文档。Router
并将相应的 HTTP 方法和路径 (/users
, /users/{username}
) 映射到对应的处理器。Error
枚举来包装 mongodb::error::Error
,并为其实现了 Writer
trait(尽管在此示例中 write
方法为空,实际应用中应提供更有意义的错误响应)。处理器返回 AppResult<T>
(即 Result<T, Error>
)。Cargo.toml
: 添加 mongodb
, serde
, serde_json
, thiserror
(用于定义自定义错误) 依赖。futures
通常是 mongodb
的间接依赖,这里显式列出可能用于 TryStreamExt
。src/main.rs
:
User
模型和 Error
枚举。Error
的 Writer
trait。OnceLock
中。add_user
, get_users
, get_user
处理器,包含数据库操作逻辑。create_username_index
函数。此示例展示了如何在 Salvo 中处理 WebSocket 连接。WebSocket 提供了一种在客户端和服务器之间进行全双工通信的协议,常用于实时应用。
主要步骤:
Cargo.toml
中启用 salvo
的 websocket
特性。Upgrade: websocket
, Connection: Upgrade
)来请求协议升级。connect
)。salvo::websocket::WebSocketUpgrade::new().upgrade(req, res, |mut ws| async move { ... })
来处理升级请求。upgrade
方法接收一个闭包,该闭包在 WebSocket 连接成功建立后执行。闭包接收一个 WebSocket
对象 ws
作为参数。async move
块中,你可以使用 ws.recv().await
来异步地接收来自客户端的消息(Message
类型),并使用 ws.send(msg).await
来异步地向客户端发送消息。recv()
可能返回的 None
(表示连接已关闭) 或 Err
(表示发生错误)。同样,send()
也可能返回 Err
(通常表示客户端已断开连接)。req.parse_queries::<User>()
),并将这些数据(如用户信息)传递到 WebSocket 处理闭包中使用。/ws
)添加到路由器。INDEX_HTML
),其中使用 JavaScript 的 WebSocket
API 连接到服务器的 /ws
端点。它还演示了如何在 URL 查询字符串中传递参数 (?id=123&name=chris
)。Cargo.toml
: 启用 salvo
的 websocket
特性。需要 serde
用于解析查询参数中的 User
结构。futures-util
可能被 salvo
内部或 WebSocket 处理需要。src/main.rs
:
User
结构体用于从查询参数解析数据。connect
处理器,使用 WebSocketUpgrade
处理连接升级和后续的 WebSocket 消息收发(echo 逻辑)。index
处理器提供包含客户端 JavaScript 的 HTML 页面。/ws
指向 connect
处理器。此示例演示了如何使用 Salvo 实现 Server-Sent Events (SSE)。SSE 是一种允许服务器向客户端单向推送更新的标准 HTTP 协议,常用于实时通知或数据流。
核心概念:
Cargo.toml
中启用 salvo
的 sse
特性。Content-Type: text/event-stream
),由一系列事件组成。event: <event_name>
, data: <event_data>
, id: <event_id>
, retry: <reconnection_time>
。事件之间用空行分隔。Salvo
提供了 SseEvent
结构体来方便地构建这些事件。futures_util::Stream<Item = Result<SseEvent, E>>
的异步流,其中 E
是某种错误类型(如果是 infallible stream,可以用 std::convert::Infallible
)。tokio::time::interval
创建一个每秒触发一次的定时器。tokio_stream::wrappers::IntervalStream
将定时器转换为一个异步流。.map()
处理流中的每个项(定时器触发信号),递增一个计数器 counter
,并调用 sse_counter(counter)
函数将计数器值包装成一个 SseEvent
(具体是只包含 data
字段的事件)。handle_tick
)中,获取到准备好的事件流 event_stream
。salvo::sse::stream(res, event_stream)
。这个函数会设置正确的响应头 (Content-Type: text/event-stream
等),并将事件流中的每个 SseEvent
格式化后写入响应体。EventSource
API 来连接 SSE 端点并接收事件。本示例未包含客户端代码,但标准的 EventSource
用法即可。此示例实现了一个简单的计数器服务,每秒向连接的客户端发送一个包含递增数字的 SSE 事件。
Cargo.toml
: 启用 salvo
的 sse
特性。需要 tokio-stream
用于将 tokio::time::interval
转换为流,以及 futures-util
来操作流 (StreamExt
)。src/main.rs
:
sse_counter
函数,将 u64
转换为 Result<SseEvent, Infallible>
。handle_tick
处理器:
IntervalStream
作为事件源。.map()
将定时器信号转换为 SseEvent
结果流。sse::stream()
将事件流发送给客户端。/ticks
指向 handle_tick
。这是一个最简单的 Salvo OpenAPI (
oapi
) 示例,演示了如何为一个基本的 "Hello World" 端点自动生成 OpenAPI 文档。
核心步骤:
Cargo.toml
中启用 salvo
的 oapi
特性。#[endpoint]
宏标记 hello
函数。
name
(类型 QueryParam<String, false>
)。QueryParam
是 salvo::oapi::extract
提供的提取器,它会自动从请求中提取参数,并将其信息添加到 OpenAPI 文档。false
表示该参数是可选的。String
。#[endpoint]
宏会确保返回值被正确处理并添加到 OpenAPI 文档的响应部分。Router
并注册 hello
端点到 /hello
路径。OpenApi
实例,提供 API 名称和版本。.merge_router(&router)
合并路由和端点元数据。OpenApi
对象转换为提供 OpenAPI JSON 的路由 (doc.into_router("/api-doc/openapi.json")
)。SwaggerUi
路由 (SwaggerUi::new("/api-doc/openapi.json").into_router("/swagger-ui")
) 以提供交互式 UI。访问 /swagger-ui
可以看到自动生成的文档,其中包含 /hello
端点,描述了其可选的 name
查询参数和返回的字符串响应。
Cargo.toml
: 启用 salvo
的 oapi
特性。src/main.rs
:
#[endpoint]
定义 hello
处理器,使用 QueryParam
提取参数。OpenApi
实例并合并路由。此示例演示了如何使用
rust-embed
crate 将静态文件(如 HTML, CSS, JS, 图像)直接嵌入到编译后的二进制文件中,并通过 Salvo 提供这些嵌入的文件服务。这对于创建单文件可执行分发很有用。
主要步骤:
Cargo.toml
中添加 rust-embed
crate 和 salvo
(需要 serve-static
特性,虽然这里没直接用 StaticDir
,但 EmbeddedFileExt
可能依赖相关功能)。Assets
)。#[derive(RustEmbed)]
宏标记该结构体。#[folder = "static"]
属性指定包含要嵌入文件的目录(相对于 Cargo.toml
的路径)。rust-embed
会在编译时扫描此目录并将文件内容包含在内。serve_file
)。"{**rest}"
捕获路径的剩余部分作为文件名。Assets::get(&path)
尝试从嵌入的资源中获取文件。Assets::get()
返回一个 Option<rust_embed::EmbeddedFile>
。Some(file)
):
file.render(req, res)
。EmbeddedFileExt
trait(由 salvo::serve_static::EmbeddedFileExt
提供,需要 use
进来)为 EmbeddedFile
添加了 render
方法。这个方法会自动设置合适的 Content-Type
响应头(基于文件扩展名或 Mime Guess)并将文件内容写入响应体。None
):
StatusCode::NOT_FOUND
。Router::with_path("{**rest}")
) 指向 serve_file
处理器。static/
目录下文件的路径(如 /test1.txt
, /test2.txt
)将返回嵌入的文件内容。Cargo.toml
: 添加 rust-embed
和 salvo
(带有 serve-static
特性) 依赖。src/main.rs
:
Assets
结构体,使用 #[derive(RustEmbed)]
和 #[folder = "static"]
。salvo::serve_static::EmbeddedFileExt
。serve_file
处理器,使用 Assets::get()
获取嵌入文件,并使用 file.render()
发送响应。serve_file
。static/
: 包含要嵌入到二进制文件中的静态文件。
此示例演示了如何使用 Salvo 配置基于 Rustls 的 TLS,并且能够动态地重新加载 TLS 证书和私钥,而无需重启服务器。这对于证书自动续期(例如使用 ACME)后更新配置非常有用。
核心机制:
Cargo.toml
中启用 salvo
的 rustls
特性。TcpListener::rustls()
方法接受一个能产生 RustlsConfig
的异步流 (Stream)。Salvo 会在需要新的 TLS 配置时(例如,当新的客户端连接尝试建立 TLS 握手时)从此流中获取最新的配置。async_stream
: 示例使用了 async_stream::stream!
宏来方便地创建一个无限循环的异步流:
loop { ... }
: 无限循环。yield load_config();
: 在每次循环中,调用 load_config()
函数加载当前的证书和密钥文件,创建一个新的 RustlsConfig
实例,并通过 yield
将其发送到流中。tokio::time::sleep(Duration::from_secs(60)).await;
: 等待 60 秒。这意味着服务器大约每分钟会尝试重新加载一次配置。你可以根据需要调整这个间隔,或者使用更复杂的逻辑(如文件系统监控或信号通知)来触发重新加载。load_config()
函数负责从指定路径(这里使用 include_bytes!
在编译时嵌入证书,实际应用中通常是从文件系统读取)加载证书和私钥,并创建一个 RustlsConfig
实例。TcpListener::new(...).rustls(stream).bind().await
完成监听器的设置和绑定。然后启动服务器。当服务器运行时,如果 certs/cert.pem
或 certs/key.pem
文件被更新,服务器将在下一次执行 yield load_config()
时(最多等待 60 秒)加载新的配置,并开始使用新的证书处理后续的 TLS 连接,整个过程无需中断服务。
Cargo.toml
: 启用 salvo
的 rustls
特性。添加 async-stream
依赖用于创建配置流。src/main.rs
:
hello
处理器。load_config()
函数,负责加载证书/密钥并创建 RustlsConfig
。main
函数中:
async_stream::stream!
创建一个定期产生 RustlsConfig
的异步流。TcpListener::new(...).rustls()
。certs/
: 包含初始的证书 (cert.pem
) 和私钥 (key.pem
) 文件。此示例演示了 Salvo 的
craft
功能,它提供了一个#[craft]
宏,可以方便地将结构体的方法转换为 Salvo 的处理器 (Handler) 或端点 (Endpoint),简化代码编写,特别是在需要将状态注入处理器时。
主要概念:
Cargo.toml
中启用 salvo
的 craft
特性。通常与 oapi
一起使用,因为 craft
可以自动生成 OpenAPI 信息。#[craft]
宏: 应用在 impl Opts { ... }
块上。#[craft(handler)]
: 将标记的方法转换为一个 Salvo Handler
。
&self
或 &mut self
来访问结构体实例的状态。QueryParam
。Writer
的类型,或者 Result
。add1
方法接收 &self
,访问 self.state
,并接收两个 QueryParam
。#[craft(endpoint)]
: 将标记的方法转换为一个 Salvo Endpoint
。这类似于 #[endpoint]
宏,会自动处理更多事情,特别是与 OpenAPI 的集成。
&self
, &mut self
, 甚至 self: Arc<Self>
或 self: &Arc<Self>
来处理共享状态(如示例中的 add2
)。Result
。#[endpoint]
一样添加 OpenAPI 相关的属性,如 responses(...)
(如示例中的 add3
)。add3
是一个静态方法 (pub fn add3(...)
),它也被标记为 endpoint
,表明 #[craft]
也可以用于非实例方法。handler
或 endpoint
,如果方法接收 &self
或 &mut self
,你需要将结构体的实例(或其引用)传递给生成的处理器。示例中是 opts.add1()
,这里 add1()
是 #[craft]
生成的函数,它内部会捕获 opts
的引用。endpoint
接收 self: Arc<Self>
,你需要传递一个 Arc
包裹的实例,如 opts.add2()
(这里 opts
是 Arc<Opts>
)。craft
生成的函数添加到路由中。craft
简化了将面向对象的方法集成到 Salvo 请求处理流程中的过程,使得代码更模块化和易于管理。
Cargo.toml
: 启用 salvo
的 craft
和 oapi
特性。src/main.rs
:
Opts
结构体包含状态 state
。#[craft]
实现 Opts
的方法:
new
: 构造函数。add1
: 使用 #[craft(handler)]
,接收 &self
。add2
: 使用 #[craft(endpoint)]
,接收 self: Arc<Self>
。add3
: 使用 #[craft(endpoint)]
,是一个静态方法。Arc<Opts>
实例。opts.add1()
, opts.add2()
, Opts::add3()
添加到不同路径。此示例演示了 Salvo 的
affix-state
功能,允许你在请求处理链路的早期将共享状态(数据)注入到请求的Depot
中,以便后续的中间件或处理器可以访问这些状态。
核心概念:
Cargo.toml
中启用 salvo
的 affix-state
特性。Depot
: Salvo 的 Depot
是一个与每个请求关联的类型化数据存储。它用于在处理请求的不同阶段(中间件、处理器)之间传递数据。affix_state::inject()
: 这是一个中间件构造器。
affix_state::inject(state)
: 创建一个中间件,该中间件会将提供的 state
对象(需要是 Clone + Send + Sync + 'static
)注入到 Depot
中。后续可以通过 depot.obtain::<StateType>()
来获取该状态的克隆。.inject(another_state)
: 可以链式调用 .inject()
来注入多个不同类型的状态。示例中注入了 Config
和 Arc<State>
。.insert(key, value)
: 除了注入可以通过类型获取的状态外,还可以使用 .insert()
注入键值对数据。示例中注入了 "custom_data"
键和 "I love this world!"
值。后续可以通过 depot.get::<ValueType>("key")
来获取。affix_state
中间件通常通过 hoop()
添加到 Router
或服务器级别,确保在请求到达最终处理器之前状态已经被注入。&mut Depot
作为参数。depot.obtain::<StateType>()
获取通过 inject()
注入的状态。返回的是 Option<&StateType>
,通常可以 unwrap()
如果你确定状态一定会被注入。注意,如果是注入的 Arc<T>
,获取到的也是 &Arc<T>
。depot.get::<&ValueType>("key")
获取通过 insert()
注入的数据。注意类型通常是 &ValueType
,因为 Depot
存储的是引用。State
包含一个 Mutex<Vec<String>>
,并通过 Arc
包裹后注入。这允许多个并发请求安全地访问和修改同一个 State
实例(通过 Arc
共享所有权,通过 Mutex
控制并发访问)。affix-state
提供了一种类型安全且灵活的方式来管理和传递请求处理所需的共享数据和配置。
Cargo.toml
: 启用 salvo
的 affix-state
特性。src/main.rs
:
Config
结构体(应用配置)。State
结构体(包含共享的可变状态 Mutex<Vec<String>>
)。hello
处理器,接收 &mut Depot
。hello
处理器内部:
depot.obtain::<Config>()
获取配置。depot.get::<&str>("custom_data")
获取插入的数据。depot.obtain::<Arc<State>>()
获取共享状态,并操作其内部的 Mutex
。main
函数中:
Config
实例和 Arc<State>
实例。Router
。hoop(affix_state::inject(...).inject(...).insert(...))
将状态和数据注入应用到路由。hello
处理器。此示例演示了如何使用 Salvo 的
size-limiter
中间件来限制传入请求体的大小,防止因接收过大的请求(如超大文件上传)而耗尽服务器资源。
主要步骤:
Cargo.toml
中启用 salvo
的 size-limiter
特性。salvo::prelude::max_size(limit_in_bytes)
函数创建一个限制请求体大小的中间件。这个函数是 SizeLimiter::new(limit_in_bytes)
的便捷写法。hoop()
应用到需要限制的 Router
或特定路径上。Router::new().hoop(max_size(1024 * 1024 * 10)).path("limited").post(upload)
将 10 MiB 的大小限制应用到了 /limited
路径的 POST 请求。max_size
的路径时:
Content-Length
头部(如果存在)小于或等于限制,请求会正常传递给后续的处理器。Content-Length
头部指示的大小超过了限制,Salvo 会立即拒绝请求,通常返回 413 Payload Too Large
状态码,而不会读取请求体内容,也不会调用后续处理器。Content-Length
头部(例如使用了 Transfer-Encoding: chunked
),Salvo 会在读取请求体数据时进行检查。一旦读取的数据量超过限制,它会停止读取并返回 413 Payload Too Large
。/unlimit
路径,没有应用大小限制,用于对比效果。使用 size-limiter
是保护服务器免受拒绝服务攻击(通过发送大量数据)或意外超大请求的重要措施。
Cargo.toml
: 启用 salvo
的 size-limiter
特性。src/main.rs
:
index
处理器提供 HTML 上传表单。upload
处理器处理文件上传。main
函数中:
temp
。Router
。/limited
路径创建一个子路由,并使用 hoop(max_size(10 * 1024 * 1024))
应用 10 MiB 的大小限制,然后注册 upload
处理器。/unlimit
路径创建一个子路由,直接注册 upload
处理器(无限制)。index
处理器。此示例演示了如何使用 Salvo 和 OpenSSL 库来为服务器启用 TLS (HTTPS) 加密。
主要步骤:
Cargo.toml
中启用 salvo
的 openssl
特性。确保你的系统安装了 OpenSSL 开发库(如 libssl-dev
on Debian/Ubuntu, openssl-devel
on Fedora/CentOS)。cert.pem
) 和对应的私钥 (key.pem
)。示例中使用 include_bytes!
在编译时嵌入它们,实际部署中通常从文件系统加载。salvo::conn::openssl::Keycert::new()
创建一个证书/密钥对对象。.with_cert(cert_bytes)
和 .with_key(key_bytes)
加载证书和密钥的字节数据。salvo::conn::openssl::OpensslConfig::new(keycert)
创建 OpenSSL TLS 配置对象。TcpListener
实例,指定要监听的地址和端口。.openssl(config)
方法,将之前创建的 OpensslConfig
应用到监听器。.bind().await
绑定监听器。现在这个监听器将接受 TLS 连接。acceptor
创建并启动 Server
。服务器现在将在指定端口上监听 HTTPS 连接,使用提供的证书进行加密通信。
Cargo.toml
: 启用 salvo
的 openssl
特性。src/main.rs
:
hello
处理器。main
函数中:
include_bytes!
加载证书和密钥。Keycert
和 OpensslConfig
。TcpListener
,调用 .openssl(config)
应用 TLS 配置。certs/
: 包含示例用的证书 (cert.pem
) 和私钥 (key.pem
)。此示例结合了 WebTransport (基于 Quinn/HTTP3) 和 ACME (通过 Let's Encrypt 获取证书) 的 HTTP-01 挑战类型。它演示了如何设置一个可以通过公共域名访问的、使用有效 TLS 证书的 WebTransport 服务器,并且证书是自动获取和管理的。
关键配置和流程:
Cargo.toml
中需要启用 salvo
的 acme
, quinn
(提供 WebTransport/HTTP3), serve-static
(用于 ACME HTTP-01 挑战通常需要服务于特定路径,以及提供客户端页面) 特性。/.well-known/acme-challenge/
) 上响应 Let's Encrypt 服务器的 HTTP 请求,以证明你对域名的控制权。TcpListener::new("0.0.0.0:443")
: 监听 HTTPS 端口 443。.acme()
: 启用 ACME 功能。.cache_path("temp/letsencrypt")
: 指定存储获取到的证书和 ACME 账户信息的路径。.add_d