23. rust服务器开发:axum详解

一、前言

因为rust拥有着极高的性能,加上tokio这个非常优秀的运行时,这使得rust很适合被用于开发服务器。

目前rust已经拥有了众多的web框架,例如rocket、axum、axtix-web、tide、warp等等。

而axum这个web框架是由tokio团队所维护的,相对来说,可能在某些方面更加专业,所以本文也将以axum框架来介绍rust的web框架开发。

二、基本使用

axum框架是构建于tokio之上的,所以你想要使用axum,首先就需要添加tokio包。

下面是一个最小的依赖包:

[dependencies]
axum = "0.7.5"
tokio = { version = "1.39.2", features = ["full"] }

将上面两个依赖项复制到你的项目配置文件中,我们就可以开始开发web项目了。

首先,写好main.rs文件中的异步入口函数:

#[tokio::main]
async fn main() {

}

不清楚tokio基本使用与原理的,可以先去看看这篇文章:rust异步库tokio入门

然后写好基本的axum服务器启动代码:

use axum::{routing::get, Router};

#[tokio::main]
async fn main() {

    let app = Router::new().route("/", get(root));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}
  
async fn root() -> &'static str {
    "Hello, World!"
}

首先是定义路由:

let app = Router::new().route("/", get(root));

所谓路由,就是我们在浏览器中看到的路径,比如本网站的一篇文章在浏览器上的链接为:

https://www.kucoding.com/article/229.html

去除前面的协议、域名,剩下的就是路由了:

/article/229.html

所以这里定义的路由/代表的就是网站的根目录。

每个路由都可以接收所有可能的HTTP请求,比如常见的GET、POST请求等,这些请求都对应于axum提供的方法。

比如GET请求,这里就对应于get方法,get方法的参数是一个处理该get请求的函数,比如这里的root函数。

更多路由定义方式会在后文介绍。

定义好了路由之后,我们需要为我们的服务器绑定一个地址:

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();

注意这里使用的是tokio库中的TcpListener,而不是标准库中的。

用其绑定了地址后,还需要将两者结合、应用一下:

axum::serve(listener, app).await.unwrap();

至此,我们一个最简单的服务器就搭建完成了,此时你就可以运行本程序:cargo run

并可以通过127.0.0.1:3000这个地址,在你的浏览器访问:

image.png

因为我么的根路径get请求所对应的函数只返回了一个字符串,其默认是文本类型、而非html文档,所以这里的背景是黑色的。

至此,我们就学会了axum框架的基本用法,下面,我们开始详解axum提供的各个方面的强大功能。

三、路由定义

首先我们如果想要在相同路由下定义多个请求方法的处理函数,可以这样做:

let app = Router::new().route("/", get(get_root).post(post_root));

也就是链式调用,get方法的返回值可以继续调用其上的post方法,传入post方法的处理函数。

当然也包括其它的处理请求方法,比如delete、put等等,都可以继续向上面这样写。

但要注意,虽然这里只有一个路由,但根据你处理该路由的请求方法数量不同,该路由所对应的处理函数就可能不止一个。

简单来说就是,一个路由可以有多个处理函数,每个处理函数都对应着一个处理HTTP请求方法。

而如果想要定义多个路由,那么可以这样做:

    let app = Router::new()
        .route("/", get(get_root))
        .route("/article", get(get_article));

方法也很简单,就是不断的在后面添加route函数、添加新的路由即可。

但要注意,同一个路由路径不能添加两次,否则运行会报错,如果想要处理其它方法,同样也是引入其它方法所对应的函数即可:

use axum::{routing::{get, post}, Router};

    let app = Router::new()
        .route("/", get(get_root))
        .route("/article", post(post_article));

这些方法都放在了routing这个子模块下。

当然,我们也可以使用动态路由:

use axum::{extract::Path, routing::get, Router};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/article/:id", get(get_article));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn get_article(Path(id): Path<u64>) -> String {
    format!("你请求的文章id为:{}", id)
}

注意动态路由的写法,是在/后面添加:id实现的,重点是这个:符号,它标记了这是一个动态的路由。

此时它所对应的处理函数,你就可以通过Path这个由axum提供的方法,将动态路由提取出来。

注意这里的写法:Path(id): Path<u64>,是要写两遍的,前面是变量名,后面是变量类型,只不过外面多用了一层Path包裹了一下,含义就是从路径中提取变量id。

此时我们启动服务器,随便输入一个数字访问该路由:

image.png

实际开发中,此时你就能通过这个数字,在自己的数据库中找到其对应的文章,并将其返回给用户进行展示了。

当然,你是可以定义多个动态路由的:

use axum::{extract::Path, routing::get, Router};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/article/:sid/:id", get(get_article));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn get_article(Path((id, id1)): Path<(u64, u64)>) -> String {
    format!("你请求的文章id为:{} {}", id, id1)
}

但提取方法并不是写多个Path参数,而是用元组的方式进行填写。

同时要注意的是,你在路径中填写的动态路由名字,和函数参数名并不是对应的,它是直接按顺序进行提取的:

image.png

因为id放在了参数的第一个,所以此时id就等于第一个动态路由参数,id1就等于第二个动态路由参数,这和我们填在路径中的/:sid/:id名字毫不相干。

更进一步,上面介绍的仅仅只是少量路由开发,看起来没有什么问题。

但如果随着路由数量增多,全部像上面这样挤在一起写,就会导致很难维护。

所以我们就有对路由进行分组的需求,可以像下面这样做:

    let user = Router::new()
        .route("/login", method_router)
        .route("/register", method_router);
        
    let article = Router::new()
        .route("/view", method_router)
        .route("/edit", method_router);

    let app = Router::new()
        .route("/", get(root))
        .nest("/user", user)
        .nest("/article", article);

上面就是定义了两个模块的路由,分别是用户模块以及文章模块,其模块内部的路由分别处理各种的逻辑。

最后,我们只需要在主模块路上,使用nest函数将子模块集成进来即可。

比如此时我将user模块集成到了/user路由上,那么user模块内部的所有路由前面都会被自动添加上/user,比如登录路由的实际访问路由其实是/user/login,而不仅仅只是user内部写的/login

当然,它并不是只能集成一层,你还可以将User模块继续细分,比如普通用户、管理员用户等等,各自维护属于自己模块的路由,然后通过nest函数集成进User模块、然后User模块再集成进主模块即可。

四、参数提取

常用的HTTP方法,一般都是需要携带参数的。

参数的分布主要有四个位置:

  • 路由
  • 查询
  • 请求头
  • 请求体

其中路由已经在前面提到过了,可以直接通过Path提取路由中的参数。

然后是查询,查询一般都用于GET请求,比如一个常见的路由:

/article?id=1024&is_vip=true

路由?符号后面的东西就是查询参数,是由键值对组成的,每个键值对之间用&分隔。

想要在代码中捕获这些路由参数,我们可以这样做:

use axum::{extract::Query, routing::get, Router};
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
struct ArtParam {
    id: u64,
    is_vip: bool,
}

#[tokio::main]
async fn main() {
作者:余识
全部文章:0
会员文章:0
总阅读量:0
c/c++pythonrustJavaScriptwindowslinux