rust的tokio是一个很棒的异步运行时,所以tokio出品的axum大概率也是个很棒的框架,处于对异步编程和tokio的喜欢,所以我打算以后都使用axum作为web开发的首选框架。

<!--more-->

因为axum相比rocket和actix两个框架出现的时间比较晚,所以文档并不出色,想要学习axum的各个功能需要去看它的examples,

一个web框架一般要解决以下问题:

  1. 参数获取, 这部分主要由四种类型的参数

    1. 路径参数, 比如http://127.0.0.1/hello/world 这个url里面的路径参数是/hello/world
    2. 查询参数,比如http://127.0.0.1/hello?name=youerning这个url里面的查询参数是name=youerning,
    3. 请求头参数, 这部分不在url里面, 比如常见的User-Agent就是一个请求头参数
    4. 请求体参数, 这部分也不再url里面, 比如客户端以Content-Type: application/json的请求头参数发起 post请求时,就会以json的格式序列化请求体
  2. 状态共享, 这部分主要是共享一些全局状态或者对象,比如说数据库的连接对象
  3. 路由,路由肯定是web框架的基本, 将各个不同的请求路由到不同的handler。
  4. 中间件,web框架一般会提供一种扩展机制控制请求流程,这种机制一般叫做中间件(middleware)
  5. 测试,测试对于一个项目是很重要的,所以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-typeapplication/json, 所以请求体的内容是一个合法的JSON字符串。

HTTP协议自然是包含请求和响应两个过程的,这里暂时只看请求的协议内容。

获取请求参数

web框架获取请求参数一般有两种方式,一种是通过一个web框架提供的特殊对象来集中获取参数,比如go的gin框架使用Context对象来获取各种请求参数,一种是要啥给啥,比如fastapi以及这里的axum,我比较喜欢后者。

路径参数

路径参数一般有以下三种情况

  1. 单个路径参数
  2. 多个路径参数
  3. 多个参数写入到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"}

查询参数

获取查询参数一般有以下两种方法

  1. 将数据反序列化到HashMap
  2. 将数据反序列化到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提供两种获取请求头参数的方式, TypedHeaderHeaderMap,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获取客户端的各种参数,然后基于这些参数就可以按需返回必要的内容给客户端了。

其他的内容就等下一篇文章了。

参考链接

原文链接: https://youerning.top/post/axum/quickstart-1/


又耳笔记
1 声望2 粉丝