跳转至

rust web框架actix入门

鸣谢

感谢译者的辛苦翻译, 可以帮译者在git上点一个星星

actix中文翻译git地址

hello-world.rs

use actix_web::{Error,middleware, web, App, HttpRequest, HttpServer, Responder, HttpResponse,guard, dev::HttpResponseBuilder};
use serde::{Deserialize,Serialize};
use actix_web::http::{HeaderName, HeaderValue, StatusCode};
use cookie::Cookie;
use actix_files::{Files, NamedFile};
use std::sync::Mutex;
use futures::future::{ready, Ready};
use std::fs::File;
use futures::StreamExt;


async fn index(req: HttpRequest) -> &'static str {  // return 的参数会转为 Responder
    println!("REQ: {:?}", req);  // 打印请求的相关信息
    "Hello world!"
}

async fn cookies() -> impl Responder{
    /*
    cookie 练习
    */
    let mut  res = HttpResponse::Ok();
    res.cookie(Cookie::build("say", "hello").finish());  // 设置一个 cookie
    res.finish()
}

async fn header() -> impl Responder {
    /*
    header 练习
    */
    let mut  res = HttpResponse::Ok();
    res.header("language", "python")  // 添加一个请求头(无论有没有相同的请求头, 都会新增一个响应头, 同样的 key 可能会有多个)
        .set_header("language", "javascript")  // 设置一个请求头(设置一个响应头(如果没有该key则会新建一个, 如果有则会覆盖))
        .set_header("say", "hello1")  // 设置一个响应头(如果没有该key则会新建一个, 如果有则会覆盖)
        .header("say", "hello2");  // 添加一个响应头
    res.finish()
}


#[derive(Deserialize,Serialize,Debug)]  // 为结构体实现序列化和反序列化的功能
struct UserInfo{
    user_id:i32,
    mobile:String
}

async fn get_user1(req:HttpRequest, params: web::Path<(UserInfo)>,) -> impl Responder{

    // Path 可以从请求中提取 路径中的参数
    println!("get_user1_REQ: {:?}", req);

    let p = params.0;  // 接收到的外部params参数 可以通过下标0 取出
    println!("收到了用户: {:?} 请求", p);

    let mut res = HttpResponse::Ok();

    res.json(p)
}
async fn get_user2(req:HttpRequest) -> impl Responder{
    println!("获得Path 参数相关 req.match_info(): {:?}", req.match_info());

    let user_id: i32 = req.match_info().query("user_id").parse().unwrap();  // 使用 query
    let mobile: String = req.match_info().get("mobile").unwrap().parse().unwrap();  // 使用 get
    HttpResponse::Ok().json(UserInfo{user_id:user_id,mobile:mobile})
}
async fn get_user3(info:web::Query<UserInfo>) -> impl Responder{
    println!("仅仅在请求查询字符串中有 'UserInfo' 相关字段时才会被调用info: {:?}", info);

    HttpResponse::Ok().json(info.into_inner())
}
async fn get_user4(req:HttpRequest,info:web::Json<UserInfo>) -> impl Responder{
    // 从body中接收了一个 json 数据 info
    HttpResponse::Ok().json(info.into_inner())
}
async fn get_user5(req:HttpRequest,info:web::Form<UserInfo>) -> impl Responder{
    // 从body中接收了一个 表单数据 数据 info
    // 仅当 content type 类型是  *x-www-form-urlencoded* 是 handler处理函数才会被调用
    // 且请求中的内容能够被反序列化到一个 "FormData" 结构体中去
    HttpResponse::Ok().json(info.into_inner())
}
// async fn my_body(info:String) -> impl Responder{  // payload 转为字符串
async fn my_body(mut info:web::Payload) -> impl Responder{
    // TODO 我这里提取不出来 可能是版本不对
    // info.next().await
    HttpResponse::Ok()
}


#[derive(Debug)]
struct Status{
    count:Mutex<i32>  // <- Mutex 必须安全的在线程之间可变
}

async fn get_states_less(data:web::Data<Status>) -> String{
    /*
    由 app 传入的数据
    */
    let mut count = data.count.lock().unwrap();  // 等待获取锁

    *count -= 1;  // 获取到锁之后 减少1

    println!("接收到了一个 state, 减少1后: {:?}", count);

    format!("count: {}", count)
}
async fn get_states_inc(data:web::Data<Status>) -> String{
    /*
    由 app 传入的数据
    */
    let mut count = data.count.lock().unwrap();  // 等待获取锁

    *count += 1;  // 获取到锁之后 增加1

    println!("接收到了一个 state, 增加1后: {:?}", count);

    format!("count: {}", count)
}


// 自定义 struct
#[derive(Serialize)]
struct Student{name: String}
// 为自定义struct 实现 Responder
impl Responder for Student{
    type Error = Error;
    type Future = Ready<Result<HttpResponse, Error>>;

    fn respond_to(self, _req: &HttpRequest) -> Self::Future {
        let body = serde_json::to_string(&self).unwrap();

        // 创建响应并设置Content-Type
        ready(Ok(HttpResponse::Ok()
            .content_type("application/json")
            .body(body)))
    }
}
async fn my_response()-> impl Responder{  // 返回自定义类型
    Student{name:"小明".to_string()}
}


async fn my_err1(file_name:web::Path<String>) -> std::io::Result<NamedFile>{
    // 如果一个处理函数在一个Result中返回Error(指普通的Rust trait std::error:Error),此Result也实现了ResponseError trait的话, actix-web将使用相应的actix_web::http::StatusCode 响应码作为一个Http response响应渲染.默认情况下会生成内部服务错误
    // impl<T: Responder, E: Into<Error>> Responder for Result<T, E>{}  // 这里为普通的 Result 实现了一些 trait, 可以点进 Responder 看

    println!("{:?}", format!("static/{}", file_name));
    let ret = NamedFile::open(format!("static/{}", file_name));
    ret
}

// 导入自定义错误需要的包
use derive_more::{Display, Error};
use actix_web::dev::BodyEncoding;

// 定义结构体(需要实现相关 trait)
#[derive(Debug,Display,Error)]
#[display(fmt = "my error: {}", msg)]
struct ErrResp{msg:&'static str}
// 为结构体使用 ResponseError 的默认实现即可 (ResponseError 的一个默认实现 error_response() 会渲染500)
impl actix_web::error::ResponseError for ErrResp{}
async fn my_err2() -> Result<HttpResponse,ErrResp>{
    Err(ErrResp{msg:"自定义的错误默认实现了 500"})  // 返回自定义的错误
}

// 使用 enum 重写 ResponseError 产生更多样的错误进行返回
#[derive(Debug,Display,Error)]
enum ErrEnum{
    #[display(fmt = "ServerError: {}",msg)]
    ServerError{msg:&'static str},
    #[display(fmt = "Forbidden: {}",msg)]
    Forbidden{msg:&'static str},
    #[display(fmt = "ResError: {}",msg)]
    ResError{msg:&'static str}
}
// 重写
impl actix_web::error::ResponseError for ErrEnum{
    fn status_code(&self) -> StatusCode {
        match *self{
            ErrEnum::ServerError{..} => StatusCode::from_u16(500).unwrap(),  // 自定义状态码 100 - 599
            ErrEnum::Forbidden{..} => StatusCode::FORBIDDEN,  // 使用预设的状态码
            ErrEnum::ResError{..} => StatusCode::from_u16(599).unwrap(),
        }
    }

    fn error_response(&self) -> HttpResponse {
        let mut resp = HttpResponseBuilder::new(self.status_code());
        resp.set_header(
            actix_web::http::header::CONTENT_TYPE,
            actix_web::http::header::HeaderValue::from_static("text/plain; charset=utf-8"),
        );
        resp.body(self.to_string())  // impl<T: fmt::Display + ?Sized> ToString for T
    }
}

async fn my_err3(flag:web::Path<i32>)-> Result<HttpResponse,ErrEnum>{
    let flag = flag.into_inner();
    match flag {
        0 => Ok(HttpResponse::Ok().body("成功")),
        1 => Err(ErrEnum::Forbidden{msg:"没有权限"}),
        _ => Err(ErrEnum::ServerError{msg:"服务器内部错误"})
    }
}

// 统一错误处理使用 map_err, 隐藏真正的错误到给用户展示
async fn my_err4()-> Result<HttpResponse,ErrEnum>{
    // 假设中间发生了一些错误
    let e:Result<HttpResponse,&str> = Err("2233");  // 模拟一个错误
    e.map_err(|e| {
        println!("发生一个错误: {:?}", e);
        // 处理错误
        ErrEnum::ServerError { msg: "出错了嗷" }
    })?;

    Ok(HttpResponse::Ok().finish())
}


async fn res(flag:web::Path<i32>) -> actix_web::Result<HttpResponse>{
    println!("进入 url_for1 视图, 接收到参数: {:?}", flag);
    Ok(
        HttpResponse::Ok().body(flag.into_inner().to_string())
    )
    // let url = req.url_for("foo", &["1", "2", "3"])?;
    // println!("url: {:?}", url);
    // Ok(
    //     HttpResponse::Ok()
    //     .set_header(actix_web::http::header::LOCATION, url.as_str())
    //     .finish()
    // )
}

async fn url_for(req:HttpRequest) -> actix_web::Result<HttpResponse>{

    let url = req.url_for("res_name", &["666"]).unwrap();
    println!("即将跳转: {:?}", url);

    // 使用 Found 跳转
    Ok(HttpResponse::Found()
        .header(actix_web::http::header::LOCATION, url.as_str())  // 跳转
        .finish())
}





#[actix_web::main]
async fn main() -> std::io::Result<()> {
    std::env::set_var("RUST_LOG", "actix_web=info");
    env_logger::init();

    // 关于路径 前缀没有包含斜杠,那么会默认自动的插入一个斜杠

    // 请求处理器
    // 默认情况下 actix-web 为 &‘static str , String 等提供了 Responder 的标准实现. 完整的实现清单可以参考 Responder documentation
    // 请求处理发生在两个阶段.首先处理对象被调用,并返回一个实现了 Responder trait的任何对象.然后在返回的对象上调用 respond_to() 方法,将其 自身转换成一个 HttpResponse 或者 Error
    // 请求处理器可以自定义返回类型, 相应的, 自定义返回的类型需要实现 Responder trait


    // 创建一个共享可变状态, 下面使用 move 移动到app中
    let counter = web::Data::new(Status { count: Mutex::new(0) });

    HttpServer::new(move || {
        App::new()
            // enable logger
            .wrap(middleware::Logger::default())  // 添加一个 log 中间件, 用来记录请求
            // .wrap(middleware::NormalizePath::default())  // TODO 这里我并没有发现有嘛用,启用了之后下面的资源都404了, 路径正规化并重定向: 比如请求的 ///res//// 很多斜杠的资源,在适配的时候会替换成 /res/

            // 状态 state
            // .data(Status { count: 0 })// 这里注册一个 普通状态, 状态通过 data 向视图中传递,State也能被中间件访问, 当然也可以单独给 service中的 resource 单独使用,
            .app_data(counter.clone())// 共享可变状态 状态通过 .app_data 向视图中传递 , 为了防止所有权转移需要 clone

            .service(  // service 用来注册一个服务
                       web::resource("/index.html").route(web::get().to(|| actix_web::web::HttpResponse::Ok().json("hello index world"))))// 这里 return 的 &str 实现了Responder trait, 所以会内部调用相关 方法
            // .service(web::resource("/").to(index))  // 一个普通的响应体

            .service(web::resource("/my_response")  // 返回自定义类型(为自定义类型实现 Responder trait, 即可)
                .route(web::get().to(my_response)))

            // 请求头和cookie
            .service(web::resource("/cookie")
                .route(web::get().to(cookies)))
            .service(web::resource("/header")  // 设置请求头相关
                .route(web::get().to(header)))

            // 视图中的共享可变 状态 state
            .service(web::resource("/get_states_less")  // 在视图中获取上面注册的状态 修改减1
                .route(web::get().to(get_states_less)))
            .service(web::resource("/get_states_inc")  // 在视图中获取上面注册的状态 修改加1
                .route(web::get().to(get_states_inc)))


            // 路由分发以及守卫  默认的情况下路由不包含任何的 guards, 因此所有的请求都可以被默认的处理器(HttpNotFound)处理.
            // web::get() 和 web::post() 返回一个 Route也就是路由
            // 路由上配置的所有guard,进来的请求都必须满足才能通过
            // 如果在检查期间注册在路由上的任意一个保护(guard)参数配置返回了false 那么当前路由就会被跳过,然后继续通过有序的路由集合进行匹配.
            .service(web::resource("/guard")  // 布置一个路由守卫, 其内可以实现一些方法更详细的文档:  https://docs.rs/actix-web/3.0.2/actix_web/guard/index.html#functions

                // .guard(guard::fn_guard(|req|  // 给整个resource 进行校验,如果请求头没有包含user_info就返回了false 返回404
                //     req.headers.contains_key("user_info"))
                // )

                .route(web::route()  // 给整个resource进行校验, 进行自定义返回
                    .guard(guard::fn_guard(
                        |req| !req.headers.contains_key("user_info")  // 如果返回 true 则进入 to视图中,这里使用了 ! 返回了 false
                    ))
                    .to(|| {  // 如果校验为 true 则进入到此分支,返回自定义内容, 如果返回false则跳过当前路由, 继续匹配下面的路由
                        println!("resource 校验失败");
                        HttpResponse::Forbidden()
                    }))

                .route(web::get() // 单独给 某个 路由, 请求方式添加守卫
                    .guard(guard::fn_guard(|req| {
                        println!("子路由 通过了校验");
                        true
                    }))
                    .to(|| {
                        println!("进入到了视图");
                        web::HttpResponse::Ok().finish()
                    })))
                // Route::guard() - 注册一个新的守卫, 每个路由都能注册多个guards.
                // Route::method() - 请求方法过滤守卫, 每个路由都能注册多个guards.
                // Route::to() - 为某个路由注册一个处理函数. 仅能注册一个处理器函数. 通常处理器函数在最后的配置操作时注册.
                // Route::to_async() - 注册一个异步处理函数. 仅能注册一个处理器函数. 通常处理器函数在最后的配置操作时注册.


            // 提取器
            // http://127.0.0.1:8080/user1/2233/13673429931
            .service(web::resource("/user1/{user_id}/{mobile}")  // 使用 path 提取器 序列化与反序列化 获取路径参数相关, 并返回application/json数据
                .route(web::get().to(get_user1)))
            // http://127.0.0.1:8080/user2/2233/13673429931
            .service(web::resource("/user2/{user_id}/{mobile}")  // 使用 match_info().get 或者 match_info().query 方法提取参数
                .route(web::get().to(get_user2)))
            // http://127.0.0.1:8080/user3?user_id=1718&mobile=13673429931
            .service(web::resource("/user3")  // 查询字符串 Query 类型为请求参数提供了提取功能. 底层是使用了 serde_urlencoded 包功能
                .route(web::get().to(get_user3)))
            // JSON格式; {"user_id": 123,"mobile": "13673429931" }
            .service(web::resource("user4")  // post 请求体中的 JSON 数据, 可以使用Json提取器允将请求 body 中的 JSON 信息反序列化到一个结构体中
                .route(web::post().to(get_user4)))
            // 表单格式: user_id = 2233, mobile = "13673429931"
            .service(web::resource("user5")  // post 请求体中的 表单From 数据, 使用 web::From<T> 提取, 注意请求的类型必须是 application/x-www-form-urlencoded
                .route(web::post().to(get_user5)))
            // 提取整个请求体使用 web::Payload, 也可以使用 String 将整个payload 转为 string类型
            .service(web::resource("body")
                .route(web::post().to(my_body)))
            // Data - 如果你需要访问应用程序状态的话.
            // HttpRequest - 如果你需要访问请求, HttpRequest本身就是一个提取器,它返回Self.
            // String - 你可以转换一个请求的playload成一个String. 可以参考文档中的example
            // bytes::Bytes - 你可以转换一个请求的playload到Bytes中. 可以参考文档中的example
            // Playload - 可以访问请求中的playload. example

            // 异常处理
            // http://127.0.0.1:8080/my_err1/1.txt
            .service(web::resource("/my_err1/{file_name}")  // 基本错误处理: Actix-web提供了一些常见的非actix error的 ResponseError 实现
                .route(web::get().to(my_err1)))
            .service(web::resource("my_err2")  // 自定义错误响应示例, 为 struct 使用 ResponseError 的默认实现即可
                .route(web::get().to(my_err2)))
            // http://127.0.0.1:8080/my_err3/0
            .service(web::resource("my_err3/{flag}")  // 重写 ResponseError 让自定义错误 产生更多样的错误进行返回
                .route(web::get().to(my_err3)))
            .service(web::resource("my_err4")  // 统一错误处理使用 map_err, 隐藏真正的错误到给用户展示  
                .route(web::get().to(my_err4)))

        // 资源模式语法
        // 在路由中使用模式可以使用斜杠开头. 如果模式不是以斜杠开头, 在匹配的时候会在前面加一个隐式的斜杠 {foo}/bar/baz 和 /{foo}/bar/baz 是等价的
        // {foo}/bar/baz 中, {}标识符意味着“直到下一个斜杠符前,可以接受任何的字符”, 并将其用作 HttpRequest.match_info()对象中的名称
        // 模式中的替换标记与正则表达式 [^{}/]+ 相匹配
        // 匹配的信息是一个 Params 对象,代表基于路由模式从URL中动态提取出来的部分 可以作为 request.match_info() 中的信息来使用
        // 对 foo/{baz}/{bar} 匹配 定义一个字段(foo)和两个标记(baz,and bar)
            // foo/1/2         -> Params {'baz': '1', 'bar':'2'}  可以匹配
            // foo/abc/def     -> Params {'baz': 'abc', 'bar':'def'} 可以匹配
            // foo/1/2/  !!! 路径根本无法匹配, 多了一个斜杠 而我们定义的是 foo/{baz}/{bar}

        // 替换标记也可以使用正则表达式 扩展的替换标记语法. 在大括号内,替换标记名后必须跟一个冒号, 然后才是正则表达式  注意反斜杠 \ 属于转义字符 需要使用 \\, 或者使用 r#
        // 正则匹配 foo/{bar}/{tail:.*} 使用正则匹配后如下
            // foo/1/2/                -> Params: { 'bar': '1', 'tail':'2' }
            // foo/abc/def/a/b/c       -> Params: { 'bar':u'abc', 'tail':'def/a/b/c' }

            // 匹配任意数字
            .service(web::resource(r#"/number/{num:\d+}"#)
                .route(web::get().to(|num:web::Path<i32>|{
                    println!("使用正则语法匹配到了一些数值: {:?}",num);
                    HttpResponse::Ok().body(format!("{:?}",num))
                })))

        // scope 范围匹配 使用 scope 来建立一个 "空间"
            .service(
                web::scope("/space1").service(
                    web::scope("/say").service(
                        web::resource("/hi")  // 访问路径 http://127.0.0.1:8080/space1/say/hi
                            .route(web::get().to(||HttpResponse::Ok().body("get_hi")))
                            .route(web::post().to(||HttpResponse::Ok().body("post_hi")))
                    )
                )
            )
            .service(
                web::scope("/space2/{flag}")  // 访问路径 http://127.0.0.1:8080/{space2}/hello
                    .service(web::resource("/hello").to(|req:web::HttpRequest|{
                        println!("req.match_info: {:?}",req.match_info());  // 使用 match_info 获取一下呢
                        HttpResponse::Ok().body("hello")})
                    )
            )

            // 匹配信息
            // 所有表示路径匹配段的值都可以在 HttpRequest::match_info() 中获取
            .service(web::resource("/match/{flag}/eat")
                .route(web::get().to(|flag:web::Path<String>,req:HttpRequest|{
                    println!("匹配的flag: {}", flag);
                    println!("匹配的一些信息: {:?}", req.match_info());
                    HttpResponse::Ok().body("eat")
                })))

            // 生成资源URL 并进行重定向
            .service(web::resource("/url_for")
                .route(web::get().to(url_for)))
            .service(web::resource("/res/{flag}")
                .name("res_name")  // 这里设置资源名, 下面就可以使用了
                .route(web::get().to(res)))

            // 直接重定向  暂时没找到现有的api, 好像没有, 还是需要手动设置请求体 以及状态码和跳转的url 比如上面 HttpResponse::TemporaryRedirect()和 HttpResponse::PermanentRedirect() 都返回了 ResponseBuild
            // .service(web::resource("into_baidu")
            //     .route(web::get().to()))


            // Requests 内容解码 如果请求头中包含一个 Content-Encoding 头, 请求的payload会根据header中的值来解压缩. 不支持多重解码器:Content-Encoding:br, gzip
            // 支持的解码器: Brotli, Chunked, Compress, Gzip, Deflate, Identity, Trailers, EncodingExt


            // Response 响应解码
            // Actix-web 能使用 Compress middleware中间件, 默认 ContentEncoding::Auto被启用 根据请求头中的 Accept-Encoding 来决定压缩 payloads.
            // 可以手动指定想要压缩的协议 支持: Brotli, gzip, Deflate, Identity
            // 在视图中,可以对单独的函数进行 指定压缩方式: ContentEncoding::Identity 可用来禁用压缩

            // .wrap(middleware::Compress::default())  // 中间件 开启默认压缩 (ContentEncoding::Auto)
            .wrap(middleware::Compress::new(actix_web::http::ContentEncoding::Br))  // 中间件 添加一个 br 压缩中间件
            .service(web::resource("zip")
                .route(web::get().to(||{
                    let s = (0..4096).map(|x|"a").collect::<String>();
                    HttpResponse::Ok()
                        // .encoding(actix_web::http::ContentEncoding::Gzip)    // 指定压缩方式
                        .encoding(actix_web::http::ContentEncoding::Identity)    // 禁用压缩
                        .body(s)
                })))



            // 文件服务
            .service(Files::new("/images", "./static/images/")  // 渲染一个文件列表(注意此service 的注册顺序需要在 / 的上面, 不然会从绑定的根资源路径去找)
                .show_files_listing())
            .service(Files::new("/", "./static/root/")  // 绑定 根路径, 并且绑定一个访问 根资源的 index 文件
                .index_file("index.html"))

            // 重写 NotFound 404
            .default_service(
                web::route().to(||HttpResponse::NotFound().body("内容不见了哦"))
            )

    })
        .bind("127.0.0.1:8080")?
        .workers(20)  // 启动 20 个 worker线程

        // .keep_alive(None)  // 全局关闭 keep-alive, 传递 Option<usize,None> 或者 usize 都是可以的因为实现了 impl From<usize> for KeepAlive 和 impl From<Option<usize>> for KeepAlive
        .keep_alive(70)  // 全局开启keep-alive, keep alive 在 HTTP/1.0是默认 关闭 的, 在 HTTP/1.1 和 HTTP/2.0 是默认开启的
        // 在视图中 链接类型可以使用 HttpResponseBuilder::connection_type() 方法来改变: HttpResponse::Ok().connection_type(http::ConnectionType::Close).force_close().finish()

        // .disable_signals()  // 禁用信号处理
        .shutdown_timeout(30)  // 在接收到停机信号后,worker线程有一定的时间来完成请求. 超过时间后的所有worker都会被强制drop掉. 默认的shutdown 超时时间设置为30秒
        // 常用信号
        // SIGINT - 强制关闭worker  kill -2
        // SIGTERM - 优雅关闭worker  kill -15
        // SIGQUIT - 强制关闭worker  kill -3

        .run()
        .await

}

#[cfg(test)]
mod tests {
    use super::*;
    use actix_web::dev::Service;
    use actix_web::{http, test, web, App, Error};

    #[actix_rt::test]
    async fn test_index() -> Result<(), Error> {
        let app = App::new().route("/", web::get().to(index));
        let mut app = test::init_service(app).await;

        let req = test::TestRequest::get().uri("/").to_request();
        let resp = app.call(req).await.unwrap();

        assert_eq!(resp.status(), http::StatusCode::OK);

        let response_body = match resp.response().body().as_ref() {
            Some(actix_web::body::Body::Bytes(bytes)) => bytes,
            _ => panic!("Response error"),
        };

        assert_eq!(response_body, r##"Hello world!"##);

        Ok(())
    }
}

Cargo.toml

[package]
name = "hello-world"
version = "2.0.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
edition = "2018"

[dependencies]
actix-web = "3"
env_logger = "0.7"

serde = "1.0.116"  # 序列化用的
serde_json = "1.0"

cookie = "0.14.2"  # cookie先关
futures = "0.3.5"
actix-files = "0.3.0"  # actix 文件相关

derive_more = "0.99.10"



[dev-dependencies]
actix-rt = "1"


[features]
#secure-cookies = ["actix-http/secure-cookies"]