Rust+Axum高性能web系列-从入门到速通教程
哈喽,大家好呀,我是呼噜噜,平时闲暇地时候我们程序员会写一些小项目,比如网页、桌面、app、小程序等等,写完后会想和大家展示,这就有对外服务的需求
如何对外服务呢?我们一般会把的应用的后端核心部分,部署到服务器上
这就会有个问题?国内的服务器资源属实不便宜,我个人习惯用java+springboot
来快速写业务,Java就比较占内存,而其starter框架SpringBoot更会吃大量的内存,哪怕是很简单的demo
当然对于企业来说内存不算什么问题,但对我个人来说,问题就大了,我的服务器都是每年618、双十一,各大云服务商搞活动
时,捡的"破烂"(每年需要打包搬家~~)
配置基本就是1核1G
的这种,一台服务器上,都跑不了几个小demo~
所以我就一直在尝试用其他语言来开发,比如Go、Rust,最后选择了Rust
,Rust这个语言近几年也比较火,Rust
是一种速度极快、高性能、注重内存安全
的静态编译型语言;而Axum
又是 Rust
语言中的一个专注于性能和简单性的Web框架,试试感觉还不错,符合我的需求,在官方文档里踩坑的同时也就顺手写了一系列的教程,另外如今网上相关资料也不多,这里就免费分享给大家了
那让我们从本文开始Rust+Axum来进Web开发之旅~(最好会一点rust基础,不难,多入几次门就好~)
什么是Axum?
Axum
它利用了 hyper 库的功能来增强Web应用程序的速度和并发性。Axum
还通过与 Tokio
库集成,将 Rust
的 async/await
功能推到了前台,使得开发者可以开发高性能的异步 API
和 Web
应用程序。
Axum是一个专注于人体工程学和模块化的 Web 应用程序框架,rust它的一些特点:
- 使用无宏的API实现路由(router)功能
- 使用提取器(extractor)对请求进行声明式的解析
- 简单和可预测的错误处理模式。
- 用最少的模板生成响应。
- 充分利用
tower
和tower-http
的中间件、服务和工具的生态系统
Axum的基本功能是基于 Tokio runtime
的,这给了Rust管理非阻塞、事件驱动活动的能力。这种能力对于平稳处理多个并发进程至关重要。
Axum
与现有框架不同的地方。Axum 没有自己的中间件系统,而是使用tower::Service
。这意味着 axum 可以无成本地获得超时、跟踪、压缩、授权等功能。它还可以让你与使用 hyper
或 tonic
编写的应用程序共享中间件。
此外 Axum 是基于Rust强大的类型系统和所有权规则构建的,这些规则在编译时防止了常见的Web开发陷阱,如数据竞争和内存泄漏。此外,Axum的模块化设计理念允许开发人员通过仅添加必要的组件来创建轻量级、专注的应用程序。
创建一个项目
我们首先开始创建一个rust项目,这里我们默认你已经安装好rust开发环境了,具体安装步骤直接上官网查看一下即可
cargo new rust_axum_web_guide
编写第一个hello world
添加依赖:
[package]
name = "rust_axum_web_guide"
version = "0.1.0"
edition = "2021"
[dependencies]
axum="0.7.9"
tokio = { version = "1.0", features = ["full"] }
axum
的版本选最新版的,当前最新的版本0.7.9
;再引入Tokio
,旨在充分利用 Tokio
的生态系统
我们使用官网上的hello world
例子,来改写 main.rs
:
use axum::{
routing::get,
Router,
};
#[tokio::main]
async fn main() {
// build our application with a single route
let app = Router::new().route("/", get(|| async { "Hello, World!" }));
// run our app with hyper, listening globally on port 3000
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
其中,tokio::main
属性宏开头 表示 Axum
其实就是一个 Tokio
应用。tokio
是 Rust 非常优秀的异步运行时框架,它提供了写异步网络服务所需的几乎所有功能。
Router::new().route
创建路由,tokio::net::TcpListener::bind
监听服务器地址,并将生成的 listener
传入axum::serve()
中使用,启动 axum
服务
启动项目
cargo run
编译并运行当前项目,执行这个命令,它还会自动下载依赖
如果一切顺利,我们应该会在终端中看到输出“Running on http://localhost:3000”。在浏览器中访问http://localhost:3000 ,或使用类似 curl
的工具,将显示消息"Hello, Rust!"
路由与处理器
Router
用于设置哪些路径指向哪些服务,是一个用于组合处理程序和服务的结构体。路由机制负责将传入的HTTP请求定向到其指定的 handler
。这些 handler
实际就是应用程序逻辑存在的地方
我们先来看个例子:
use axum::{
routing::get,
Router,
};
#[tokio::main]
async fn main() {
// build our application with a single route
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }))
.route("/foo", get(get_foo).post(post_foo))
.route("/foo/bar", get(foo_bar));
// run our app with hyper, listening globally on port 3000
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn get_foo() -> String{
return String::from("get请求foo方法")
}
async fn post_foo() -> String{
return String::from("post请求foo方法")
}
async fn foo_bar() -> String{
return String::from("get请求->foo_bar")
}
将路由和handler
绑定,handler
类似于Java Spring 中的 controller
,处理器 handler
是一个异步异步函数或者异步代码块,它接受零个或多个 extractors
作为参数,并返回一些可以转换为一个 IntoResponse
的内容。
也可以将路由和多个handler绑定,比如.route("/foo", get(get_foo).post(post_foo))
,同时绑定了GET及POST方法
;当然还可以指定了HTTP方法PUT、DELETE
等
处理器更详细的信息,可见axum::handler - Rust
用postman访问"/foo",get请求或者post请求,可以看到不同的结果
如果有多个相同的路由,则会恐慌
路由更详情信息,可见Router in axum - Rust
use axum::{routing::get, Router};
let app = Router::new()
.route("/", get(|| async {}))
.route("/", get(|| async {}));
提取器
提取器 Extractors
: 分离传入请求以获得处理程序所需的部分(比如解析异步函数的参数)
捕获动态URL值以及查询参数
我们先来看看如何捕获动态URL值以及查询参数,主要使用 path
和 query
提取器
use axum::{
routing::get,
Router,
extract::{ Path, Query}
};
use std::collections::HashMap;
use serde::Deserialize;
#[derive(Deserialize)]
struct Page {
number: u32,
}
#[tokio::main]
async fn main() {
// build our application with a single route
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }))
.route("/item/:id", get(print_item))
.route("/item2/:name/:age", get(print_item_handler2))
.route("/item3/:fullpath", get(print_item_handler3))
.route("/query/:id", get(print_query));
// run our app with hyper, listening globally on port 3000
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn print_item(Path(_id): Path<u32>) -> String {
format!("print_item: {}", _id)
}
async fn print_item_handler2(Path((name, age)): Path<(String, i64)>) -> String {
format!("name: {name}, age: {age}")
}
async fn print_item_handler3(Path(fullpath): Path<HashMap<String, String>>) -> String {
format!("path: /{fullpath:?}")
}
async fn print_query(Path(id): Path<u32>, Query(page): Query<Page>) -> String{
format!("Query {} on page {}", id, page.number)
}
还需要添加依赖,来支持序列化
serde={ version = "1.0.216", features = ["derive"] }
测试结果:
/item/321 #结果 print_item: 321
/item2/zj/18 #结果 name: zj, age: 18
/item3/{name: zzjj} #结果 path: /{"fullpath": "{name: zzjj}"}
/query/10?number=31 #结果 Query 10 on page 31
我们还能将多个参数写入到hashmap
中
use axum::{
routing::get,
Router,
extract::Query
};
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Deserialize, Debug)]
struct Info {
name: String,
age: u8,
}
#[tokio::main]
async fn main() {
// build our application with a single route
let app = Router::new().route("/", get(|| async { "Hello, World!" }))
.route("/query_1", get(print_map_handler))
.route("/query_2", get(print_map2_handler));
// run our app with hyper, listening globally on port 3000
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn print_map_handler(query: Query<Info>) -> String {
let info: Info = query.0;
format!("query1: {info:?}")
}
async fn print_map2_handler(query: Query<HashMap<String, String>>) -> String {
format!("query2: {query:?}")
}
测试结果:
/query_1?name=zj&age=18 #result query1: Info { name: "zj", age: 18 }
/query_2?name=zj&age=18 #result query2: Query({"age": "18", "name": "zj"})
path
和 query
提取器一般适用于 GET
请求,那么 POST
请求的参数呢?
捕获请求体的参数
常见的请求体参数一般有两种格式:json
和 form
,我们下面举个例子
use axum::{
routing::{get, post},
Router,
extract::Json,Form
};
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct CreateUser {
username: String,
age: u8
}
#[derive(Deserialize, Debug)]
struct LoginFormData {
field_name: String,
field_pwd: String,
}
#[tokio::main]
async fn main() {
// build our application with a single route
let app = Router::new().route("/", get(|| async { "Hello, World!" }))
.route("/addUser", post(create_user_handler))
.route("/submit", post(submit_form_handler));
// run our app with hyper, listening globally on port 3000
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn create_user_handler(Json(payload): Json<CreateUser>) -> String{
format!("Added user: {payload:?}")
}
async fn submit_form_handler(Form(data): Form<LoginFormData>) -> String {
format!("submit form: {data:?}")
}
测试结果:
post /addUser #result Added user: CreateUser { username: "zsan", age: 18 }
post /submit #result submit form: LoginFormData { field_name: "lis", field_pwd: "123" }
捕获请求头参数
一般通过HeaderMap
来获取请求头参数,它会提取所有标头,如果想获取指定标头需要再处理
use axum::{
routing::get,
Router,
http::header::HeaderMap
};
#[tokio::main]
async fn main() {
// build our application with a single route
let app = Router::new().route("/", get(|| async { "Hello, World!" }))
.route("/header", get(get_header_handler));
// run our app with hyper, listening globally on port 3000
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn get_header_handler(headers: HeaderMap) -> String{
// format!("{:?}", headers) //获取所有请求头
headers
.get(axum::http::header::USER_AGENT)
.and_then(|v| v.to_str().ok())
.map(|v| v.to_string())
.unwrap()
}
测试结果:
(get) /header #result PostmanRuntime/7.43.0
除此之外,axum
还提供了许多有用的提取器,比如 Bytes
, String
, Body
和 BodyStream
用于获取请求正文。你也可以通过实现 FromRequest
来定义你自己的提取器
响应处理
任何实现 IntoResponse
东西 ,都可以被处理程序返回,它将被自动转换为响应,我们来看个例子:
use axum::{
body::Body,
routing::get,
response::Json,
Router,
};
use serde_json::{Value, json};
// `&'static str` becomes a `200 OK` with `content-type: text/plain; charset=utf-8`
async fn plain_text() -> &'static str {
"foo"
}
// `Json` gives a content-type of `application/json` and works with any type
// that implements `serde::Serialize`
async fn json() -> Json<Value> {
Json(json!({ "data": 42 }))
}
let app = Router::new()
.route("/plain_text", get(plain_text))
.route("/json", get(json));
更多详情的例子,看官网
错误处理
Axum 的目标是拥有一个简单且可预测的错误处理模型。这意味着将错误转换为响应很简单,并且可以保证所有错误都得到处理
axum 基于 tower::Service
,它通过其关联的 错误
类型。如果你的 Service
产生错误,并且该错误一直到 hyper,则连接将终止,而不会 发送响应。这通常是不可取的,因此 axum 确保您 始终依赖 type system 生成响应。axum 通过要求所有服务都将 Infallible
作为其错误类型来实现这一点。Infallible
是永远不会发生的错误的错误类型
这段引用自官方文档,但太绕了,其核心意思,说人话就是:发生错误,不能让http连接中断,这导致响应无法正常返回;而是保证响应能正常返回,继而携带错误类型和错误信息;而axum能很轻易地做到~
所以一般axum的处理器绑定的方法都是这样定义:
use axum::http::StatusCode;
async fn handler() -> Result<String, StatusCode> {
// ...
}
虽然看起来可能会因为 StatusCode
失败,但实际上这并不是一个“错误”。 如果此处理程序返回 Err(some_status_code)
,它仍将转换为响应并发送回客户端。 这是通过 StatusCode
的 IntoResponse
实现完成的。
无论您返回 Err(StatusCode::NOT_FOUND)
还是 Err(StatusCode::INTERNAL_SERVER_ERROR)
, 在 axum 中这些都不被视为错误
而不是直接使用 StatusCode
,请使用中间的错误类型,最终可以转换为响应。 这样就可以在处理程序中使用?
运算符
我们来看一个例子:
use axum::{
routing::get,
Router,
body::Body,
http::{Response, StatusCode},
error_handling::HandleError,
};
use tokio::sync::mpsc::error;
use tower;
#[tokio::main]
async fn main() {
// build our application with a single route
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }))
.merge(router_fallible_service())//使用Service的错误处理
.route("/foo", get(get_foo));
// run our app with hyper, listening globally on port 3000
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
fn router_fallible_service() -> Router {
// this service 可能出现任何错误
let some_fallible_service = tower::service_fn(|_req| async {
thing_that_might_fail().await?;
Ok::<_, anyhow::Error>(Response::new(Body::empty()))
});
Router::new().route_service(
"/test_error",
// Service 适配器通过将错误转换为响应来处理错误。
HandleError::new(some_fallible_service, handle_anyhow_error),
)
}
async fn thing_that_might_fail() -> Result<(), anyhow::Error> {
// 模拟报错
anyhow::bail!("thing_that_might_fail")
}
// 将错误转化为 IntoResponse
async fn handle_anyhow_error(err: anyhow::Error) -> (StatusCode, String) {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)
}
async fn get_foo() -> Result<String, StatusCode>{
Ok(String::from("get请求foo方法"))
}
大家可以自行去运行一下,看看返回的响应,而要正常运行上述示例,还需添加依赖
anyhow="1.0.95"
tower = { version = "0.5.2", features = ["full"] }
更多其他用法,请移步axum::error_handling - Rust
中间件
Axum
的独特之处在于它没有自己的定制中间件系统,而是与 Tower 集成。这意味着 tower 和 tower-http 中间件都可以与 Axum 一起使用。axum
也可以将请求路由到任何 tower 服务。可以是你用 service_fn
编写的服务,也可以是来自其他 crate 的东西;从而充分复用和利用不同应用的生态,潜力巨大
中间件功能允许你在请求到达处理程序之前或之后添加自定义逻辑。这为通用功能的实现提供了强大的方法,例如身份验证、日志记录或性能监视
我们这里举个简单的例子:
use axum::{
body::Body,
http::Request,
middleware::{self, Next},
response::Response,
routing::get,
Router
};
// use tower::{Service, Layer};
async fn logging_middleware(req: Request<Body>, next: Next<>) -> Response {
println!("Received a request to {}", req.uri());
next.run(req).await
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(|| async { "Hello, world!" }))
.layer(middleware::from_fn(logging_middleware));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
运行后,用户访问任意请求都会被终端记录下来:
Received a request to /foo
Received a request to /
Received a request to /test
更多例子,请移步axum::middleware - Rust
本文就先到这里啦,一个web框架基本解决的就是文章中的这些方面,后续会探索axum与数据库的集成,或者其他有意思的特性,我们下期再见~
参考资料:
https://docs.rs/axum/latest/axum/index.html
https://www.twilio.com/en-us/blog/build-high-performance-rest-apis-rust-axum
作者:小牛呼噜噜
本文到这里就结束啦,感谢阅读,关注同名公众号:小牛呼噜噜,防失联+获取更多技术干货