Rust web 开发-1.web 框架

2023/11/17 17:49 下午 posted in  rust Rust web

本文介绍第一部分,web 框架。
目前比较出名的 web 框架有

  1. axum
  2. Rocket
  3. actix-web
  4. Hyper
  5. Tide
  6. Warp

接下来主要介绍一下 actix-web,其他框架待后面有时间再介绍。

actix-web

actix-web 服务端的逻辑都是建立在 App 实例上的,通过 App 实例为所有资源和中间件注册路由,存储同一范围内所有线程共享的状态。
接下来从零开始实现一个 actix-web 示例。

  1. 创建rust 工程
cargo new hello-world
  1. Cargo.toml 添加依赖
[dependencies]
actix-web="4"
  1. 使用 actix-web
    在 src/main.rs 中编写如下代码
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};

#[get("/")]
async fn hello() -> impl Responder {
    HttpResponse::Ok().body("Hello world!")
}

#[post("/echo")]
async fn echo(req_body: String) -> impl Responder {
    HttpResponse::Ok().body(req_body)
}

async fn manual_hello() -> impl Responder {
    HttpResponse::Ok().body("Hey there!")
}


#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new( || {
        App::new()
            .service(hello)
            .service(echo)
            .route("/hey", web::get().to(manual_hello))
    })
        .bind(("127.0.0.1", 8099))?
        .run()
        .await
}

HttpServer 绑定到 ip、端口,同时,提供了两个 get 方法(/,/hey),一个 post 方法(/echo),通过浏览器访问 localhost:8099 即可访问。
2022-11-20-13.36.34

HttpServer

HttpServer 接受的参数类型是 application factory,实现了 Send + Sync。

bind,为 server 绑定 ip 和端口。
bind 成功后,HttpServer::run() 返回一个 server 实例。
server 必须使用 await 或者在多线程中才能启动 http 服务器。
server 默认启动的线程数与 CPU 个数相等,可以通过 HttpServer::workers() 改变线程数。

scope

scope 相当于请求路径的前缀

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().service(
            web::scope("/app")
                .route("/hey", web::get().to(manual_hello)),
        )
    })
        .bind(("127.0.0.1", 8099))?
        .run()
        .await
}

这样的话在请求 localhost:8099/hey 就请求不到了,这样要请求的路径是 localhost:8099/app/hey

Shared Mutable State

相当于全局变量、多个线程之间共享的。

web::Data<T>,T 是 Arc 类型。
Arc 和 Mutex 结合使用,实现在多线程间共享数据。

use std::sync::Mutex;

use actix_web::{App, HttpServer, web};

struct AppStateWithCounter {
    counter: Mutex<i32>, // <- Mutex is necessary to mutate safely across threads
}

async fn index(data: web::Data<AppStateWithCounter>) -> String {
    let mut counter = data.counter.lock().unwrap(); // <- get counter's MutexGuard
    *counter += 1; // <- access counter inside MutexGuard

    format!("Request number: {counter}") // <- response with count
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // Note: web::Data created _outside_ HttpServer::new closure
    let counter = web::Data::new(AppStateWithCounter {
        counter: Mutex::new(0),
    });

    HttpServer::new(move || {
        // move counter into the closure
        App::new()
            .app_data(counter.clone()) // <- register the created data
            .route("/", web::get().to(index))
    })
        .bind(("127.0.0.1", 8099))?
        .run()
        .await
}

浏览器请求 localhost:8099,每次刷新 counter+1;

返回 json

一般 web 应用服务端接口返回的数据都是 json 格式,
actix-web 中如何实现返回 json格式的数据:

cargo.toml添加依赖

serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0.2", optional = true }

main.rs

use actix_web::{App, get, HttpResponse, HttpServer, post, Responder, web};
use actix_web::web::Json;
use serde::{ser, Serialize};

#[derive(Serialize)]
pub struct JsonSuccess<T: ser::Serialize> {
    pub code: u32,
    pub data: Option<T>,
    pub error: Option<String>,
}

#[derive(Serialize)]
pub struct JsonError {
    pub code: u32,
    pub data: Option<String>,
    pub error: Option<String>,
}

pub fn success<T: ser::Serialize>(r: Option<T>) -> HttpResponse {
    HttpResponse::Ok().json(JsonSuccess {
        code: 0,
        data: r,
        error: None,
    })
}

pub fn error(err: Option<String>) -> HttpResponse {
    HttpResponse::Ok().json(JsonError {
        code: 1,
        data: None,
        error: err,
    })
}


#[get("/")]
async fn hello() -> HttpResponse {
    success(Option::from("Hello"))
}


#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(hello)
    })
        .bind(("127.0.0.1", 8099))?
        .run()
        .await
}

2022-11-20-14.58.01

接收请求参数

Path

use actix_web::{App, get, HttpResponse, HttpServer, post, Responder, web,Result};
use actix_web::web::Json;
use serde::{ser, Serialize};

/// extract path info from "/users/{user_id}/{friend}" url
/// {user_id} - deserializes to a u32
/// {friend} - deserializes to a String
#[get("/users/{user_id}/{friend}")] // <- define path parameters
async fn index(path: web::Path<(u32, String)>) -> HttpResponse {
    let (user_id, friend) = path.into_inner();
    success(Some(format!("Welcome {}, user_id {}!", friend, user_id)))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().service(index))
        .bind(("127.0.0.1", 8099))?
        .run()
        .await
}

web::Path<(u32,String)> 接收这个 get("/users/{user_id}/{friend}") 请求的两个参数

将这两个参数定义为 struct 也是可以的

#[derive(Deserialize)]
struct Info {
    user_id: u32,
    friend: String,
}

/// extract path info using serde
#[get("/users/{user_id}/{friend}")] // <- define path parameters
async fn index(path: web::Path<(u32, String)>) -> HttpResponse {
    let (user_id, friend) = path.into_inner();
    success(Some(format!("Welcome {}, user_id {}!", friend, user_id)))
}

get 请求,接收参数

#[derive(Deserialize)]
struct Info {
    username: String,
    age:i32
}

#[get("/")]
async fn index(info: web::Query<Info>) -> HttpResponse {
    success(Some(format!("Welcome {}!,age = {}", info.username,info.age)))
}

post 请求,接收 json

#[derive(Deserialize)]
struct Info {
    username: String,
    age:i32
}

#[post("/")]
async fn index(info: web::Json<Info>) -> HttpResponse {
    success(Some(format!("Welcome {}!,age = {}", info.username,info.age)))
}

middleware

middleware 系统可以让我们针对请求或者响应添加自定义操作。

  1. Pre-process the Request:可以在请求时做前置处理
  2. Post-process a Response:在响应时做后置处理
  3. Modify application state:修改 state
  4. Access external services (redis, logging, sessions):访问外部服务

如果有多个中间件,后添加的会先执行。

类似 Java 中的 AOP,只不过这个中间件只是针对 Controller 的。

自定义 middleware 要实现 Service和 Transform 两个 trait。
先用 wrap_fn 做一个简单的 middleware:

cargo.toml添加依赖

futures-util = "0.3"

main.rs

#[get("/{name}")]
async fn hello(path:web::Path<String>) -> HttpResponse {
    let name = path.to_owned();
    success(Option::from(format!("Hello {}",name)))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .wrap_fn(|req, srv| {
                // Pre-process the Request
                println!("Hi from start. You requested: {}", req.path());
                srv.call(req).map(|res| {
                    // Post-process a Response
                    println!("Hi from response");
                    res
                })
            })
            .service(hello)
    })
    .bind(("127.0.0.1", 8099))?
    .run()
    .await
}

请求 http://localhost:8099/tom
控制台会打印出

接下来,做一个比较常见的中间件--鉴权中间件。

impl<S, B> Service<ServiceRequest> for AuthMiddleWare<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

    forward_ready!(service);

    fn call(&self, req: ServiceRequest) -> Self::Future {
        let value = HeaderValue::from_str("").unwrap();
        let token: &HeaderValue = req.headers().get("token").unwrap_or(&value);
        if token.len() > 0 || req.path().to_string() == "/login" {
            let fut: <S as Service<ServiceRequest>>::Future = self.service.call(req);
            Box::pin(async move {
                let res = fut.await;
                res
            })
        } else {
            Box::pin(async move { Err(ErrorUnauthorized("PLEASE LOGIN")) })
        }
    }
}


#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .wrap(middleware::timed::Timed)
            .wrap(middleware::auth::Auth)
            .service(hello1)
            .service(hello2)
    })
        .bind(("127.0.0.1", 8099))?
        .run()
        .await
}

这里是写了两个 middleware,一个打印接口耗时,一个鉴权,代码篇幅较多,放在了 github

浏览器访问 hello1,由于没有 token,所以鉴权不通过,而通过 postman 访问 hello2,header 上加上 token,就可以请求通过。

小结

第一部分关于 web 框架就介绍到这里了,主要介绍了 actix-web 的使用:如何写一个接口、如何接收参数、如何返回 json 、如何通过中间件鉴权。