要相信信念的力量
大家好,我是柒八九。
前言
在之前的用 Rust 搭建 React Server Components 的 Web 服务器我们利用了Axum构建了RSC的服务器。也算是用Rust在构建Web服务上的小试牛刀。
虽然说Axum在Rust Web应用中一枝独秀。但是,市面上也有很多不同的解决方案。所以,今天我们就比较一些 Rust 框架,突出它们各自的优势和缺点,以帮助我们为项目做出明智的决策。没有对比就没有选择,我们只有在真正的了解各个框架的优缺点和适应场景,在以后的开发中才能有的放矢的放心选择。
文本中,我们会介绍很多Rust框架。并且会按照如下的受欢迎程度的顺序来讲。
好了,天不早了,干点正事哇。
我们能所学到的知识点
- Axum
- Actix Web
- Rocket
- Warp
- Tide
- Poem
1. Axum
Axum 是 Rust 生态系统中具有特殊地位的 Web 应用程序框架(从下载量就可见端倪)。它是 Tokio 项目的一部分,Tokio 是使用 Rust 编写异步网络应用程序的运行时。Axum 不仅使用 Tokio 作为其异步运行时,还与 Tokio 生态系统的其他库集成,利用 Hyper 作为其 HTTP 服务器和 Tower 作为中间件。通过这样做,我们能够重用 Tokio 生态系统中现有的库和工具。
Axum不依赖于宏,而是利用 Rust 的类型系统提供安全且人性化的 API。这是通过使用特性来定义框架的核心抽象实现的,例如 Handler 特性,用于定义应用程序的核心逻辑。这种方法允许我们轻松地从较小的组件中组合应用程序,这些组件可以在多个应用程序中重用。
在 Axum 中,处理程序(handler)是一个接受请求并返回响应的函数。这与其他后端框架类似,但使用 Axum 的 FromRequest 特性,我们可以指定从请求中提取的数据类型。返回类型需要实现 IntoResponse 特性(trait),已经有许多类型实现了这个特性,包括允许轻松更改响应的状态代码的元组类型。
Rust 的类型系统、泛型,尤其是在traits中使用异步方法(或更具体地说是返回的 Future),当不满足trait限制时,Rust 的错误消息会很复杂。特别是当尝试匹配抽象trait限制时,经常会得到一堆难以解读的文本。为此Axum 提供了一个带有辅助宏的库,将错误放到实际发生错误的地方,使得更容易理解发生了什么错误。
虽然Axum 做了很多正确的事情,可以很容易地启动执行许多任务的应用程序。但是,有一些事情需要特别注意。Axum版本仍然低于 1.0,也就意味着Axum 团队保留在版本之间根本性地更改 API 的自由,这可能导致我们的应用程序出现严重问题。
Axum 示例
下面展示了一个 WebSocket 处理程序,它会回显收到的任何消息。
// #[tokio::main] 宏标记了 `main` 函数,表明这是一个异步的`Tokio`应用程序。 #[tokio::main] async fn main() { // 首先创建了一个 `TcpListener` 监听器,绑定到地址 "127.0.0.1:3000" 上 // 然后,通过 `await` 等待监听器绑定完成 // 如果绑定失败,会通过 `unwrap` 方法抛出错误。 let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") .await .unwrap(); println!("listening on {}", listener.local_addr().unwrap()); // 使用 `axum::serve` 启动 Axum 框架的服务器, // 监听前面创建的 `TcpListener`。 // `app()` 函数返回的是一个 `Router` // 它定义了一个简单的路由,将路径 "/a" 映射到处理函数 `a_handler`。 axum::serve(listener, app()).await.unwrap(); } // 返回一个 `Router`,它只有一个路由规则, // 将 "/a" 路径映射到 `a_handler` 处理函数 fn app() -> Router { Router::new() .route("/a", get(a_handler)) } // 一个WebSocket处理程序,它会回显收到的任何消息。 // 定义为一个WebSocket处理程序, // 它接收一个 `WebSocketUpgrade` 参数,表示WebSocket升级。 async fn a_handler(ws: WebSocketUpgrade) -> Response { // 调用将WebSocket升级后的对象传递给 `a_handle_socket` 处理函数。 ws.on_upgrade(a_handle_socket) } async fn a_handle_socket(mut socket: WebSocket) { // 使用 while let 循环,持续从 WebSocket 连接中接收消息。 // socket.recv().await 通过异步的方式接收消息,返回一个 Result, // 其中 Ok(msg) 表示成功接收到消息。 while let Some(Ok(msg)) = socket.recv().await { // 使用 if let 匹配,判断接收到的消息是否为文本消息。 // WebSocket消息可以是不同类型的,这里我们只处理文本消息。 if let Message::Text(msg) = msg { // 构造一个回显消息,将客户端发送的消息包含在回显消息中。 // 然后,使用 socket.send 方法将回显消息发送回客户端。 // await 等待发送操作完成。 if socket .send(Message::Text(format!("You said: {msg}"))) .await // 检查 send 操作是否返回错误。 // 如果发送消息出现错误(例如,连接断开), // 就通过 break 跳出循环,结束处理函数。 .is_err() { break; } } } }
Axum 特点
- 无宏 API。
- 利用
Tokio、Tower和Hyper构建强大的生态系统。 - 出色的开发体验。
- 仍处于
0.x版本,因此可能发生重大变更。
2. Actix Web
Actix Web 是 Rust 中存在已久且非常受欢迎的 Web 框架之一。像任何良好的开源项目一样,它经历了许多迭代,但已经达到了主要版本(不再是 0.x),换句话说:在主要版本内,它可以确保没有破坏性的更改。
乍一看,Actix Web 与 Rust 中的其他 Web 框架非常相似。我们使用宏来定义 HTTP 方法和路由(类似于 Rocket),并使用提取器(extractors)从请求中获取数据(类似于 Axum)。与 Axum 相比,它们之间的相似之处显著,甚至在它们命名概念和特性的方式上也很相似。最大的区别是 Actix Web 没有将自己与Tokio 生态系统强关联在一起。虽然 Tokio 仍然是 Actix Web 底层的运行时,但是该框架具有自己的抽象和特性,以及自己的一套 crates 生态系统。这既有利有弊。一方面,我们可以确保事物能够很好地配合使用,另一方面,我们可能会错失 Tokio 生态系统中已经可用的许多功能。
Actix Web 实现了自己的 Service 特性,它基本上与 Tower 的 Service 相同,但仍然不兼容。这意味着在 Tower 生态系统中大多数可用的中间件在 Actix 中不可用。
如果在 Actix Web 中需要实现一些特殊任务,而需要自己实现,我们可能会碰到运行框架中的 Actor 模型。这可能会增加一些意想不到的问题。
但 Actix Web 社区很给力。该框架支持 HTTP/2 和 WebSocket 升级,提供了用于 Web 框架中最常见任务的 crates 和指南,以及出色的文档,而且速度很快。Actix Web 之所以受欢迎,是有原因的,如果我们需要保证版本,请注意它可能是我们目前的最佳选择。
Actix Web 示例
在 Actix Web 中,一个简单的 WebSocket 回显服务器如下所示:
use actix::{Actor, StreamHandler}; use actix_web::{ web, App, Error, HttpRequest, HttpResponse, HttpServer }; use actix_web_actors::ws; /// 定义HTTP Actor // 定义了一个名为 MyWs 的结构体,这将用作WebSocket的Actix Actor。 // Actors 是Actix框架中的并发单元,用于处理异步消息 struct MyWs; // 为 MyWs 结构体实现了 Actor trait,指定了 WebsocketContext 作为上下文类型。 impl Actor for MyWs { type Context = ws::WebsocketContext<Self>; } /// 处理ws::Message消息的处理程序 // 为 MyWs 结构体实现了 StreamHandler trait,处理WebSocket连接中的消息。 impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWs { // 对接收到的不同类型的消息进行处理。例如,对于 Ping 消息,发送 Pong 消息作为响应。 fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) { match msg { Ok(ws::Message::Ping(msg)) => ctx.pong(&msg), Ok(ws::Message::Text(text)) => ctx.text(text), Ok(ws::Message::Binary(bin)) => ctx.binary(bin), _ => (), } } } // 定义了一个处理HTTP请求的异步函数。 async fn index(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> { // 将WebSocket连接升级,并将请求委托给 MyWs Actor 处理。 let resp = ws::start(MyWs {}, &req, stream); println!("{:?}", resp); resp } #[actix_web::main] async fn main() -> std::io::Result<()> { // 创建了一个 HttpServer 实例,通过 App::new() 创建一个应用, // 该应用只有一个路由,将路径 "/ws/" 映射到处理函数 index 上。 HttpServer::new(|| App::new().route("/ws/", web::get().to(index))) // 绑定服务器到地址 "127.0.0.1" 和端口 8080。 .bind(("127.0.0.1", 8080))? // 启动服务器并等待其完成运行。 .run() .await }
Actix Web 特点
- 拥有强大的生态系统。
- 基于
Actor模型。 - 通过主要版本保证的稳定 API。
- 出色的文档。
3. Rocket
Rocket 在 Rust Web 框架生态系统中已经有一段时间了:它的主要特点是基于宏的路由、内置表单处理、对数据库和状态管理的支持,以及其自己版本的模板!Rocket 确实尽力做到构建 一个 Web 应用程序所需的一切。
然而,Rocket 的雄心壮志也带来了一些代价。尽管仍在积极开发中,但发布的频率不如以前。这意味着框架的用户会错过许多重要的东西。
此外,由于其一体化的方法,我们还需要了解 Rocket 的实现方式。Rocket 应用程序有一个生命周期,构建块以特定的方式连接,如果出现问题,我们需要理解问题出在哪里。
Rocket 是一个很棒的框架,如果我们想开始使用 Rust 进行 Web 开发,它是一个很好的选择。对于我们许多人来说,Rocket 是进入 Rust 的第一步,使用它仍然很有趣。
Rocket 示例
处理表单的 Rocket 应用程序的简化示例:
// 定义了一个名为 Password 的结构体,该结构体派生了 Debug 和 FromForm traits。 // FromForm trait 用于从表单数据中提取数据。 // 该结构体包含两个字段 first 和 second,分别表示密码的第一个和第二个部分。 #[derive(Debug, FromForm)] struct Password<'v> { // 表示对字段的长度进行了验证,要求长度在6个字符以上 #[field(validate = len(6..))] // 表示第一个字段必须等于第二个字段 #[field(validate = eq(self.second))] first: &'v str, // 表示第二个字段必须等于第一个字段。 #[field(validate = eq(self.first))] second: &'v str, } // 省略其他结构体和实现... // 定义了一个处理GET请求的函数 index,返回一个 Template 对象。 // 这个函数用于渲染首页。 #[get("/")] fn index() -> Template { Template::render("index", &Context::default()) } // 定义了一个处理POST请求的函数 submit。 // 这个函数接受一个 Form 对象,其中包含了表单的数据 #[post("/", data = "<form>")] fn submit(form: Form<Submit<'_>>) -> (Status, Template) { // 通过检查 form.value 是否包含 Some(ref submission) 来判断表单是否提交。 let template = match form.value { // 如果提交了表单,打印提交的内容,并渲染 "success" 页面; Some(ref submission) => { println!("submission: {:#?}", submission); Template::render("success", &form.context) } // 否则,渲染 "index" 页面。 None => Template::render("index", &form.context), }; (form.context.status(), template) } // 定义了启动Rocket应用程序的函数。 #[launch] fn rocket() -> _ { // 使用 rocket::build() 创建一个Rocket应用程序实例 rocket::build() // 并通过 .mount() 方法挂载路由。 // routes![index, submit] 定义了两个路由, // 分别映射到 index 和 submit 函数。 .mount("/", routes![index, submit]) // 添加了一个模板处理的Fairing(Rocket中的中间件) .attach(Template::fairing()) // 将静态文件服务挂载到根路径。 .mount("/", FileServer::from(relative!("/static"))) }
Rocket 特点
- 一体化的方法。
- 出色的开发体验。
- 开发活跃度不如以前。
- 初学者的绝佳选择。
