要相信信念的力量
大家好,我是柒八九。
前言
在之前的用 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
处理程序,它会回显收到的任何消息。
rust
// #[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
回显服务器如下所示:
rust
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
应用程序的简化示例:
rust
// 定义了一个名为 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 特点
- 一体化的方法。
- 出色的开发体验。
- 开发活跃度不如以前。
- 初学者的绝佳选择。
4. Warp
Warp 是一个构建在 Tokio
之上的 Web 框架,而且是一个非常好的框架。它与我们之前看到的其他框架非常不同。
Warp
与 Axum
有一些共同的特点:它构建在 Tokio
和 Hyper
之上,并利用了 Tower
中间件。然而,它在方法上有很大的不同。Warp
是建立在 Filter
trait 之上的。
在 Warp
中,我们构建一系列应用于传入请求的过滤器
,并将请求传递到管道直到达到末端。过滤器可以链接,它们可以组合。这使我们能够构建非常复杂的管道,但仍然易于理解。
Warp
也比 Axum
更接近 Tokio
生态系统,这意味着我们可能会在没有任何粘合特性的情况下处理更多 Tokio
结构和概念。
Warp
采用非常功能化的方法,如果这是我们的编程风格,我们将喜欢 Warp
的表达能力和可组合性。当我们查看 Warp
代码片段时,它通常读起来像正在发生的事情的故事,这在 Rust
中能够实现是有趣且令人惊讶的。
然而,随着这些不同的函数和过滤器被链接在一起,Warp
中的类型变得非常长且非常复杂,而且难以理解。错误消息也是如此,可能是难以理解的一大堆文本。
Warp
是一个很棒的框架。但是,它并不是最适合初学者的框架,也不是最流行的框架 。这意味着我们可能在寻找帮助和资源方面会更加困难。但它非常适用于快速小型应用程序!
Warp 示例
来自其示例仓库的 WebSocket 聊天的 Warp 应用程序的简化示例:
rust
// 定义了一个静态的原子 usize 计数器,用于为每个连接的用户分配唯一的用户ID。
static NEXT_USER_ID: AtomicUsize = AtomicUsize::new(1);
// 当前连接用户的状态。
// 定义了一个类型别名 Users,它是一个原子引用计数的可读写锁的 HashMap,将用户ID映射到消息的发送器。
// Arc 是原子引用计数的智能指针,RwLock 是读写锁。
// - 键是其id
// - 值是`warp::ws::Message`的发送器
type Users = Arc<RwLock<HashMap<usize, mpsc::UnboundedSender<Message>>>>;
#[tokio::main]
async fn main() {
// 创建了一个 users 变量,用于存储连接的用户信息
let users = Users::default();
// 将其包装成 Warp 过滤器,以便在不同的路由中共享用户状态。
let users = warp::any().map(move || users.clone());
// chat 路由处理 WebSocket 握手
let chat = warp::path("chat")
// `ws()`过滤器将准备WebSocket握手...
.and(warp::ws())
.and(users)
// 调用 user_connected 函数处理 WebSocket 连接。
.map(|ws: warp::ws::Ws, users| {
// 如果握手成功,将调用我们的函数。
ws.on_upgrade(move |socket| user_connected(socket, users))
});
// 处理 HTTP GET 请求,返回一个包含聊天室链接的 HTML 页面
let index = warp::path::end().map(|| warp::reply::html(INDEX_HTML));
let routes = index.or(chat);
warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
}
async fn user_connected(ws: WebSocket, users: Users) {
// 使用计数器为此用户分配新的唯一ID。
let my_id = NEXT_USER_ID.fetch_add(1, Ordering::Relaxed);
eprintln!("new chat user: {}", my_id);
// 将套接字拆分为消息的发送器和接收器。
let (mut user_ws_tx, mut user_ws_rx) = ws.split();
// 创建一个新的消息通道 (mpsc::unbounded_channel) 用于将用户的消息广播给其他用户
let (tx, rx) = mpsc::unbounded_channel();
let mut rx = UnboundedReceiverStream::new(rx);
tokio::task::spawn(async move {
// 不断接收用户的消息。一旦用户断开连接,就会退出这个循环。
while let Some(message) = rx.next().await {
user_ws_tx
.send(message)
.unwrap_or_else(|e| {
eprintln!("websocket send error: {}", e);
})
.await;
}
});
//将发送器保存在我们的已连接用户列表中。
users.write().await.insert(my_id, tx);
// 返回一个基本上是管理此特定用户连接的状态机的"Future"。
// 每当用户发送消息时,将其广播给
// 所有其他用户...
while let Some(result) = user_ws_rx.next().await {
let msg = match result {
Ok(msg) => msg,
Err(e) => {
eprintln!("websocket error(uid={}): {}", my_id, e);
break;
}
};
user_message(my_id, msg, &users).await;
}
// 只要用户保持连接,user_ws_rx流就会继续处理。一旦他们断开连接,那么...
user_disconnected(my_id, &users).await;
}
// 处理用户发送的消息。它跳过非文本消息,将文本消息格式化为 <User#ID>: Message,然后将其广播给所有其他用户。
async fn user_message(my_id: usize, msg: Message, users: &Users) {
// 跳过任何非文本消息...
let msg = if let Ok(s) = msg.to_str() {
s
} else {
return;
};
let new_msg = format!("<User#{}>: {}", my_id, msg);
// 来自此用户的新消息,将其发送给所有其他用户(除了相同的uid)...
for (&uid, tx) in users.read().await.iter() {
if my_id != uid {
if let Err(_disconnected) = tx.send(Message::text(new_msg.clone())) {
// 发送器已断开连接,我们的`user_disconnected`代码
// 应该在另一个任务中执行,这里没有更多的事情要做。
}
}
}
}
async fn user_disconnected(my_id: usize, users: &Users) {
eprintln!("good bye user: {}", my_id);
// 流关闭,因此从用户列表中删除
users.write().await.remove(&my_id);
}
Warp 特点
- 函数式方法。
- 良好的表达能力。
- 通过接近
Tokio
、Tower
和Hyper
构建强大的生态系统。 - 不适合初学者的框架
5. Tide
Tide 是一个建立在 async-std
运行时之上的极简主义 Web 框架 。极简主义的方法意味着我们得到了一个非常小的 API 表面。Tide
中的处理函数是 async fn
,接受一个 Request
并返回一个 Response
的 tide::Result
。提取数据或发送正确的响应格式由我们自行完成。
虽然这可能对我们来说是更多的工作,但也更直接,意味着我们完全掌控正在发生的事情。在某些情况下,能够离 HTTP 请求和响应如此近是一种愉悦,使事情变得更容易。
Tide
的中间件方法与我们从 Tower
中了解的类似,但 Tide
公开了 async trait
crate,使实现变得更加容易。
Tide 示例
来自其示例仓库的用户会话示例:
rust
// async-std crate 提供的异步 main 函数。它返回一个 Result,表示可能的错误。
#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
// 使用 femme crate 启用颜色日志。这是一个美观的日志记录库,可以使日志输出更易读。
femme::start();
// 创建一个 Tide 应用程序实例
let mut app = tide::new();
// 添加一个日志中间件,用于记录请求和响应的日志信息。
app.with(tide::log::LogMiddleware::new());
// 添加一个会话中间件,用于处理会话数据。这里使用内存存储,并提供一个密钥(TIDE_SECRET),用于加密和验证会话数据。
app.with(tide::sessions::SessionMiddleware::new(
tide::sessions::MemoryStore::new(),
std::env::var("TIDE_SECRET")
.expect(
"Please provide a TIDE_SECRET value of at \
least 32 bytes in order to run this example",
)
.as_bytes(),
));
// 添加一个 Before 中间件,它在处理请求之前执行。在这里,它用于增加访问计数,存储在会话中。
app.with(tide::utils::Before(
|mut request: tide::Request<()>| async move {
let session = request.session_mut();
let visits: usize = session.get("visits").unwrap_or_default();
session.insert("visits", visits + 1).unwrap();
request
},
));
// 定义了一个处理根路径的GET请求的路由。这个路由通过 async move 来处理请求,获取会话中的访问计数,并返回一个包含访问次数的字符串。
app.at("/").get(|req: tide::Request<()>| async move {
let visits: usize = req.session().get("visits").unwrap();
Ok(format!("you have visited this website {} times", visits))
});
// 定义了一个处理 "/reset" 路径的GET请求的路由。这个路由通过 async move 处理请求,将会话数据清除,然后重定向到根路径
app.at("/reset")
.get(|mut req: tide::Request<()>| async move {
req.session_mut().destroy();
Ok(tide::Redirect::new("/"))
});
// 启动应用程序并监听在 "127.0.0.1:8080" 地址上。使用 await? 处理可能的启动错误。
app.listen("127.0.0.1:8080").await?;
Ok(())
}
Tide 简要概述
- 极简主义方法。
- 使用
async-std
运行时。 - 简单的处理函数。
- 异步特性的试验场。
6. Poem
Poem 声称自己是一个功能齐全但易于使用的 Web 框架。乍一看,它的使用方式与 Axum
非常相似,唯一的区别是它需要使用相应的宏标记处理程序函数。它还建立在 Tokio
和 Hyper
之上,完全兼容 Tower
中间件,同时仍然暴露自己的中间件特性。
Poem
的中间件特性也非常简单易用。我们可以直接为所有或特定的 Endpoint
(Poem 表达一切都可以处理 HTTP 请求的方式)实现该特性,或者只需编写一个接受 Endpoint
作为参数的异步函数。
Poem
不仅与更广泛的生态系统中的许多功能兼容,而且还具有丰富的功能,包括对 OpenAPI
和 Swagger
文档的全面支持。它不仅限于基于 HTTP
的 Web 服务,还可以用于基于 Tonic
的 gRPC
服务,甚至在 Lambda
函数中使用,而无需切换框架。添加对 OpenTelemetry
、Redis
、Prometheus
等的支持,我们就可以勾选所有现代企业级应用程序 Web 框架的所有框。
Poem
仍然处于 0.x
版本,但如果保持势头并交付出色的 1.0 版本,这将是一个值得关注的框架!
Poem 示例
来自其示例仓库的 WebSocket 聊天的缩写版本:
rust
// 注解表示这是一个处理器函数,用于处理 WebSocket 请求
#[handler]
fn ws(
// 提取了 WebSocket 路径中的名字参数
Path(name): Path<String>,
// WebSocket 对象,表示与客户端的连接
ws: WebSocket,
// 是一个数据提取器,用于获取广播通道的发送器。
sender: Data<&tokio::sync::broadcast::Sender<String>>,
) -> impl IntoResponse {
// 克隆了广播通道的发送器 sender。
let sender = sender.clone();
// 它订阅了广播通道,创建了一个接收器 receiver
let mut receiver = sender.subscribe();
// 处理 WebSocket 连接升级
ws.on_upgrade(move |socket| async move {
// 将连接的读写部分拆分为 sink 和 stream
let (mut sink, mut stream) = socket.split();
// 从 WebSocket 客户端接收消息
// 如果是文本消息,则将其格式化为 {name}: {text} 的形式,并通过广播通道发送。
// 如果发送失败(例如,通道关闭),则任务终止。
tokio::spawn(async move {
while let Some(Ok(msg)) = stream.next().await {
if let Message::Text(text) = msg {
if sender.send(format!("{name}: {text}")).is_err() {
break;
}
}
}
});
// 从广播通道接收消息,并将其发送到 WebSocket 客户端
tokio::spawn(async move {
while let Ok(msg) = receiver.recv().await {
if sink.send(Message::Text(msg)).await.is_err() {
break;
}
}
});
})
}
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
// 使用 tide::Route 创建了一个路由,其中包括两个路径:
// - / 路径处理 HTTP GET 请求,调用 index 函数。
// - /ws/:name 路径处理 WebSocket 请求,调用 ws 函数。
let app = Route::new().at("/", get(index)).at(
"/ws/:name",
// 通过 tokio::sync::broadcast::channel 创建一个广播通道;
// 并通过 tokio::sync::broadcast::channel::<String>(32).0
// 获取其发送器,将其作为数据传递给 ws 处理函数
get(ws.data(tokio::sync::broadcast::channel::<String>(32).0)),
);
// 创建了一个服务器实例
Server::new(TcpListener::bind("127.0.0.1:3000"))
// 启动服务器,并等待其完成运行。
.run(app)
.await
}
Poem 简要概述
- 丰富的功能集。
- 与 Tokio 生态系统兼容。
- 易于使用。
- 适用于 gRPC 和 Lambda。
后记
正如我们所见,Rust Web
框架的世界非常多样化。没有一种解决方案适用于所有情况,我们需要选择最符合我们需求的框架。如果我们刚刚开始,我建议我们选择 Actix
或 Axum
,因为它们是最适合初学者的框架,而且它们有着出色的文档。
分享是一种态度。
全文完,既然看到这里了,如果觉得不错,随手点个赞和"在看"吧。