跳转到正文
zeno's blog
返回

Rust Web:Actix-web HTTP Server 指南

专题: Rust Web

Table of contents

Open Table of contents

Cargo.toml

[dependencies]
actix-web = "4"
actix-rt = "2"
actix-cors = "0.7"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
env_logger = "0.11"

1. 最小 Server

use actix_web::{get, App, HttpServer, Responder};

#[get("/")]
async fn hello() -> impl Responder {
    "Hello, World!"
}

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

2. 路由

宏方式(推荐)

use actix_web::{get, post, put, delete, web, HttpResponse, Responder};

#[get("/users")]
async fn list_users() -> impl Responder {
    HttpResponse::Ok().json(serde_json::json!([]))
}

#[post("/users")]
async fn create_user(body: web::Json<CreateUser>) -> impl Responder {
    HttpResponse::Created().json(serde_json::json!({"id": 1}))
}

#[get("/users/{id}")]
async fn get_user(path: web::Path<u32>) -> impl Responder {
    let id = path.into_inner();
    HttpResponse::Ok().json(serde_json::json!({"id": id}))
}

#[put("/users/{id}")]
async fn update_user(
    path: web::Path<u32>,
    body: web::Json<UpdateUser>,
) -> impl Responder {
    HttpResponse::Ok().json(serde_json::json!({"updated": true}))
}

#[delete("/users/{id}")]
async fn delete_user(path: web::Path<u32>) -> impl Responder {
    HttpResponse::NoContent().finish()
}

// 注册
App::new()
    .service(list_users)
    .service(create_user)
    .service(get_user)
    .service(update_user)
    .service(delete_user)

手动方式

App::new()
    .route("/health", web::get().to(|| async { "OK" }))
    .route("/users", web::get().to(list_users))
    .route("/users", web::post().to(create_user))

分组路由(scope)

use actix_web::web;

App::new()
    .service(
        web::scope("/api/v1")
            .service(list_users)
            .service(create_user)
            .service(get_user)
    )
    .service(
        web::scope("/admin")
            .route("/dashboard", web::get().to(dashboard))
    )
// 结果: /api/v1/users, /api/v1/users/{id}, /admin/dashboard

3. Extractor

Path 参数

use actix_web::web;

#[get("/users/{id}")]
async fn get_user(path: web::Path<u32>) -> impl Responder {
    let id = path.into_inner();
    format!("User {}", id)
}

// 多个路径参数
#[get("/users/{user_id}/items/{item_id}")]
async fn get_item(path: web::Path<(u32, u32)>) -> impl Responder {
    let (user_id, item_id) = path.into_inner();
    format!("User {} Item {}", user_id, item_id)
}

// 解构到结构体
#[derive(Deserialize)]
struct ItemPath {
    user_id: u32,
    item_id: u32,
}

#[get("/users/{user_id}/items/{item_id}")]
async fn get_item_v2(path: web::Path<ItemPath>) -> impl Responder {
    format!("User {} Item {}", path.user_id, path.item_id)
}

Query 参数

#[derive(Deserialize)]
struct Pagination {
    page: Option<u32>,
    per_page: Option<u32>,
}

#[get("/users")]
async fn list_users(query: web::Query<Pagination>) -> impl Responder {
    let page = query.page.unwrap_or(1);
    format!("Page {}", page)
}
// GET /users?page=2&per_page=10

JSON Body

use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct CreateUser {
    username: String,
    email: String,
}

#[derive(Serialize)]
struct User {
    id: u64,
    username: String,
    email: String,
}

#[post("/users")]
async fn create_user(body: web::Json<CreateUser>) -> impl Responder {
    let user = User {
        id: 1,
        username: body.username.clone(),
        email: body.email.clone(),
    };
    HttpResponse::Created().json(user)
}

Header 提取

use actix_web::HttpRequest;

#[get("/info")]
async fn info(req: HttpRequest) -> impl Responder {
    let ua = req
        .headers()
        .get("user-agent")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("unknown");
    format!("UA: {}", ua)
}

4. 共享状态(web::Data)

use actix_web::web;
use std::sync::Mutex;

struct AppState {
    db: DbPool,
    counter: Mutex<u32>,
}

#[get("/count")]
async fn get_count(data: web::Data<AppState>) -> impl Responder {
    let count = data.counter.lock().unwrap();
    format!("Count: {}", count)
}

#[post("/increment")]
async fn increment(data: web::Data<AppState>) -> impl Responder {
    let mut count = data.counter.lock().unwrap();
    *count += 1;
    HttpResponse::Ok().json(serde_json::json!({"count": *count}))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // 在闭包外创建,web::Data 内部是 Arc
    let state = web::Data::new(AppState {
        db: DbPool::new(),
        counter: Mutex::new(0),
    });

    HttpServer::new(move || {
        App::new()
            .app_data(state.clone()) // clone 的是 Arc,不是数据本身
            .service(get_count)
            .service(increment)
    })
    .bind(("0.0.0.0", 8080))?
    .run()
    .await
}

5. 响应

use actix_web::HttpResponse;

// 纯文本
async fn text() -> impl Responder {
    "hello"
}

// JSON
async fn json() -> impl Responder {
    HttpResponse::Ok().json(serde_json::json!({"status": "ok"}))
}

// 自定义响应
async fn custom() -> impl Responder {
    HttpResponse::Ok()
        .insert_header(("X-Custom", "value"))
        .content_type("text/plain")
        .body("custom body")
}

// 无 body
async fn no_content() -> impl Responder {
    HttpResponse::NoContent().finish()
}

6. 错误处理

自定义错误类型

use actix_web::{HttpResponse, ResponseError};
use derive_more::{Display, Error};

#[derive(Debug, Display, Error)]
enum AppError {
    #[display("Not found: {}", message)]
    NotFound { message: String },

    #[display("Bad request: {}", message)]
    BadRequest { message: String },

    #[display("Internal error")]
    Internal,
}

impl ResponseError for AppError {
    fn error_response(&self) -> HttpResponse {
        match self {
            AppError::NotFound { .. } => {
                HttpResponse::NotFound().json(serde_json::json!({"error": self.to_string()}))
            }
            AppError::BadRequest { .. } => {
                HttpResponse::BadRequest().json(serde_json::json!({"error": self.to_string()}))
            }
            AppError::Internal => {
                HttpResponse::InternalServerError()
                    .json(serde_json::json!({"error": "Internal server error"}))
            }
        }
    }
}

#[get("/users/{id}")]
async fn get_user(path: web::Path<u32>) -> Result<HttpResponse, AppError> {
    let id = path.into_inner();
    if id == 0 {
        return Err(AppError::BadRequest { message: "ID cannot be 0".into() });
    }
    if id > 100 {
        return Err(AppError::NotFound { message: format!("User {}", id) });
    }
    Ok(HttpResponse::Ok().json(serde_json::json!({"id": id, "name": "Alice"})))
}

JSON 提取器错误配置

let json_config = web::JsonConfig::default()
    .limit(4096) // body 大小限制 4KB
    .error_handler(|err, _req| {
        actix_web::error::InternalError::from_response(
            err,
            HttpResponse::BadRequest().json(serde_json::json!({"error": "Invalid JSON"})),
        ).into()
    });

App::new().app_data(json_config)

7. 中间件

内置中间件

use actix_web::middleware;
use actix_cors::Cors;

App::new()
    // 请求日志
    .wrap(middleware::Logger::default())
    // 响应压缩
    .wrap(middleware::Compress::default())
    // 默认 header
    .wrap(middleware::DefaultHeaders::new().add(("X-Version", "1.0")))
    // URL 路径规范化(去除尾部斜杠)
    .wrap(middleware::NormalizePath::trim())
    // CORS
    .wrap(
        Cors::default()
            .allow_any_origin()
            .allow_any_method()
            .allow_any_header()
    )

自定义中间件(from_fn)

use actix_web::{
    dev::ServiceRequest,
    body::MessageBody,
    middleware::{self, Next},
    HttpResponse, Error,
};

async fn auth_middleware(
    req: ServiceRequest,
    next: Next<impl MessageBody>,
) -> Result<actix_web::dev::ServiceResponse<impl MessageBody>, Error> {
    let has_auth = req.headers().get("Authorization").is_some();

    if has_auth {
        next.call(req).await
    } else {
        let response = HttpResponse::Unauthorized().body("Missing auth");
        Ok(req.into_response(response).map_into_right_body())
    }
}

// 应用到特定 scope
App::new()
    .service(
        web::scope("/api")
            .wrap(middleware::from_fn(auth_middleware))
            .service(list_users)
    )

8. 文件上传(Multipart)

# Cargo.toml
actix-multipart = "0.7"
use actix_multipart::form::{tempfile::TempFile, MultipartForm};

#[derive(MultipartForm)]
struct UploadForm {
    #[multipart(limit = "50MB")]
    file: TempFile,
}

#[post("/upload")]
async fn upload(MultipartForm(form): MultipartForm<UploadForm>) -> impl Responder {
    let size = form.file.size;
    let filename = form.file.file_name.unwrap_or_default();
    format!("Uploaded {} ({} bytes)", filename, size)
}

9. 静态文件

# Cargo.toml
actix-files = "0.6"
use actix_files::Files;

App::new()
    .service(Files::new("/static", "./static").prefer_utf8(true))

10. HttpServer 配置

HttpServer::new(|| App::new().service(hello))
    .bind(("0.0.0.0", 8080))?
    .workers(4)                    // worker 线程数(默认 = CPU 核数)
    .keep_alive(Duration::from_secs(75))
    .shutdown_timeout(30)          // 优雅关闭超时(秒)
    .run()
    .await

11. 完整示例

use actix_web::{get, post, web, App, HttpServer, HttpResponse, Responder, middleware};
use actix_cors::Cors;
use serde::{Deserialize, Serialize};
use std::sync::Mutex;

struct AppState {
    db: DbPool,
}

#[derive(Deserialize)]
struct Pagination {
    page: Option<u32>,
    limit: Option<u32>,
}

#[derive(Serialize)]
struct Player {
    id: u32,
    name: String,
    level: u32,
}

#[derive(Deserialize)]
struct CreatePlayer {
    name: String,
}

#[get("/players")]
async fn list_players(
    data: web::Data<AppState>,
    query: web::Query<Pagination>,
) -> impl Responder {
    HttpResponse::Ok().json(Vec::<Player>::new())
}

#[get("/players/{id}")]
async fn get_player(
    data: web::Data<AppState>,
    path: web::Path<u32>,
) -> impl Responder {
    let id = path.into_inner();
    HttpResponse::Ok().json(Player { id, name: "Bear".into(), level: 15 })
}

#[post("/players")]
async fn create_player(
    data: web::Data<AppState>,
    body: web::Json<CreatePlayer>,
) -> impl Responder {
    let player = Player { id: 1, name: body.name.clone(), level: 1 };
    HttpResponse::Created().json(player)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    env_logger::init();

    let state = web::Data::new(AppState { db: DbPool::new() });

    HttpServer::new(move || {
        App::new()
            .app_data(state.clone())
            .wrap(middleware::Logger::default())
            .wrap(Cors::default().allow_any_origin().allow_any_method().allow_any_header())
            .service(list_players)
            .service(get_player)
            .service(create_player)
    })
    .bind(("0.0.0.0", 8080))?
    .run()
    .await
}

12. 与 axum 对比

actix-webaxum
路由风格#[get("/path")]函数式 route("/path", get(handler))
状态web::Data<T>(内部 Arc)State<T>(需自己 Clone)
中间件自有 trait + from_fntower 生态(通用)
错误处理ResponseError traitIntoResponse trait
Worker 模型多 worker 线程,每个有独立 App 实例单 App 实例,靠 tokio 调度
配置HttpServer 链式配置(workers/keep_alive)外部 TcpListener + axum::serve
成熟度更老,API 更稳定更新,tokio 官方出品

分享这篇文章:

上一篇
Rust Web:Axum Extractor Rejection-Handler 执行前的守门员
下一篇
分布式基础(四):容灾-容错与灾难恢复