Salvo 是一个基于 Rust 语言构建的异步、高性能、功能丰富的 Web 服务器框架。它设计简洁、模块化,易于使用和扩展,并提供了强大的路由、中间件、数据提取、WebSocket、TLS (支持 OpenSSL/Rustls 证书热重载)、HTTP/3、反向代理、请求超时控制等支持。

concurrency-limiter

此示例演示了如何使用 Salvo 的 concurrency-limiter 中间件来限制可以同时处理的并发请求数量。这对于控制资源密集型操作(如文件上传)的负载非常有用。

此示例设置了两个文件上传端点:

  1. /limited:此路径应用了 max_concurrency(1) 中间件,意味着一次只允许处理一个上传请求。后续的请求将排队等待,直到当前请求完成。
  2. /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

csrf-session-store

此示例展示了如何使用 Salvo 的 CSRF (跨站请求伪造) 保护功能,并将 CSRF token 存储在会话 (Session) 中。它演示了多种不同的加密/签名算法来保护 token。

为了实现 CSRF 保护,该示例依赖于 Salvo 的 session 功能来存储每个客户端的 CSRF token。主要步骤包括:

  1. 会话设置: 配置 SessionHandler,使用 MemoryStore (内存存储) 和一个密钥来管理会话数据。
  2. CSRF Token 生成与验证:
    • 使用 FormFinder 指定从表单的哪个字段 (csrf_token) 提取 token。
    • 初始化多种 CSRF 保护中间件实例,每种使用不同的算法(Bcrypt, HMAC, AES-GCM, ChaCha20Poly1305),并将它们配置为使用会话存储 (bcrypt_session_csrf, hmac_session_csrf 等)。
    • 在 GET 请求处理器 (get_page) 中,通过 depot.csrf_token() 获取(或生成新的)CSRF token,并将其嵌入到返回的 HTML 表单中。
    • 在 POST 请求处理器 (post_page) 中,Salvo 的 CSRF 中间件会自动验证请求中提交的 token 是否与会话中存储的匹配。如果验证通过,处理器逻辑才会执行;否则,将返回错误。处理器随后会再次生成新的 token 用于下一次请求。
  3. 路由: 设置不同的路径 (/bcrypt/, /hmac/, /aes_gcm/, /ccp/),每个路径应用对应算法的 CSRF 中间件。所有这些路径都共享 get_pagepost_page 处理器。

此示例的核心在于演示如何结合会话管理来实现有状态的 CSRF 保护,并提供了多种安全级别的算法选项。

文件列表

  • Cargo.toml: 引入 salvo 依赖,并启用 csrfsession 特性。同时需要 serde 用于表单数据的序列化/反序列化。
  • src/main.rs:
    • 定义 home, get_page, post_page 处理器。
    • 配置 SessionHandler 和各种基于 Session 的 Csrf 中间件。
    • get_page 中生成并显示 CSRF token。
    • post_page 中解析表单数据(包含 csrf_token),CSRF 中间件隐式执行验证。
    • 设置路由,将不同的 CSRF 中间件应用到相应的路径。
    • 启动服务器。

webtransport

此示例演示了如何使用 Salvo 和 Quinn (一个 QUIC 协议实现) 来创建一个 WebTransport 服务器。WebTransport 是一种基于 HTTP/3 的现代双向通信协议,提供低延迟的数据传输,适用于实时应用。

该示例实现了一个简单的 echo 服务器,可以通过 WebTransport 连接:

  1. 协议基础: 它依赖于 QUIC (HTTP/3) 协议,因此需要 TLS 加密。示例中使用自签名证书进行本地测试。
  2. 监听器: 使用 QuinnListener 来处理 QUIC 连接,并通过 join 与一个标准的 TcpListener (使用 Rustls 处理 TLS) 结合,允许服务器同时处理 HTTP/1.1 或 HTTP/2(用于提供静态文件和可能的备用连接)和 HTTP/3 (用于 WebTransport)。
  3. 连接处理:
    • connect 处理器通过 req.web_transport_mut().await 获取 WebTransport 会话。
    • 服务器在连接建立后立即主动打开一个双向流 (session.open_bi) 向客户端发送消息。
    • 服务器进入循环,等待并处理来自客户端的不同类型的交互:
      • 数据报 (Datagrams): session.accept_datagram() 接收不可靠的数据报,并在前面加上 "Response: " 后 echo 回去。
      • 单向流 (Unidirectional Streams): session.accept_uni() 接收客户端发起的单向流,读取所有数据,然后通过服务器端打开的对应单向流 (session.open_uni) 将数据分块 echo 回去。
      • 双向流 (Bidirectional Streams): session.accept_bi() 接收客户端发起的双向流,将其分割为读写两半,然后同样将接收到的数据分块 echo 回去。
  4. 静态文件服务: 使用 StaticDir 提供了一个简单的 HTML/JS 客户端页面 (static/client.html, static/client.js),用户可以通过浏览器访问此页面来与 WebTransport 服务器交互。

此示例展示了 Salvo 处理高级网络协议 (如 WebTransport) 的能力,以及如何管理其不同的通信模式(数据报、单向/双向流)。

文件列表

  • Cargo.toml:
    • 启用 salvoquinn (提供 WebTransport 和 HTTP/3 支持) 和 serve-static (提供静态文件服务) 特性。
    • 包含 anyhow (错误处理), bytes (数据处理), futures-util (异步流处理) 等辅助库。
  • src/main.rs:
    • 定义 connect 处理器,核心的 WebTransport 会话处理逻辑。
    • 定义 echo_streamsend_chunked 辅助函数处理流数据。
    • 配置 RustlsConfig 加载证书。
    • 设置 QuinnListenerTcpListener,并将它们绑定到端口 5800。
    • 设置路由,将 /counter 路径指向 connect 处理器,并将其他路径用于提供静态文件。
    • 启动服务器。
  • certs/: 包含用于本地测试的自签名证书 (cert.pem) 和私钥 (key.pem)。
  • static/:
    • client.html: 提供给用户的 WebTransport 客户端界面。
    • client.js, client.css: 客户端的 JavaScript 逻辑和样式。

catch-error

此示例演示了 Salvo 如何捕获和处理来自处理器 (Handler) 的不同类型的错误,包括标准库错误、anyhow::Erroreyre::Report 以及自定义错误类型。

Salvo 处理器可以返回 Result<T, E>。如果返回 Ok(T),则正常处理 T(通常 T 实现了 Writer trait 来写入响应)。如果返回 Err(E),Salvo 会尝试处理这个错误 E

  1. 内置错误处理: Salvo 对一些常见的错误类型(如 salvo::Error, std::io::Error 等)有内置的处理逻辑,通常会转换成合适的 HTTP 状态码和响应体。
  2. 通过 Writer Trait 处理: 如果错误类型 E 实现了 salvo::Writer trait,Salvo 会调用其 write 方法,允许你完全自定义错误对应的 HTTP 响应。示例中的 CustomError 就是这样处理的,它实现了 Writer trait,将响应状态码设置为 500 并写入自定义消息 "custom error"。
  3. 集成 anyhoweyre: 当启用 salvoanyhoweyre 特性时,Salvo 会自动为 anyhow::Erroreyre::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:
    • 启用 salvoanyhoweyre 特性以集成这两种错误处理库。
    • 包含 anyhoweyre 依赖。
  • src/main.rs:
    • 定义 CustomError 结构体。
    • CustomError 实现 Writer trait来自定义错误响应。
    • 定义 handle_anyhow, handle_eyre, handle_custom 三个处理器,分别返回不同类型的错误。
    • 设置路由,将路径映射到相应的错误处理器。
    • 启动服务器。

join-listeners

此示例演示了如何让 Salvo 服务器同时监听多个 TCP 地址和端口。

通过 TcpListenerjoin() 方法,可以将多个监听器配置组合在一起。当调用 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:5800http://localhost:5801 都会路由到同一个 hello 处理器,并得到 "Hello World" 响应。

文件列表

  • Cargo.toml: 包含基本的 salvotokio 依赖。
  • src/main.rs:
    • 定义简单的 hello 处理器。
    • 使用 TcpListener::new(...).join(...) 配置多端口监听。
    • bind().await 绑定监听器。
    • 启动服务器,为所有监听的端口提供相同的路由服务。

oapi-todos

此示例展示了如何使用 Salvo 的 oapi (OpenAPI) 功能来构建一个带有交互式 API 文档的 TODO List RESTful API。

核心特性和步骤:

  1. 启用 OAPI: 在 Cargo.toml 中启用 salvooapi 特性。
  2. 定义数据模型: 创建 Todo 结构体,并使用 #[derive(Serialize, Deserialize, ToSchema)] 让它能被序列化/反序列化,并自动生成 OpenAPI Schema。可以使用 #[salvo(schema(example = ...))] 提供示例值。
  3. 定义 API 端点:
    • 使用 #[endpoint] 宏标记处理 HTTP 请求的函数。这个宏会自动处理请求解析、响应序列化,并为 OpenAPI 文档收集元数据。
    • 使用 oapi::extract::* 中的提取器(如 QueryParam, JsonBody, PathParam)来声明和获取请求参数/体。这些提取器会自动将参数信息添加到 OpenAPI 文档。
    • 可以在 #[endpoint] 中添加 tags(...), status_codes(...), parameters(...) 等属性来丰富 OpenAPI 文档信息。
  4. 实现业务逻辑: 编写端点函数的具体逻辑,如此示例中的 list_todos, create_todo, update_todo, delete_todo,它们操作一个内存中的 STORE (使用 tokio::sync::Mutex<Vec<Todo>>) 来模拟数据库。
  5. 生成 OpenAPI 文档:
    • 创建一个 OpenApi 实例,提供 API 的标题和版本。
    • 调用 .merge_router(&router) 将路由信息和从 #[endpoint] 宏收集到的元数据合并到 OpenAPI 文档对象中。
  6. 提供 API 文档 UI:
    • 将生成的 OpenApi 文档对象转换为一个路由,通常提供 JSON 格式的文档 (doc.into_router("/api-doc/openapi.json"))。
    • 添加 Salvo 提供的多种 API 文档 UI 路由:SwaggerUi, Scalar, RapiDoc, ReDoc。它们都指向之前生成的 JSON 文档路径。
  7. 组合路由并启动: 将业务 API 路由和文档 UI 路由组合起来,然后启动服务器。

访问 /swagger-ui, /scalar, /rapidoc, 或 /redoc 可以查看和交互式地测试 TODO API。

文件列表

  • Cargo.toml: 启用 salvooapi 特性,并包含 serde (数据序列化) 和 tokio (异步运行时) 依赖。
  • src/main.rs:
    • 定义 Todo 数据结构和内存 STORE
    • 使用 #[endpoint] 定义 CRUD 操作的处理器 (list_todos, create_todo, update_todo, delete_todo)。
    • 创建 Router 注册业务 API 路径。
    • 创建 OpenApi 实例并合并路由信息。
    • 添加 OpenAPI JSON 端点和多个 UI (Swagger, Scalar, RapiDoc, ReDoc) 的路由。
    • 定义 index 处理器提供一个简单的 HTML 页面链接到各个文档 UI。
    • 启动服务器。

db-mongodb

此示例演示了如何在 Salvo 应用中集成 MongoDB 数据库,实现基本的 CRUD (创建、读取、更新、删除 - 此示例主要展示创建和读取) 操作。

主要步骤和概念:

  1. 添加依赖: 在 Cargo.toml 中添加 mongodb crate。
  2. 数据库连接:
    • main 函数中,使用 mongodb::Client::with_uri_str 连接到 MongoDB 实例。连接 URI 通常从环境变量读取或使用默认值。
    • 为了在处理器中方便地访问数据库客户端,示例使用了 std::sync::OnceLockClient 实例存储为全局静态变量 MONGODB_CLIENT。提供了一个辅助函数 get_mongodb_client() 来获取它。
  3. 定义数据模型: 创建 User 结构体,并使用 #[derive(Debug, Deserialize, Serialize)] 使其可以与 BSON (MongoDB 的文档格式) 进行相互转换。注意 _id 字段通常是 Option<ObjectId> 类型。
  4. 创建索引 (可选但推荐): 示例中定义了一个 create_username_index 函数,在启动时为 users 集合的 username 字段创建一个唯一索引,以确保用户名的唯一性并提高查询性能。
  5. 实现数据库操作处理器:
    • add_user:
      • 从请求体中解析 User 数据 (req.parse_json::<User>())。
      • 获取 MongoDB 集合 (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> 中。
      • 将用户列表以 JSON 格式返回 (res.render(Json(vec_users)))。
    • get_user:
      • 从 URL 路径参数中获取 username (req.param::<String>("username"))。
      • 调用 coll_users.find_one(doc! { "username": &username }, None) 按用户名查询单个文档。
      • 处理查询结果:如果找到用户,返回 JSON;如果未找到,返回提示信息;如果出错,返回错误信息。
  6. 路由设置: 创建 Router 并将相应的 HTTP 方法和路径 (/users, /users/{username}) 映射到对应的处理器。
  7. 错误处理: 示例定义了一个简单的 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 枚举。
    • 实现 ErrorWriter trait。
    • 初始化 MongoDB 连接并存储在 OnceLock 中。
    • 实现 add_user, get_users, get_user 处理器,包含数据库操作逻辑。
    • 实现 create_username_index 函数。
    • 设置路由。
    • 启动服务器。

websocket

此示例展示了如何在 Salvo 中处理 WebSocket 连接。WebSocket 提供了一种在客户端和服务器之间进行全双工通信的协议,常用于实时应用。

主要步骤:

  1. 启用 WebSocket: 在 Cargo.toml 中启用 salvowebsocket 特性。
  2. 升级请求: WebSocket 连接始于一个标准的 HTTP GET 请求,该请求包含特殊的头部(如 Upgrade: websocket, Connection: Upgrade)来请求协议升级。
  3. 处理器逻辑:
    • 创建一个处理器(如此示例中的 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 (通常表示客户端已断开连接)。
    • 示例中的闭包简单地将接收到的任何消息 echo 回客户端。
    • 示例还演示了如何在升级前从请求查询参数中解析数据 (req.parse_queries::<User>()),并将这些数据(如用户信息)传递到 WebSocket 处理闭包中使用。
  4. 路由: 将配置了 WebSocket 升级处理器的路径(如 /ws)添加到路由器。
  5. 客户端: 示例包含一个简单的 HTML 页面 (INDEX_HTML),其中使用 JavaScript 的 WebSocket API 连接到服务器的 /ws 端点。它还演示了如何在 URL 查询字符串中传递参数 (?id=123&name=chris)。

文件列表

  • Cargo.toml: 启用 salvowebsocket 特性。需要 serde 用于解析查询参数中的 User 结构。futures-util 可能被 salvo 内部或 WebSocket 处理需要。
  • src/main.rs:
    • 定义 User 结构体用于从查询参数解析数据。
    • 定义 connect 处理器,使用 WebSocketUpgrade 处理连接升级和后续的 WebSocket 消息收发(echo 逻辑)。
    • 定义 index 处理器提供包含客户端 JavaScript 的 HTML 页面。
    • 设置路由,将 /ws 指向 connect 处理器。
    • 启动服务器。

sse

此示例演示了如何使用 Salvo 实现 Server-Sent Events (SSE)。SSE 是一种允许服务器向客户端单向推送更新的标准 HTTP 协议,常用于实时通知或数据流。

核心概念:

  1. 启用 SSE: 在 Cargo.toml 中启用 salvosse 特性。
  2. 事件流: SSE 的核心是服务器向客户端发送一个事件流 (event stream)。这个流是一个遵循特定格式的文本响应 (Content-Type: text/event-stream),由一系列事件组成。
  3. 事件格式: 每个 SSE 事件通常由一个或多个字段组成,如 event: <event_name>, data: <event_data>, id: <event_id>, retry: <reconnection_time>。事件之间用空行分隔。Salvo 提供了 SseEvent 结构体来方便地构建这些事件。
  4. 创建事件流:
    • 你需要创建一个实现了 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 字段的事件)。
  5. 发送流:
    • 在处理器(如 handle_tick)中,获取到准备好的事件流 event_stream
    • 调用 salvo::sse::stream(res, event_stream)。这个函数会设置正确的响应头 (Content-Type: text/event-stream 等),并将事件流中的每个 SseEvent 格式化后写入响应体。
  6. 客户端: 客户端需要使用 JavaScript 的 EventSource API 来连接 SSE 端点并接收事件。本示例未包含客户端代码,但标准的 EventSource 用法即可。

此示例实现了一个简单的计数器服务,每秒向连接的客户端发送一个包含递增数字的 SSE 事件。

文件列表

  • Cargo.toml: 启用 salvosse 特性。需要 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
    • 启动服务器。

oapi-hello

这是一个最简单的 Salvo OpenAPI (oapi) 示例,演示了如何为一个基本的 "Hello World" 端点自动生成 OpenAPI 文档。

核心步骤:

  1. 启用 OAPI: 在 Cargo.toml 中启用 salvooapi 特性。
  2. 定义端点: 使用 #[endpoint] 宏标记 hello 函数。
    • 函数接受一个可选的查询参数 name (类型 QueryParam<String, false>)。QueryParamsalvo::oapi::extract 提供的提取器,它会自动从请求中提取参数,并将其信息添加到 OpenAPI 文档。false 表示该参数是可选的。
    • 函数返回一个 String#[endpoint] 宏会确保返回值被正确处理并添加到 OpenAPI 文档的响应部分。
  3. 生成文档:
    • 创建 Router 并注册 hello 端点到 /hello 路径。
    • 创建 OpenApi 实例,提供 API 名称和版本。
    • 调用 .merge_router(&router) 合并路由和端点元数据。
  4. 提供文档 UI:
    • OpenApi 对象转换为提供 OpenAPI JSON 的路由 (doc.into_router("/api-doc/openapi.json"))。
    • 添加 SwaggerUi 路由 (SwaggerUi::new("/api-doc/openapi.json").into_router("/swagger-ui")) 以提供交互式 UI。
  5. 启动服务器: 组合路由并启动。

访问 /swagger-ui 可以看到自动生成的文档,其中包含 /hello 端点,描述了其可选的 name 查询参数和返回的字符串响应。

文件列表

  • Cargo.toml: 启用 salvooapi 特性。
  • src/main.rs:
    • 使用 #[endpoint] 定义 hello 处理器,使用 QueryParam 提取参数。
    • 创建路由。
    • 创建 OpenApi 实例并合并路由。
    • 添加 OpenAPI JSON 和 Swagger UI 路由。
    • 启动服务器。

static-embed-file

此示例演示了如何使用 rust-embed crate 将静态文件(如 HTML, CSS, JS, 图像)直接嵌入到编译后的二进制文件中,并通过 Salvo 提供这些嵌入的文件服务。这对于创建单文件可执行分发很有用。

主要步骤:

  1. 添加依赖: 在 Cargo.toml 中添加 rust-embed crate 和 salvo (需要 serve-static 特性,虽然这里没直接用 StaticDir,但 EmbeddedFileExt 可能依赖相关功能)。
  2. 定义嵌入资源:
    • 创建一个结构体(如 Assets)。
    • 使用 #[derive(RustEmbed)] 宏标记该结构体。
    • 使用 #[folder = "static"] 属性指定包含要嵌入文件的目录(相对于 Cargo.toml 的路径)。rust-embed 会在编译时扫描此目录并将文件内容包含在内。
  3. 创建处理器:
    • 定义一个处理器(如 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
  4. 设置路由: 将捕获所有路径的路由 (Router::with_path("{**rest}")) 指向 serve_file 处理器。
  5. 启动服务器: 编译并运行。现在,请求服务器上对应于 static/ 目录下文件的路径(如 /test1.txt, /test2.txt)将返回嵌入的文件内容。

文件列表

  • Cargo.toml: 添加 rust-embedsalvo (带有 serve-static 特性) 依赖。
  • src/main.rs:
    • 定义 Assets 结构体,使用 #[derive(RustEmbed)]#[folder = "static"]
    • 导入 salvo::serve_static::EmbeddedFileExt
    • 定义 serve_file 处理器,使用 Assets::get() 获取嵌入文件,并使用 file.render() 发送响应。
    • 设置路由捕获所有路径并指向 serve_file
    • 启动服务器。
  • static/: 包含要嵌入到二进制文件中的静态文件。

tls-rustls-reload

此示例演示了如何使用 Salvo 配置基于 Rustls 的 TLS,并且能够动态地重新加载 TLS 证书和私钥,而无需重启服务器。这对于证书自动续期(例如使用 ACME)后更新配置非常有用。

核心机制:

  1. 启用 Rustls: 在 Cargo.toml 中启用 salvorustls 特性。
  2. 动态配置流: TcpListener::rustls() 方法接受一个能产生 RustlsConfig 的异步流 (Stream)。Salvo 会在需要新的 TLS 配置时(例如,当新的客户端连接尝试建立 TLS 握手时)从此流中获取最新的配置。
  3. 使用 async_stream: 示例使用了 async_stream::stream! 宏来方便地创建一个无限循环的异步流:
    • loop { ... }: 无限循环。
    • yield load_config();: 在每次循环中,调用 load_config() 函数加载当前的证书和密钥文件,创建一个新的 RustlsConfig 实例,并通过 yield 将其发送到流中。
    • tokio::time::sleep(Duration::from_secs(60)).await;: 等待 60 秒。这意味着服务器大约每分钟会尝试重新加载一次配置。你可以根据需要调整这个间隔,或者使用更复杂的逻辑(如文件系统监控或信号通知)来触发重新加载。
  4. 配置加载函数: load_config() 函数负责从指定路径(这里使用 include_bytes! 在编译时嵌入证书,实际应用中通常是从文件系统读取)加载证书和私钥,并创建一个 RustlsConfig 实例。
  5. 绑定和启动: TcpListener::new(...).rustls(stream).bind().await 完成监听器的设置和绑定。然后启动服务器。

当服务器运行时,如果 certs/cert.pemcerts/key.pem 文件被更新,服务器将在下一次执行 yield load_config() 时(最多等待 60 秒)加载新的配置,并开始使用新的证书处理后续的 TLS 连接,整个过程无需中断服务。

文件列表

  • Cargo.toml: 启用 salvorustls 特性。添加 async-stream 依赖用于创建配置流。
  • src/main.rs:
    • 定义 hello 处理器。
    • 定义 load_config() 函数,负责加载证书/密钥并创建 RustlsConfig
    • main 函数中:
      • 使用 async_stream::stream! 创建一个定期产生 RustlsConfig 的异步流。
      • 将此流传递给 TcpListener::new(...).rustls()
      • 绑定监听器并启动服务器。
  • certs/: 包含初始的证书 (cert.pem) 和私钥 (key.pem) 文件。

craft

此示例演示了 Salvo 的 craft 功能,它提供了一个 #[craft] 宏,可以方便地将结构体的方法转换为 Salvo 的处理器 (Handler) 或端点 (Endpoint),简化代码编写,特别是在需要将状态注入处理器时。

主要概念:

  1. 启用 Craft: 在 Cargo.toml 中启用 salvocraft 特性。通常与 oapi 一起使用,因为 craft 可以自动生成 OpenAPI 信息。
  2. #[craft]: 应用在 impl Opts { ... } 块上。
  3. #[craft(handler)]: 将标记的方法转换为一个 Salvo Handler
    • 方法签名可以像普通函数一样,但可以接收 &self&mut self 来访问结构体实例的状态。
    • 方法的参数通常是 Salvo 的提取器 (Extractor),如 QueryParam
    • 返回值需要是实现了 Writer 的类型,或者 Result
    • 示例中的 add1 方法接收 &self,访问 self.state,并接收两个 QueryParam
  4. #[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] 也可以用于非实例方法。
  5. 状态注入:
    • 对于 handlerendpoint,如果方法接收 &self&mut self,你需要将结构体的实例(或其引用)传递给生成的处理器。示例中是 opts.add1(),这里 add1()#[craft] 生成的函数,它内部会捕获 opts 的引用。
    • 对于 endpoint 接收 self: Arc<Self>,你需要传递一个 Arc 包裹的实例,如 opts.add2() (这里 optsArc<Opts>)。
  6. 路由: 像普通处理器一样将 craft 生成的函数添加到路由中。

craft 简化了将面向对象的方法集成到 Salvo 请求处理流程中的过程,使得代码更模块化和易于管理。

文件列表

  • Cargo.toml: 启用 salvocraftoapi 特性。
  • 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() 添加到不同路径。
    • 生成 OpenAPI 文档并添加 Swagger UI。
    • 启动服务器。

affix-state

此示例演示了 Salvo 的 affix-state 功能,允许你在请求处理链路的早期将共享状态(数据)注入到请求的 Depot 中,以便后续的中间件或处理器可以访问这些状态。

核心概念:

  1. 启用 Affix State: 在 Cargo.toml 中启用 salvoaffix-state 特性。
  2. Depot: Salvo 的 Depot 是一个与每个请求关联的类型化数据存储。它用于在处理请求的不同阶段(中间件、处理器)之间传递数据。
  3. affix_state::inject(): 这是一个中间件构造器。
    • affix_state::inject(state): 创建一个中间件,该中间件会将提供的 state 对象(需要是 Clone + Send + Sync + 'static)注入到 Depot 中。后续可以通过 depot.obtain::<StateType>() 来获取该状态的克隆。
    • .inject(another_state): 可以链式调用 .inject() 来注入多个不同类型的状态。示例中注入了 ConfigArc<State>
    • .insert(key, value): 除了注入可以通过类型获取的状态外,还可以使用 .insert() 注入键值对数据。示例中注入了 "custom_data" 键和 "I love this world!" 值。后续可以通过 depot.get::<ValueType>("key") 来获取。
  4. 注入时机: affix_state 中间件通常通过 hoop() 添加到 Router 或服务器级别,确保在请求到达最终处理器之前状态已经被注入。
  5. 在处理器中访问状态:
    • 处理器需要接收 &mut Depot 作为参数。
    • 使用 depot.obtain::<StateType>() 获取通过 inject() 注入的状态。返回的是 Option<&StateType>,通常可以 unwrap() 如果你确定状态一定会被注入。注意,如果是注入的 Arc<T>,获取到的也是 &Arc<T>
    • 使用 depot.get::<&ValueType>("key") 获取通过 insert() 注入的数据。注意类型通常是 &ValueType,因为 Depot 存储的是引用。
  6. 共享状态: 示例中的 State 包含一个 Mutex<Vec<String>>,并通过 Arc 包裹后注入。这允许多个并发请求安全地访问和修改同一个 State 实例(通过 Arc 共享所有权,通过 Mutex 控制并发访问)。

affix-state 提供了一种类型安全且灵活的方式来管理和传递请求处理所需的共享数据和配置。

文件列表

  • Cargo.toml: 启用 salvoaffix-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 处理器。
    • 启动服务器。

size-limiter

此示例演示了如何使用 Salvo 的 size-limiter 中间件来限制传入请求体的大小,防止因接收过大的请求(如超大文件上传)而耗尽服务器资源。

主要步骤:

  1. 启用 Size Limiter: 在 Cargo.toml 中启用 salvosize-limiter 特性。
  2. 应用限制:
    • 使用 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 请求。
  3. 行为: 当一个请求到达应用了 max_size 的路径时:
    • 如果请求没有请求体(如 GET 请求),或者请求体的 Content-Length 头部(如果存在)小于或等于限制,请求会正常传递给后续的处理器。
    • 如果 Content-Length 头部指示的大小超过了限制,Salvo 会立即拒绝请求,通常返回 413 Payload Too Large 状态码,而不会读取请求体内容,也不会调用后续处理器。
    • 如果请求没有 Content-Length 头部(例如使用了 Transfer-Encoding: chunked),Salvo 会在读取请求体数据时进行检查。一旦读取的数据量超过限制,它会停止读取并返回 413 Payload Too Large
  4. 对比: 示例还提供了一个 /unlimit 路径,没有应用大小限制,用于对比效果。

使用 size-limiter 是保护服务器免受拒绝服务攻击(通过发送大量数据)或意外超大请求的重要措施。

文件列表

  • Cargo.toml: 启用 salvosize-limiter 特性。
  • src/main.rs:
    • 定义 index 处理器提供 HTML 上传表单。
    • 定义 upload 处理器处理文件上传。
    • main 函数中:
      • 创建临时目录 temp
      • 创建主 Router
      • /limited 路径创建一个子路由,并使用 hoop(max_size(10 * 1024 * 1024)) 应用 10 MiB 的大小限制,然后注册 upload 处理器。
      • /unlimit 路径创建一个子路由,直接注册 upload 处理器(无限制)。
      • 注册 index 处理器。
    • 启动服务器。

tls-openssl

此示例演示了如何使用 Salvo 和 OpenSSL 库来为服务器启用 TLS (HTTPS) 加密。

主要步骤:

  1. 启用 OpenSSL: 在 Cargo.toml 中启用 salvoopenssl 特性。确保你的系统安装了 OpenSSL 开发库(如 libssl-dev on Debian/Ubuntu, openssl-devel on Fedora/CentOS)。
  2. 准备证书和密钥: 你需要一个有效的 TLS 证书 (cert.pem) 和对应的私钥 (key.pem)。示例中使用 include_bytes! 在编译时嵌入它们,实际部署中通常从文件系统加载。
  3. 创建 OpenSSL 配置:
    • 使用 salvo::conn::openssl::Keycert::new() 创建一个证书/密钥对对象。
    • 使用 .with_cert(cert_bytes).with_key(key_bytes) 加载证书和密钥的字节数据。
    • 使用 salvo::conn::openssl::OpensslConfig::new(keycert) 创建 OpenSSL TLS 配置对象。
  4. 配置监听器:
    • 创建 TcpListener 实例,指定要监听的地址和端口。
    • 调用 .openssl(config) 方法,将之前创建的 OpensslConfig 应用到监听器。
    • 调用 .bind().await 绑定监听器。现在这个监听器将接受 TLS 连接。
  5. 启动服务器: 使用配置了 OpenSSL 的 acceptor 创建并启动 Server

服务器现在将在指定端口上监听 HTTPS 连接,使用提供的证书进行加密通信。

文件列表

  • Cargo.toml: 启用 salvoopenssl 特性。
  • src/main.rs:
    • 定义 hello 处理器。
    • main 函数中:
      • 使用 include_bytes! 加载证书和密钥。
      • 创建 KeycertOpensslConfig
      • 创建 TcpListener,调用 .openssl(config) 应用 TLS 配置。
      • 绑定监听器并启动服务器。
  • certs/: 包含示例用的证书 (cert.pem) 和私钥 (key.pem)。

webtransport-acme-http01

此示例结合了 WebTransport (基于 Quinn/HTTP3) 和 ACME (通过 Let's Encrypt 获取证书) 的 HTTP-01 挑战类型。它演示了如何设置一个可以通过公共域名访问的、使用有效 TLS 证书的 WebTransport 服务器,并且证书是自动获取和管理的。

关键配置和流程:

  1. 启用特性: 在 Cargo.toml 中需要启用 salvoacme, quinn (提供 WebTransport/HTTP3), serve-static (用于 ACME HTTP-01 挑战通常需要服务于特定路径,以及提供客户端页面) 特性。
  2. ACME HTTP-01 挑战:
    • 这种挑战类型要求你的服务器能在特定路径 (/.well-known/acme-challenge/) 上响应 Let's Encrypt 服务器的 HTTP 请求,以证明你对域名的控制权。
    • 因此,你的服务器必须能接收来自公网的 HTTP (端口 80) 请求。
  3. 监听器配置链:
    • TcpListener::new("0.0.0.0:443"): 监听 HTTPS 端口 443。
    • .acme(): 启用 ACME 功能。
    • .cache_path("temp/letsencrypt"): 指定存储获取到的证书和 ACME 账户信息的路径。
    • .add_d