rust的tokio是一个很棒的异步运行时,所以tokio出品的axum大概率也是个很棒的框架,处于对异步编程和tokio的喜欢,所以我打算以后都使用axum作为web开发的首选框架。
<!--more-->
因为axum相比rocket和actix两个框架出现的时间比较晚,所以文档并不出色,想要学习axum的各个功能需要去看它的examples,
一个web框架一般要解决以下问题:
参数获取, 这部分主要由四种类型的参数
- 路径参数, 比如
http://127.0.0.1/hello/world
这个url里面的路径参数是/hello/world
- 查询参数,比如
http://127.0.0.1/hello?name=youerning
这个url里面的查询参数是name=youerning
, - 请求头参数, 这部分不在url里面, 比如常见的
User-Agent
就是一个请求头参数 - 请求体参数, 这部分也不再url里面, 比如客户端以
Content-Type: application/json
的请求头参数发起 post请求时,就会以json的格式序列化请求体
- 路径参数, 比如
- 状态共享, 这部分主要是共享一些全局状态或者对象,比如说数据库的连接对象
- 路由,路由肯定是web框架的基本, 将各个不同的请求路由到不同的handler。
- 中间件,web框架一般会提供一种扩展机制控制请求流程,这种机制一般叫做中间件(middleware)
- 测试,测试对于一个项目是很重要的,所以web框架会提供对应的测试接口
本文主要以这五个方面来介绍axum, 由于涉及方面比较多,所以文章会分段。
快速入门
下面是一个和官方文档类似的Hello World。
use axum::{response::Html, routing::get, Router};
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(handler));
let addr = "0.0.0.0:8080";
axum::Server::bind(&addr.parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
async fn handler() -> Html<&'static str> {
Html("<h1>Hello, World!</h1>")
}
对应的依赖如下:
[dependencies]
axum = { version="0.6", features=["default", "headers"] }
axum-extra = { version = "0.8" }
tokio = { version = "1.0", features = ["full"] }
reqwest = { version="0.11.22", features=["json", "multipart"]}
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.107"
HTTP协议
下面是HTTP/1.1的语法定义, 详细内容可参考: https://www.rfc-editor.org/rfc/rfc7230
HTTP-message = start-line *( header-field CRLF ) CRLF [ message-body
start-line = request-line / status-line
request-line = method SP request-target SP HTTP-version CRLF
status-line = HTTP-version SP status-code SP reason-phrase CRLF
header-field = field-name ":" OWS field-value OWS
OWS = *( SP / HTAB )
; optional whitespace
上面的内容叫做ABNF语法,有兴趣的可以搜索学习一下,总的来说,HTTP协议有着固定的格式,只有满足这个格式才算一个合法的HTTP请求,上面的内容对于不会ABNF语法的人来说太枯燥了,这里也不打算深入,所以下面是一个更加友好的例子。
HTTP/2以前,HTTP的一个很大有点就是协议以文本的方式存在,这好处是可以直接阅读,坏处没有二进制协议效率高。
POST /hello/world?name=youerning.top HTTP/1.1
User-Agent: curl/7.29.0
Host: youerning.top
Accept: */*
Content-type: application/json
Content-Length: 24
{"name":"youerning.top"}
有兴趣的可以使用telnet连接youerning.top然后发送上面的内容。
第一行就是方法(method), 请求目标(request-target)和状态行(HTTP-version), 它们以空格分隔,方法很好理解,就是GET, POST等, 请求目标包含路径参数和查询参数,其中/hello/world
是路径参数, name=youerning.top
二到六行是请求头参数, 他们提供了一系列的键值对,比较出名的就是User-Agent
了,有一个比较重要的参数是Content-Length
,这个参数告诉服务端自己发送的请求体的大小,如果错误的话,会导致请求体无法正确识别,一般来说,终端用户不需要在意。
最后一行就是请求体了,因为请求的Content-type
是application/json
, 所以请求体的内容是一个合法的JSON字符串。
HTTP协议自然是包含请求和响应两个过程的,这里暂时只看请求的协议内容。
获取请求参数
web框架获取请求参数一般有两种方式,一种是通过一个web框架提供的特殊对象来集中获取参数,比如go的gin
框架使用Context
对象来获取各种请求参数,一种是要啥给啥,比如fastapi
以及这里的axum
,我比较喜欢后者。
路径参数
路径参数一般有以下三种情况
- 单个路径参数
- 多个路径参数
多个参数写入到hashmap
除了HashMap还可以反序列化到Deserialize对象,以及Vec对象, 详情参考:https://docs.rs/axum/latest/axum/extract/path/struct.Path.html
代码如下:
use axum::{response::Html, routing::get, Router, extract::Path};
use std::collections::HashMap;
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(handler))
.route("/path1/:name", get(path_handler1))
.route("/path2/:name/:age", get(path_handler2))
.route("/path3/:fullpath", get(path_handler3));
let addr = "0.0.0.0:8080";
axum::Server::bind(&addr.parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
async fn handler() -> Html<&'static str> {
Html("<h1>Hello, World!</h1>")
}
async fn path_handler1(Path(name): Path<String>) -> String {
format!("name: {name}")
}
async fn path_handler2(Path((name, age)): Path<(String, i64)>) -> String {
format!("name: {name}, age: {age}")
}
async fn path_handler3(Path(fullpath): Path<HashMap<String, String>>) -> String {
format!("path: /{fullpath:?}")
}
请求和响应对应如下:
/path1/youerning.top: name: youerning.top
/path2/youerning.top/11: name: youerning.top, age: 11
http://127.0.0.1:8080/path3/youerning.top/123: path: /{"age": "123", "name": "youerning.top"}
查询参数
获取查询参数一般有以下两种方法
- 将数据反序列化到HashMap
- 将数据反序列化到Deserialize对象
use axum::{response::Html, 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() {
let app = Router::new()
.route("/", get(handler))
.route("/query1", get(query2hashmap_handler))
.route("/query2", get(query2deserialize_handler));
let addr = "0.0.0.0:8080";
axum::Server::bind(&addr.parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
async fn handler() -> Html<&'static str> {
Html("<h1>Hello, World!</h1>")
}
async fn query2hashmap_handler(query: Query<Info>) -> String {
let info: Info = query.0;
format!("query1: {info:?}")
}
async fn query2deserialize_handler(query: Query<HashMap<String, String>>) -> String {
format!("query2: {query:?}")
}
请求和响应对应如下:
/query1?name=youerning.top&age=123: 'query1: Info { name: "youerning.top", age: 123 }'
/query2?name=youerning.top&age=123: 'query2: {"name": "youerning.top", "age": "123"}'
请求头参数
axum
提供两种获取请求头参数的方式, TypedHeader
和HeaderMap
,axum推荐使用前者,原因就如它的名字那样,具有类型,不过只可以获取单个参数,而后者可以获取所有的请求头参数。
use axum::{
response::Html,
routing::get, Router,
extract::TypedHeader,
http::{Request, header::HeaderMap},
headers::UserAgent
};
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(handler))
.route("/header1", get(header_handler))
.route("/headers2", get(headers_handler));
let addr = "0.0.0.0:8080";
axum::Server::bind(&addr.parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
async fn handler() -> Html<&'static str> {
Html("<h1>Hello, World!</h1>")
}
async fn header_handler(TypedHeader(user_agent): TypedHeader<UserAgent>) -> String {
format!("header.user_agent: {user_agent:?}")
}
async fn headers_handler(headers: HeaderMap) -> String {
format!("headers: {headers:?}")
}
请求和响应对应如下:
/header1: 'header.user_agent: UserAgent("python-requests/2.28.1")'
/headers2: 'headers: {"host": "127.0.0.1:8080", "user-agent": "python-requests/2.28.1", "accept-encoding": "gzip, deflate", "accept": "*/*", "connection": "keep-alive"}'
请求体参数
常见的请求体参数一般有两种,json和form,这里为了简单就只展示json的代码了。
注意: 这里要使用post方法包装处理函数
use axum::{response::Html, routing::{post, get}, Router, extract::Json};
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Deserialize, Debug)]
struct Info {
name: String,
age: u8,
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(handler))
.route("/json", post(json_handler))
.route("/json2", post(json_handler2));
let addr = "0.0.0.0:8080";
axum::Server::bind(&addr.parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
async fn handler() -> Html<&'static str> {
Html("<h1>Hello, World!</h1>")
}
async fn json_handler(Json(info): Json<Info>) -> String {
format!("info: {info:?}")
}
async fn json_handler2(Json(info): Json<HashMap<String, String>>) -> String {
format!("info: {info:?}")
}
请求和响应对应如下:
/json: 'info: Info { name: "youerning", age: 18 }'
/json2: 'info: {"name": "youerning", "age": "18"}'
值得注意的是,/json2的请求体如果age用数字类型请求会报反序列化失败的错。
小结
至此我们可以通过axum
提供的各种extractor
获取客户端的各种参数,然后基于这些参数就可以按需返回必要的内容给客户端了。
其他的内容就等下一篇文章了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。