使用 Rust + Axum 构建灵活的 API 模拟服务器

使用 Rust + Axum 构建灵活的 API 模拟服务器

在日常开发中,我们经常需要模拟后端 API 接口来测试前端应用、调试第三方集成,或者在前后端并行开发时提供临时的接口服务。传统的做法可能是使用 Postman、Mockoon 等工具,但今天我要介绍一个更轻量、更灵活的解决方案------使用 Rust 和 Axum 框架构建一个可配置的 API 模拟服务器。

一、完整代码

1. 目录结构

rust 复制代码
api-server
├── src
│   └── main.rs
├── Cargo.lock
├── Cargo.toml
└── config.toml

2. Cargo.toml

toml 复制代码
[package]
name = "rust_api_simulator"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.8"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-appender = "0.2"
http = "1.0"
local-ip-address = "0.6"


[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61.2", features = ["Win32_System_Console"] }

3. main.rs

rust 复制代码
use axum::{
    body::Bytes,
    extract::{ConnectInfo, Request, State},
    http::{header, HeaderValue, StatusCode},
    response::IntoResponse,
    routing::any,
    Router,
};
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::net::SocketAddr;
use std::sync::Arc;
use tracing::{error, info};
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt};

// --- 配置模型 ---

#[derive(Deserialize, Clone)]
struct InterfaceConfig {
    path: String,
    response_body: String,
    content_type: String,
}

#[derive(Deserialize)]
struct AppConfig {
    port: u16,
    interfaces: Vec<InterfaceConfig>,
}

// 共享状态:映射路径到其特定的配置
struct AppState {
    responses: HashMap<String, InterfaceConfig>,
}

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

    // 强制设置 Windows 控制台输出为 UTF-8
    #[cfg(windows)]
    unsafe {
        use windows_sys::Win32::System::Console::{SetConsoleOutputCP, GetConsoleOutputCP};
        SetConsoleOutputCP(65001);
    }

    // 1. 初始化日志系统 (每日滚动日志文件 + 控制台实时输出)
    let file_appender = tracing_appender::rolling::daily("logs", "api_simulator.log");
    let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);

    // 修改为(显式关闭 ANSI 开关)
    tracing_subscriber::registry()
        .with(fmt::layer().with_ansi(false).with_writer(std::io::stdout)) // 增加 .with_ansi(false)
        .with(fmt::layer().with_ansi(false).with_writer(non_blocking))   // 文件日志通常也要关闭颜色
        .init();

    // 2. 加载并解析配置文件
    let config_content = fs::read_to_string("config.toml")
        .expect("错误: 无法在根目录下找到 config.toml 文件");
    let config: AppConfig = toml::from_str(&config_content)
        .expect("错误: config.toml 格式不正确");

    let mut responses_map = HashMap::new();
    let mut app = Router::new();

    // 3. 动态注册路由:根据配置文件的接口数量循环注册
    for iface in config.interfaces {
        info!("成功挂载接口: [{}] -> 返回类型: {}", iface.path, iface.content_type);
        responses_map.insert(iface.path.clone(), iface.clone());

        // 任何 HTTP 方法 (GET/POST/PUT等) 都会进入同一个 handler
        app = app.route(&iface.path, any(handle_request));
    }

    // 设置共享状态
    let state = Arc::new(AppState { responses: responses_map });
    let app = app.with_state(state);

    // 3. 确定监听地址与获取局域网 IP
    // 监听 0.0.0.0 是内网访问的关键,表示接受任何网卡的连接
    let addr = SocketAddr::from(([0, 0, 0, 0], config.port));

    // 自动获取本机在内网的实际 IP 地址
    let my_local_ip = local_ip_address::local_ip()
        .map(|ip| ip.to_string())
        .unwrap_or_else(|_| "127.0.0.1".to_string());

    info!("模拟服务器已启动!");
    info!("------------------------------------------");
    info!("本地访问: http://localhost:{}", config.port);
    info!("内网访问: http://{}:{}", my_local_ip, config.port);
    info!("------------------------------------------");

    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();

    // 启用 ConnectInfo 以支持获取客户端 IP
    axum::serve(
        listener,
        app.into_make_service_with_connect_info::<SocketAddr>(),
    )
        .await
        .unwrap();
}

// --- 通用请求处理器 ---

async fn handle_request(
    ConnectInfo(addr): ConnectInfo<SocketAddr>, // 提取客户端 IP
    State(state): State<Arc<AppState>>,         // 提取接口配置
    req: Request,                               // 提取请求内容
) -> impl IntoResponse {
    let method = req.method().clone();
    let uri = req.uri().clone();
    let path = uri.path().to_string();
    let headers = req.headers().clone();
    let client_ip = addr.ip().to_string();

    // 提取 Cookie 字段
    let cookie = headers
        .get(header::COOKIE)
        .and_then(|v| v.to_str().ok())
        .unwrap_or("无 Cookie 信息");

    // 提取并读取 Body 数据
    let body_bytes = match axum::body::to_bytes(req.into_body(), usize::MAX).await {
        Ok(bytes) => bytes,
        Err(e) => {
            error!("读取请求体失败: {}", e);
            Bytes::new()
        }
    };

    let raw_body = String::from_utf8_lossy(&body_bytes);

    // JSON 美化处理逻辑
    let display_body = if !raw_body.is_empty() {
        // 尝试解析为 JSON,如果成功则格式化,否则原样返回
        match serde_json::from_str::<serde_json::Value>(&raw_body) {
            Ok(json_val) => serde_json::to_string_pretty(&json_val).unwrap_or(raw_body.to_string()),
            Err(_) => raw_body.to_string(),
        }
    } else {
        "Empty Body".to_string()
    };

    // 记录格式化日志
    info!(
        "\n┌────── 收到请求 ──────┐\n\
         │ 来源 IP: {}\n\
         │ 访问路径: {}\n\
         │ HTTP 方法: {}\n\
         │ Cookie: {}\n\
         │ 所有请求头: {:?}\n\
         │ 内容详情 (Body):\n{}\n\
         └────────────────────┘",
        client_ip, path, method, cookie, headers, display_body
    );

    // 根据路径查找并返回预设的响应
    if let Some(iface_config) = state.responses.get(&path) {
        let mut response = iface_config.response_body.clone().into_response();

        // 设置自定义的 Content-Type 头
        if let Ok(content_type) = HeaderValue::from_str(&iface_config.content_type) {
            response.headers_mut().insert(header::CONTENT_TYPE, content_type);
        }

        response
    } else {
        (StatusCode::NOT_FOUND, "该接口未在配置文件中定义").into_response()
    }
}

4. config.toml

toml 复制代码
# 服务器监听端口
port = 3000

# 配置多个接口
[[interfaces]]
path = "/api/v1/user"
response_body = '{"status": "success", "message": "User Found", "id": 1001}'
content_type = "application/json"

[[interfaces]]
path = "/webhook/test"
response_body = "Warning: Webhook processed successfully."
content_type = "text/plain"

[[interfaces]]
path = "/v2/login/check"
response_body = '{"is_login": true, "user": "admin"}'
content_type = "application/json"

注:如果响应体是下面这样的多行json,需要用 ''' 来代替 '

json 复制代码
{
    "user_name":"123",
    "grade":"2"
}

二、项目概述

这个 API 模拟服务器具有以下核心特性:

  • 零代码配置:通过 TOML 配置文件定义接口,无需修改代码
  • 完整日志记录:记录所有请求的详细信息,包括 IP、方法、请求头、Cookie 和 Body
  • 内网访问支持:自动获取本机 IP,支持局域网内其他设备访问
  • 多方法支持:同一路径支持所有 HTTP 方法(GET、POST、PUT、DELETE 等)
  • JSON 美化:自动格式化 JSON 请求体,便于阅读
  • 灵活响应:支持自定义 Content-Type 和响应内容

三、核心代码解析

1. 配置模型设计

rust 复制代码
#[derive(Deserialize, Clone)]
struct InterfaceConfig {
    path: String,
    response_body: String,
    content_type: String,
}

#[derive(Deserialize)]
struct AppConfig {
    port: u16,
    interfaces: Vec<InterfaceConfig>,
}

使用 serde​ 和 toml crate 实现配置文件的解析,每个接口配置包含路径、响应体和内容类型。

2. 动态路由注册

rust 复制代码
for iface in config.interfaces {
    info!("成功挂载接口: [{}] -> 返回类型: {}", iface.path, iface.content_type);
    responses_map.insert(iface.path.clone(), iface.clone());
    app = app.route(&iface.path, any(handle_request));
}

根据配置文件动态注册路由。使用 any() 方法让同一个路径支持所有 HTTP 方法,极大简化了配置。

3. 请求处理器

rust 复制代码
async fn handle_request(
    ConnectInfo(addr): ConnectInfo<SocketAddr>, // 提取客户端 IP
    State(state): State<Arc<AppState>>,         // 提取接口配置
    req: Request,                               // 提取请求内容
) -> impl IntoResponse {
    let method = req.method().clone();
    let uri = req.uri().clone();
    let path = uri.path().to_string();
    let headers = req.headers().clone();
    let client_ip = addr.ip().to_string();

    // 提取 Cookie 字段
    let cookie = headers
        .get(header::COOKIE)
        .and_then(|v| v.to_str().ok())
        .unwrap_or("无 Cookie 信息");

    // 提取并读取 Body 数据
    let body_bytes = match axum::body::to_bytes(req.into_body(), usize::MAX).await {
        Ok(bytes) => bytes,
        Err(e) => {
            error!("读取请求体失败: {}", e);
            Bytes::new()
        }
    };

    let raw_body = String::from_utf8_lossy(&body_bytes);

    // JSON 美化处理逻辑
    let display_body = if !raw_body.is_empty() {
        // 尝试解析为 JSON,如果成功则格式化,否则原样返回
        match serde_json::from_str::<serde_json::Value>(&raw_body) {
            Ok(json_val) => serde_json::to_string_pretty(&json_val).unwrap_or(raw_body.to_string()),
            Err(_) => raw_body.to_string(),
        }
    } else {
        "Empty Body".to_string()
    };

    // 记录格式化日志
    info!(
        "\n┌────── 收到请求 ──────┐\n\
         │ 来源 IP: {}\n\
         │ 访问路径: {}\n\
         │ HTTP 方法: {}\n\
         │ Cookie: {}\n\
         │ 所有请求头: {:?}\n\
         │ 内容详情 (Body):\n{}\n\
         └────────────────────┘",
        client_ip, path, method, cookie, headers, display_body
    );

    // 根据路径查找并返回预设的响应
    if let Some(iface_config) = state.responses.get(&path) {
        let mut response = iface_config.response_body.clone().into_response();

        // 设置自定义的 Content-Type 头
        if let Ok(content_type) = HeaderValue::from_str(&iface_config.content_type) {
            response.headers_mut().insert(header::CONTENT_TYPE, content_type);
        }

        response
    } else {
        (StatusCode::NOT_FOUND, "该接口未在配置文件中定义").into_response()
    }
}

处理器提取了请求的所有关键信息,并对 JSON 格式的请求体进行美化处理,然后记录详细的日志。

4. 日志系统

rust 复制代码
let file_appender = tracing_appender::rolling::daily("logs", "api_simulator.log");
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);

tracing_subscriber::registry()
    .with(fmt::layer().with_ansi(false).with_writer(std::io::stdout))
    .with(fmt::layer().with_ansi(false).with_writer(non_blocking))
    .init();

使用 tracing​ 和 tracing-appender 实现了每日滚动的日志文件,同时支持控制台实时输出。

四、基本使用

  1. 使用 cargo build --release命令编译成 exe 文件。
  2. 将 exe 文件以及 config.toml 放在同一个文件夹。
  3. 配置好 config.toml 文件,双击 exe 文件,会启动控制台,这时候服务已经正常启动。
  4. 如果出现控制台闪烁一下关闭了,说明程序有报错,用控制台启动 exe 文件可以看到报错。
  5. 这时候就可以测试配置好的接口,可以用浏览器或者postman等工具。
  6. 如果鼠标点到控制台上,会造成接口一直响应,ctrl + c 就可以让接口正常响应了。

五、总结

这个 API 模拟服务器展示了 Rust 在构建工具类应用方面的优势:简洁、高效、类型安全。通过配置文件驱动的方式,大大提高了使用的灵活性,让开发者能够快速搭建测试环境,无论是个人开发还是团队协作,这样的工具都能显著提升开发效率。

相关推荐
小杨同学492 小时前
【嵌入式 C 语言实战】单链表的完整实现与核心操作详解
后端·算法·架构
咋吃都不胖lyh2 小时前
RESTful API 调用详解(零基础友好版)
后端·restful
源代码•宸2 小时前
Golang原理剖析(map)
经验分享·后端·算法·golang·哈希算法·散列表·map
小镇cxy2 小时前
Ragas 大模型评测框架深度调研指南
后端
qq_256247052 小时前
拯救“复读机”:从小模型死循环看 Logits 到 Dist 的全流程采样机制
后端
七八星天2 小时前
Exception异常与异常处理(.Net)
后端
千寻技术帮2 小时前
10340_基于Springboot的游戏网站
spring boot·后端·游戏·vue·商城
于顾而言2 小时前
【一文带你搞懂】漏扫核弹Nuclei
后端