MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Rust构建简单的Web服务器

2024-10-257.5k 阅读

Rust 构建简单 Web 服务器基础准备

在使用 Rust 构建 Web 服务器之前,我们需要确保开发环境已经准备妥当。首先,要安装 Rust 编程语言的开发工具链。可以通过官方提供的 rustup 工具进行安装,在大多数操作系统上,运行以下命令即可完成安装:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

安装完成后,通过 rustc --version 命令可以查看 Rust 编译器的版本,确保安装成功。

同时,我们还需要一个合适的 Rust Web 框架来简化服务器的开发。在 Rust 生态系统中,有许多优秀的 Web 框架可供选择,如 actix-webrocket 等。这里我们以 actix-web 为例来构建简单的 Web 服务器。actix-web 是一个基于 Actix 异步框架的高性能 Rust Web 框架,具有轻量级、易于使用等特点。

在项目的 Cargo.toml 文件中添加 actix-web 依赖:

[dependencies]
actix-web = "4.0.0"

添加依赖后,运行 cargo build 命令,Cargo 会自动下载并构建所需的依赖库。

创建基本的 Web 服务器

有了开发环境和框架依赖,我们就可以开始编写代码来创建一个基本的 Web 服务器了。首先,在 src/main.rs 文件中编写以下代码:

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

// 定义一个处理函数
#[get("/")]
async fn index() -> impl Responder {
    HttpResponse::Ok().body("Hello, World!")
}

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

在上述代码中:

  1. 我们首先导入了 actix-web 框架中所需的模块。get 宏用于定义一个处理 HTTP GET 请求的路由;web 模块提供了一些用于处理请求和响应的工具;App 用于构建应用程序;HttpResponse 用于生成 HTTP 响应;HttpServer 用于启动 HTTP 服务器;Responder 是一个 trait,实现了该 trait 的类型可以作为 HTTP 响应返回。
  2. 定义了一个名为 index 的异步函数,它使用 #[get("/")] 宏来指定该函数处理根路径 "/" 的 GET 请求。函数返回一个 HttpResponse,这里返回的是一个状态码为 200(OK),正文内容为 "Hello, World!" 的响应。
  3. main 函数中,使用 HttpServer::new 创建一个新的 HTTP 服务器实例。在闭包中,通过 App::new 创建一个新的应用程序,并使用 .service(index)index 函数注册为一个服务,也就是将根路径 "/" 的 GET 请求与 index 函数关联起来。
  4. 然后使用 .bind(("127.0.0.1", 8080)) 将服务器绑定到本地地址 127.0.0.1 的 8080 端口上。最后通过 .run().await 启动服务器并等待其运行结束。

运行 cargo run 命令启动服务器,在浏览器中访问 http://127.0.0.1:8080,就可以看到 "Hello, World!" 的响应。

处理不同类型的请求

除了 GET 请求,Web 服务器通常还需要处理其他类型的请求,如 POST、PUT、DELETE 等。actix-web 框架提供了相应的宏来处理不同类型的请求。

处理 POST 请求

假设我们要处理一个接收 JSON 数据的 POST 请求。首先,在 Cargo.toml 文件中添加 serdeserde_json 依赖,用于处理 JSON 数据的序列化和反序列化:

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

然后,修改 src/main.rs 文件如下:

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

// 定义 JSON 数据结构
#[derive(Deserialize, Serialize)]
struct User {
    username: String,
    password: String,
}

// 处理 POST 请求的函数
#[post("/login")]
async fn login(user: web::Json<User>) -> impl Responder {
    if user.username == "admin" && user.password == "password" {
        HttpResponse::Ok().json(json!({"message": "Login successful"}))
    } else {
        HttpResponse::Unauthorized().json(json!({"message": "Login failed"}))
    }
}

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

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

在上述代码中:

  1. 导入了 serdeserde_json 相关的模块,并使用 derive 特性为 User 结构体自动生成 JSON 序列化和反序列化的实现。
  2. 定义了 User 结构体,用于表示登录请求中的用户名和密码。
  3. 编写了 login 函数,使用 #[post("/login")] 宏指定该函数处理 /login 路径的 POST 请求。函数参数 user: web::Json<User> 表示期望接收一个 JSON 格式的 User 数据。在函数内部,根据用户名和密码进行简单的验证,并返回相应的 JSON 响应。

处理 PUT 和 DELETE 请求

以处理 /users/{id} 路径的 PUT 和 DELETE 请求为例,假设我们有一个简单的内存数据库来存储用户信息,代码如下:

use actix_web::{delete, get, put, web, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};

// 定义 JSON 数据结构
#[derive(Deserialize, Serialize)]
struct User {
    id: u32,
    username: String,
    email: String,
}

// 模拟内存数据库
static mut USERS: Vec<User> = Vec::new();

// 处理 PUT 请求,更新用户信息
#[put("/users/{id}")]
async fn update_user(path: web::Path<u32>, user: web::Json<User>) -> impl Responder {
    let id = *path;
    unsafe {
        if let Some(index) = USERS.iter().position(|u| u.id == id) {
            USERS[index] = user.into_inner();
            HttpResponse::Ok().json(json!({"message": "User updated successfully"}))
        } else {
            HttpResponse::NotFound().json(json!({"message": "User not found"}))
        }
    }
}

// 处理 DELETE 请求,删除用户
#[delete("/users/{id}")]
async fn delete_user(path: web::Path<u32>) -> impl Responder {
    let id = *path;
    unsafe {
        if let Some(index) = USERS.iter().position(|u| u.id == id) {
            USERS.remove(index);
            HttpResponse::Ok().json(json!({"message": "User deleted successfully"}))
        } else {
            HttpResponse::NotFound().json(json!({"message": "User not found"}))
        }
    }
}

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

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

在上述代码中:

  1. 定义了 User 结构体,增加了 idemail 字段。
  2. 使用 static mut 定义了一个可变的静态变量 USERS 来模拟内存数据库。这里使用 unsafe 块是因为直接修改静态变量是不安全的操作。
  3. update_user 函数处理 /users/{id} 路径的 PUT 请求,通过 web::Path<u32> 获取路径中的 id,并根据 idUSERS 中查找并更新用户信息。
  4. delete_user 函数处理 /users/{id} 路径的 DELETE 请求,同样通过路径中的 idUSERS 中查找并删除用户信息。

处理请求参数

在 Web 开发中,经常需要从请求中获取参数。请求参数可以分为路径参数、查询参数和表单参数等。actix-web 框架提供了方便的方式来处理这些参数。

路径参数

我们前面已经在处理 PUT 和 DELETE 请求时使用过路径参数。例如,在 update_userdelete_user 函数中:

#[put("/users/{id}")]
async fn update_user(path: web::Path<u32>, user: web::Json<User>) -> impl Responder {
    let id = *path;
    //...
}

#[delete("/users/{id}")]
async fn delete_user(path: web::Path<u32>) -> impl Responder {
    let id = *path;
    //...
}

通过 web::Path<u32> 来获取路径中的 id 参数,并且指定参数的类型为 u32。如果路径中的参数类型不匹配,actix-web 会返回一个 404 错误。

查询参数

假设我们有一个搜索用户的功能,通过查询参数来指定搜索关键词。代码如下:

use actix_web::{get, web, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};

// 定义 JSON 数据结构
#[derive(Deserialize, Serialize)]
struct User {
    id: u32,
    username: String,
    email: String,
}

// 模拟内存数据库
static mut USERS: Vec<User> = Vec::new();

// 处理查询参数,搜索用户
#[get("/search")]
async fn search_users(query: web::Query<SearchParams>) -> impl Responder {
    let SearchParams { keyword } = query.into_inner();
    let results = unsafe {
        USERS.iter()
           .filter(|user| user.username.contains(&keyword) || user.email.contains(&keyword))
           .cloned()
           .collect::<Vec<User>>()
    };
    HttpResponse::Ok().json(results)
}

// 查询参数结构体
#[derive(Deserialize)]
struct SearchParams {
    keyword: String,
}

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

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

在上述代码中:

  1. 定义了 SearchParams 结构体,用于表示查询参数。这里只有一个 keyword 字段。
  2. search_users 函数使用 web::Query<SearchParams> 来获取查询参数,并将其转换为 SearchParams 结构体。然后在内存数据库中根据关键词搜索用户,并返回搜索结果。

表单参数

处理表单参数与处理 JSON 数据类似,只不过需要使用 Form 类型。假设我们有一个注册用户的表单,代码如下:

use actix_web::{post, web, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};

// 定义 JSON 数据结构
#[derive(Deserialize, Serialize)]
struct User {
    id: u32,
    username: String,
    email: String,
}

// 模拟内存数据库
static mut USERS: Vec<User> = Vec::new();

// 处理表单参数,注册用户
#[post("/register")]
async fn register_user(form: web::Form<User>) -> impl Responder {
    let user = form.into_inner();
    let new_id = unsafe { USERS.len() as u32 + 1 };
    let new_user = User { id: new_id, username: user.username, email: user.email };
    unsafe {
        USERS.push(new_user);
    }
    HttpResponse::Ok().json(json!({"message": "User registered successfully"}))
}

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

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

在上述代码中:

  1. register_user 函数使用 web::Form<User> 来获取表单数据,并将其转换为 User 结构体。然后为新用户生成一个 id,将新用户添加到内存数据库中,并返回注册成功的响应。

中间件的使用

中间件是 Web 开发中非常重要的概念,它可以在请求到达处理函数之前或响应返回给客户端之前执行一些通用的逻辑,如日志记录、身份验证、错误处理等。actix-web 框架提供了丰富的中间件支持。

日志中间件

首先,在 Cargo.toml 文件中添加 actix-web 的日志中间件依赖:

[dependencies]
actix-web = { version = "4.0.0", features = ["logger"] }

然后,修改 src/main.rs 文件如下:

use actix_web::{get, web, App, HttpResponse, HttpServer, Responder};
use actix_web::middleware::Logger;

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

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

在上述代码中,通过 App::new().wrap(Logger::default()) 将日志中间件添加到应用程序中。这样,每次有请求到达服务器时,日志中间件都会记录请求的相关信息,如请求方法、路径、状态码等。

身份验证中间件

假设我们有一个简单的基于 API 密钥的身份验证中间件。首先定义中间件结构体和实现:

use actix_web::{dev::Service, dev::ServiceRequest, dev::ServiceResponse, http::header, Error, HttpResponse};
use futures_util::future::{ok, Ready};

struct ApiKeyAuth {
    api_key: String,
}

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

    fn poll_ready(&mut self, _cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
        std::task::Poll::Ready(Ok(()))
    }

    fn call(&mut self, req: ServiceRequest) -> Self::Future {
        let headers = req.headers();
        if let Some(api_key) = headers.get(header::AUTHORIZATION) {
            if api_key.to_str().unwrap() == &self.api_key {
                ok(req.call())
            } else {
                ok(ServiceResponse::new(
                    req.into_request(),
                    HttpResponse::Unauthorized().finish(),
                ))
            }
        } else {
            ok(ServiceResponse::new(
                req.into_request(),
                HttpResponse::Unauthorized().finish(),
            ))
        }
    }
}

然后在应用程序中使用这个中间件:

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

#[get("/protected")]
async fn protected() -> impl Responder {
    HttpResponse::Ok().body("This is a protected route")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let api_key_auth = ApiKeyAuth {
        api_key: "my_secret_api_key".to_string(),
    };
    HttpServer::new(|| {
        App::new()
           .wrap(api_key_auth)
           .service(protected)
    })
   .bind(("127.0.0.1", 8080))?
   .run()
   .await
}

在上述代码中:

  1. 定义了 ApiKeyAuth 结构体,它包含一个 api_key 字段,用于存储有效的 API 密钥。
  2. 实现了 Service trait,在 call 方法中,检查请求头中的 Authorization 字段是否与存储的 API 密钥匹配。如果匹配,则继续处理请求;否则,返回 401 未经授权的响应。
  3. main 函数中,创建了 ApiKeyAuth 实例,并通过 App::new().wrap(api_key_auth) 将其添加到应用程序中,保护了 /protected 路由。

模板引擎的集成

为了生成动态的 HTML 页面,我们可以集成模板引擎。在 Rust 生态系统中,tera 是一个流行的模板引擎。

首先,在 Cargo.toml 文件中添加 tera 依赖:

[dependencies]
tera = "1.16.0"

然后,创建一个 templates 目录,并在其中创建一个 index.html 文件:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello, Rust Web Server</title>
</head>
<body>
    <h1>Hello, {{ name }}!</h1>
</body>
</html>

修改 src/main.rs 文件如下:

use actix_web::{get, web, App, HttpResponse, HttpServer, Responder};
use tera::Tera;

#[get("/")]
async fn index() -> impl Responder {
    let tera = match Tera::new("templates/**/*") {
        Ok(t) => t,
        Err(e) => {
            println!("Parsing error: {}", e);
            return HttpResponse::InternalServerError().body("Template parsing error");
        }
    };
    let context = tera::Context::new();
    context.insert("name", &"World");
    match tera.render("index.html", &context) {
        Ok(html) => HttpResponse::Ok().content_type("text/html").body(html),
        Err(e) => HttpResponse::InternalServerError().body(format!("Rendering error: {}", e)),
    }
}

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

在上述代码中:

  1. 导入了 tera 相关的模块。
  2. index 函数中,首先使用 Tera::new("templates/**/*") 加载 templates 目录下的所有模板文件。如果加载失败,返回 500 内部服务器错误。
  3. 创建一个 Context 对象,并插入一个名为 name 的变量,值为 "World"。
  4. 使用 tera.render("index.html", &context) 渲染 index.html 模板,并根据渲染结果返回相应的 HTTP 响应。如果渲染成功,返回包含渲染后 HTML 内容的 200 响应;如果渲染失败,返回 500 错误响应。

通过以上步骤,我们已经使用 Rust 和 actix-web 框架构建了一个功能较为丰富的简单 Web 服务器,涵盖了请求处理、参数处理、中间件使用以及模板引擎集成等方面的内容。在实际开发中,可以根据具体需求进一步扩展和优化这个服务器。