使用 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 实现了每日滚动的日志文件,同时支持控制台实时输出。
四、基本使用
- 使用
cargo build --release命令编译成 exe 文件。 - 将 exe 文件以及 config.toml 放在同一个文件夹。
- 配置好 config.toml 文件,双击 exe 文件,会启动控制台,这时候服务已经正常启动。
- 如果出现控制台闪烁一下关闭了,说明程序有报错,用控制台启动 exe 文件可以看到报错。
- 这时候就可以测试配置好的接口,可以用浏览器或者postman等工具。
- 如果鼠标点到控制台上,会造成接口一直响应,ctrl + c 就可以让接口正常响应了。
五、总结
这个 API 模拟服务器展示了 Rust 在构建工具类应用方面的优势:简洁、高效、类型安全。通过配置文件驱动的方式,大大提高了使用的灵活性,让开发者能够快速搭建测试环境,无论是个人开发还是团队协作,这样的工具都能显著提升开发效率。