-
因大多在线提供get、post的服务需要注册、登录才能用,还容易受到反爬虫限制,导致响应错误。
-
用axum写的本地服务,编译后体积小,内存占用低,响应快,还可以随时根据reqwest学习需要修改响应内容。
-
本手写服务,主要返回json格式的相应数据。
1、服务器接口(接口地址末尾不要加 / ):
| get请求 | http://127.0.0.1:8080/get | get | 无 | 无 | json | {"msg":"hello get!","请求方式":"GET"} | |
| 查询字符串 | http://127.0.0.1:8080/get?name=xx&age=22 | get | 无 | 无 | json | {"msg":{"查询参数":{"age":"22","name":"xx"},"路径参数":["query"]},"请求方式":"GET"} | 任意数量的任意查询字符串 |
| 路径参数 | http://127.0.0.1:8080/123/xyz | get | 无 | 无 | json | {"msg":{"路径参数":["path","123","xyz"]},"请求方式":"GET"} | 任意数量的任意路径参数 |
| 路径参数+查询字符串 | http://127.0.0.1:8080/123/xyz?name=小王&age=25 | get | 无 | 无 | json | {"msg":{"查询参数":{"age":"22","name":"小王"},"路径参数":["path","123","xyz"]},"请求方式":"GET"} | 任意数量的任意路径参数、查询字符串 |
| 获取请求头 | http://127.0.0.1:8080/get/headers | get | 无 | 无 | json | {"msg":{"headers":{"accept":"*/*","host":"127.0.0.1:8080"}},"请求方式":"GET"} | 请求时可携带自定义请求头,一并返回 |
| 获取字节数据 | http://127.0.0.1:8080/get/bytes | get | 无 | 无 | Bytes | 浏览器打开会下载文件 | |
| 服务器延迟10秒返回数据 | http://127.0.0.1:8080/get/sleep | get | 无 | 无 | json | {"msg":"延时10秒后返回","请求方式":"GET"} | |
| 重定向 | http://127.0.0.1:8080/get/redirect | get | 无 | 无 | json | {"msg":{"路径参数":["重定向的网页"]},"请求方式":"GET"} | 307,重定向到http://127.0.0.1:8080/get/重定向的网页 |
| 获取gzip压缩数据 | http://127.0.0.1:8080/get/gzip | get | 无 | 无 | json | Hello, World! This is a compressed response. Hello, World! This is a…. | |
| 下载文件 | http://127.0.0.1:8080/get/file/mp3.zip | get | 无 | 无 | Bytes | 浏览器打开会下载文件 | |
| 获取所有用户信息 | http://127.0.0.1:8080/users | get | 无 | 无 | json | [{"id":1,"name":"张三","age":18,"email":"zhangsan@example.com"},{"id":2,"name":"李四","age":20,"email":"lisi@example.com"},{"id":3,"name":"王五","age":22,"email":"wangwu@example.com"}] | 数据为服务器固定数据 |
| 路径参数根据id获取单个用户信息 | http://127.0.0.1:8080/users/{{id}} | get | 无 | 无 | json | {"id":1,"name":"张三","age":18,"email":"zhangsan@example.com"} | id为用户id |
| post发送json数据 | http://127.0.0.1:8080/post/json | post | json | {"key":"value"} | json | {"msg":{"body":{"key":"value"}},"请求体":"json","请求方式":"POST"} | |
| post发送表单数据 | http://127.0.0.1:8080/post/form | post | Form | form(&[("key", "value")]) | json | {"msg":{"body":{"key":"value"}},"请求体":"form","请求方式":"POST"} | |
| post发送字节数据 | http://127.0.0.1:8080/post/byte | post | body(Bytes) | body(b"binary data".to_vec()) | json | {"msg":{"body":"binary data"},"请求体":"body(Bytes)","请求方式":"POST"} | |
| post发送字符串数据 | http://127.0.0.1:8080/post/str | post | body(String) | body("hello world") | json | {"msg":{"body":"hello world"},"请求体":"body(String)","请求方式":"POST"} | |
| 表单上传数据及文件 | http://127.0.0.1:8080/post/upload | post | Form | form-data ("name"="小王","age"="22",(name="file"; filename="test.jpg" Content-Type: image/jpeg) | json | {"data":{"上传的信息":{"age":"22","name":"小王"},"上传的文件":{"大小":"189","字段名":"file","文件名":"Cargo111.toml","类型":"text/toml"}},"msg":"模拟文件上传成功","请求体":"file","请求方式":"POST"} | 发送数据写法仅供参考,格式以实际form表单为(文件未实际上传,仅解析返回) |
| get错误路径 | 其他路径 | get | 无 | 无 | json | {"msg":"请求路径不存在!"} | |
2、前后端(reqwest – axum)同项目
- axum 后端服务器与 reqwest 前端请求同在一个项目目录下
- 两种服务可单独运行,方便前端学习需要时添加后端服务
3、项目目录
项目目录下:
├─ Cargo.toml # 项目主配置文件,reqwest与axum所需的相同依赖
├─ files # server 提供下载文件的服务器目录
│ └─ mp3.zip # 提供的下载文件,自己随意添加
├─ client # 学习 reqwest 写代码的文件夹
│ ├─ Cargo.toml # reqwest 所需的依赖
│ └─ main.rs
└─ server # axum 后端服务代码
├─ Cargo.toml # axum 后端代码所需的依赖
├─ main.rs # 主文件,启动服务、主路由、打印访问的中间件
├─ my_get.rs # 提供get请求
├─ my_post.rs # 提供post请求
└─ my_users.rs # 提供预订的模拟用户信息
4、主Cargo.toml配置
[workspace]
members = ["server", "client"]
[workspace.dependencies]
tokio = { version = "1.52", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
-
[workspace]:定义独立运行的工作空间路径
- members:工作空间的相对路径,["server", "client"]对应的是server文件夹、 client文件夹
-
[workspace.dependencies]:提供共同所需依赖
5、server/Cargo.toml 子配置
[package]
name = "server"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "server"
path = "main.rs"
[dependencies]
axum = { version = "0.8", features = ["macros","multipart"] }
tower-http = { version = "0.7", features = ["fs","compression-gzip"] }
tokio.workspace = true
serde_json.workspace = true
serde.workspace = true
- [[bin]]:定义编译后生成的二进制文件
- name:生成的可执行文件名,同时编译运行:cargo run -p server,另一个命令窗口:cargo run -p client
- path:指定入口主文件路径。不指定默认寻找src/main.rs
- tokio.workspace = true:通过继承使用根Cargo.toml定义的依赖。
- 写法二:tokio = {workspace = true}
- 子工作空间单独增加依赖功能:tokio = {workspace = true, features = ["xxxx"]}
6、server/main.rs
- 定义主路由
- 绑定本地ip及端口
- 启动服务,等待客户端连接
- 打印日志中间件,用于打印客户端连接的路径地址和请求方式
mod my_get;
mod my_post;
mod my_users;
use axum::{
Json, Router,
http::Request,
middleware::{self, Next},
response::Response,
};
use serde_json::json;
use tokio::net::TcpListener;
#[tokio::main]
async fn main() {
let router = Router::new()
.nest("/get", my_get::get_route().await)
.nest("/post", my_post::post_route().await)
.nest("/users", my_users::users_route().await)
.fallback(|| async { Json(json!({"msg": "请求路径不存在!"})) })
.layer(middleware::from_fn(log_request));
let listener = TcpListener::bind("0.0.0.0:8080").await.unwrap();
println!("服务器启动成功,监听地址:http://127.0.0.1:8080,以下为具体接口:\\n");
println!("GET(路径参数、查询参数均可): http://127.0.0.1:8080/get");
println!("GET 请求头测试: http://127.0.0.1:8080/get/headers");
println!("GET 字节数组测试: http://127.0.0.1:8080/get/bytes");
println!("GET 延时10秒返回测试: http://127.0.0.1:8080/get/sleep");
println!("GET 重定向测试: http://127.0.0.1:8080/get/redirect");
println!("GET 压缩测试: http://127.0.0.1:8080/get/gzip");
println!("GET 下载文件测试: http://127.0.0.1:8080/get/file/mp3.zip\\n");
println!("GET 用户列表: http://127.0.0.1:8080/users");
println!("GET 用户详情: http://127.0.0.1:8080/users/{{id}}\\n");
println!("POST json请求体: http://127.0.0.1:8080/post/json");
println!("POST form请求体: http://127.0.0.1:8080/post/form");
println!("POST byte请求体: http://127.0.0.1:8080/post/byte");
println!("POST str请求体: http://127.0.0.1:8080/post/str");
println!("POST 模拟file上传: http://127.0.0.1:8080/post/upload");
println!("——————————————————–");
axum::serve(listener, router).await.unwrap();
}
/// 日志中间件,打印每个请求的路径、方法
async fn log_request(req: Request<axum::body::Body>, next: Next) -> Response {
println!("<{}> 请求路径: {}", req.method(), req.uri().path());
next.run(req).await
}
7、server/my_get.rs
- 处理get请求,返回相应的数据
use axum::Json;
use axum::Router;
use axum::body::Bytes;
use axum::extract::{Path, Query};
use axum::http::HeaderMap;
use axum::response::Redirect;
use axum::routing;
use serde_json::Map;
use serde_json::Value;
use serde_json::json;
use std::collections::HashMap;
use tower_http::compression::CompressionLayer;
use tower_http::services::ServeDir;
/// get子路由
pub async fn get_route() -> Router {
Router::new()
.route("/", routing::get(get_page))
.route("/{*params}", routing::get(get_path)) // 路径参数
.route("/headers", routing::get(get_headers))
.route("/bytes", routing::get(get_bytes))
.route("/sleep", routing::get(get_sleep))
.nest_service("/file", ServeDir::new("files"))
.route(
"/redirect",
routing::get(|| async { Redirect::temporary("/get/重定向的网页") }),
)
.nest(
"/gzip",
Router::new()
.route("/", routing::get(get_gzip))
.layer(CompressionLayer::new()),
)
}
/// 有查询参数返回查询参数,无查询参数返回hello get!
async fn get_page(Query(query): Query<HashMap<String, String>>) -> Json<serde_json::Value> {
if query.is_empty() {
Json(json!({"请求方式": "GET","msg": "hello get!"}))
} else {
Json(json!({"请求方式": "GET","msg": query}))
}
}
/// 有查询参数返回路径参数和查询参数,无查询参数返回路径参数
async fn get_path(
Path(params): Path<String>,
Query(query): Query<HashMap<String, String>>,
) -> Json<serde_json::Value> {
let params = params.split('/').collect::<Vec<_>>();
if query.is_empty() {
Json(json!({"请求方式": "GET","msg": {"路径参数":params}}))
} else {
Json(json!({"请求方式": "GET","msg": {"路径参数":params,"查询参数":query}}))
}
}
/// 获取get请求头,返回请求头
async fn get_headers(headers: HeaderMap) -> Json<serde_json::Value> {
let mut headers_map = Map::new();
for (name, value) in headers.iter() {
// name 是 &HeaderName, value 是 &HeaderValue
// 将它们转换为 String
if let Ok(val_str) = value.to_str() {
headers_map.insert(
name.as_str().to_string(),
Value::String(val_str.to_string()),
);
} else {
// 如果值包含非 UTF-8 字符,可以跳过或转为 base64,这里简单跳过
headers_map.insert(
name.as_str().to_string(),
Value::String("<non-utf8-value>".to_string()),
);
}
}
Json(json!({"请求方式": "GET","msg": {"headers":headers_map}}))
}
/// 返回字节数组
async fn get_bytes() -> Bytes {
Bytes::from("hello bytes")
}
/// 延时返回数据
async fn get_sleep() -> Json<serde_json::Value> {
// 延时 10 秒
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
Json(json!({"请求方式": "GET","msg": "延时10秒后返回"}))
}
/// 对返回数据进行gzip压缩
async fn get_gzip() -> String {
// 生成重复文本以展示高压缩率
let data = "Hello, World! This is a compressed response. ".repeat(1000);
data
}
8、server/my_post.rs
- 处理post请求,返回相应的数据
use axum::Form;
use axum::Json;
use axum::Router;
use axum::body::Bytes;
use axum::extract::Multipart;
use axum::routing;
use serde_json::json;
use std::collections::HashMap;
/// post子路由
pub async fn post_route() -> Router {
Router::new()
.route("/json", routing::post(post_json_page))
.route("/form", routing::post(post_form_page))
.route("/byte", routing::post(post_byte_page))
.route("/str", routing::post(post_str_page))
.route("/upload", routing::post(post_upload_page))
}
/// 接收json参数,返回json参数和请求方式
async fn post_json_page(Json(body): Json<serde_json::Value>) -> Json<serde_json::Value> {
Json(json!({"请求方式": "POST","请求体":"json","msg": {"body": body}}))
}
/// 接收form参数,返回form参数和请求方式
async fn post_form_page(Form(form): Form<HashMap<String, String>>) -> Json<serde_json::Value> {
Json(json!({"请求方式": "POST","请求体":"form","msg":{"body":form}}))
}
/// 接收byte参数,返回byte参数和请求方式
async fn post_byte_page(body: Bytes) -> Json<serde_json::Value> {
let text = String::from_utf8(body.to_vec())
.map_err(|_| "请求体不是 UTF-8 编码")
.unwrap_or("请求体为空".to_string());
Json(json!({"请求方式": "POST","请求体":"body(Bytes)","msg":{"body":text}}))
}
/// 接收str参数,返回str参数和请求方式
async fn post_str_page(body: String) -> Json<serde_json::Value> {
Json(json!({"请求方式": "POST","请求体":"body(String)","msg":{"body":body}}))
}
/// 模拟文件上传,实际返回上传的信息和上传的文件信息
async fn post_upload_page(mut multipart: Multipart) -> Json<serde_json::Value> {
let mut hm1 = HashMap::new();
let mut hm2 = HashMap::new();
// 迭代 multipart 中的每一个字段
while let Some(field) = multipart.next_field().await.unwrap_or_default() {
let name = field.name().unwrap_or("").to_string();
// 判断是文件还是普通键值对
if let Some(file_name) = field.file_name() {
// — 处理文件 —
let content_type = field.content_type().unwrap_or("").to_string();
let file_name = file_name.to_string();
// 读取文件数据
let data = field.bytes().await.unwrap_or_default();
hm2.insert("字段名".to_string(), name);
hm2.insert("文件名".to_string(), file_name);
hm2.insert("类型".to_string(), content_type);
hm2.insert("大小".to_string(), data.len().to_string());
// 在此处添加保存文件的逻辑,例如 tokio::fs::write(…)
} else {
// — 处理普通键值对 (文本) —
let text = field.text().await.unwrap_or_default();
hm1.insert(name, text);
// 在此处处理业务逻辑,例如存入数据库或结构体
}
}
println!(
"< OK > <上传的信息> {:?}\\n< OK > <上传的文件> {:?}",
hm1, hm2
);
Json(
json!({"请求方式": "POST","请求体":"file","msg":"模拟文件上传成功","data":{"上传的信息":hm1,"上传的文件":hm2}}),
)
}
9、server/my_user.rs
- 根据get请求,返回相应的用户信息
use axum::Json;
use axum::Router;
use axum::extract::Path;
use axum::routing;
use serde::Serialize;
use std::sync::LazyLock;
// 用户信息结构体
#[derive(Serialize, Clone, Default)]
struct User {
id: u32,
name: String,
age: u32,
email: String,
}
impl User {
// 初始化直接生成固定数据
fn new_users() -> Vec<User> {
vec![
User {
id: 1,
name: "张三".to_string(),
age: 18,
email: "zhangsan@example.com".to_string(),
},
User {
id: 2,
name: "李四".to_string(),
age: 20,
email: "lisi@example.com".to_string(),
},
User {
id: 3,
name: "王五".to_string(),
age: 22,
email: "wangwu@example.com".to_string(),
},
]
}
}
// 初始化用户数据
static USERS: LazyLock<Vec<User>> = LazyLock::new(|| User::new_users());
// 用户子路由
pub async fn users_route() -> Router {
Router::new()
.route("/", routing::get(get_users))
.route("/{id}", routing::get(get_user))
}
/// 获取所有用户信息
async fn get_users() -> Json<Vec<User>> {
Json(USERS.clone())
}
/// 通过路径参数,获取指定id的用户信息
async fn get_user(Path(id): Path<u32>) -> Json<User> {
let user = USERS
.iter()
.find(|u| u.id == id)
.cloned()
.unwrap_or_default();
Json(user)
}
网硕互联帮助中心




评论前必须登录!
注册