40-HTTP 客户端与服务端
18 分钟阅读
HTTP 客户端与服务端入门 (HTTP Client and Server)
面向 Rust 1.97.1 (stable, 2026-07)。本篇假设你熟悉 Go,凡有可比性处均给出 🐹 Go 对比。
热度:
hot高频 |common常见 |occasional偶尔 |advanced进阶少见
本篇能解决什么:
- 你是否习惯了 Go 的
net/http,一进 Rust 就找「标准库 HTTP」,却发现 std 几乎没有? - 你是否想用几行代码发 GET/POST JSON,却不知道该选哪个 client crate、超时和 Header 怎么设?
- 你是否想写一个最小 hello 服务、带路径参数和 query,却分不清 axum 路由怎么挂?
- 你是否想返回 JSON、共享 State、加中间件,却不知道和 Go 的 ServeMux / middleware 怎么对照?
- 你是否纠结:怎么优雅关停、怎么测 handler、什么时候才需要碰 tower?
- 你是否分不清 reqwest 的 rustls 与 native-tls,或不知道 Authorization 鉴权头怎么挂?
- 你是否在找 CORS / WebSocket / multipart?→ 见 52-http-advanced。
术语速查表:
| 术语 / 缩略词 | 全称 / 读法 | 中文 | 一句话解释 | Go 里的近亲 |
|---|---|---|---|---|
| HTTP | HyperText Transfer Protocol | 超文本传输协议 | Web 上最常用的请求/响应协议 | 同名 |
| client | — | 客户端 | 主动发请求、收响应的一方 | http.Client |
| server | — | 服务端 | 监听端口、处理请求的一方 | http.Server |
| reqwest | request(谐音) | 常用 HTTP 客户端 crate | 高层封装,默认 async | net/http Client |
| axum | — | 常用 Web 框架 | 基于 tower/hyper 的路由与提取器 | ServeMux + 中间件生态 |
| hyper | — | 底层 HTTP 实现 | 偏底层的 HTTP 库,很多框架建其上 | 更接近自己拼 http.Server |
| tower | — | 服务中间件栈 | Service trait + 层叠 Layer | 中间件链 / RoundTripper |
| extractor | — | 提取器 | 从请求里抽出 Path/Query/Json/State 等 | 手写 r.URL / json.NewDecoder |
| State | — | 共享状态 | 注入到 handler 的应用级数据(连接池等) | 闭包捕获 / 结构体字段 |
| middleware | — | 中间件 | 包在 handler 外的横切逻辑(日志、鉴权) | http.Handler 包装 |
| graceful shutdown | — | 优雅关停 | 停接新连接,等在途请求结束再退出 | Shutdown / ShutdownContext |
| status code | — | 状态码 | 如 200/404/500,表示结果类别 | http.StatusOK 等 |
| JSON | JavaScript Object Notation | JSON | 常见 API 正文格式 | encoding/json |
| serde | SERialize/DEserialize | 序列化框架 | 类型 ↔ JSON 等格式;详见 36-serde | encoding/json |
| async | asynchronous | 异步 | 用 Future/.await 做并发 I/O;详见 31-async | goroutine + 阻塞 I/O 或异步库 |
| rustls | — | 纯 Rust TLS 实现 | reqwest 常用的 TLS 后端之一 | 自定义 tls.Config / 纯 Go TLS |
| native-tls | — | 系统原生 TLS | 走平台 TLS(Schannel/Secure Transport/OpenSSL) | 系统证书库那条路 |
| Authorization | — | 鉴权头 | 常见 Bearer / Basic 凭证所在 Header | req.Header.Set("Authorization", ...) |
说明:本表覆盖本篇出现的所有专业名词与缩略词;正文首次出现时仍会就地解释一次。
热度索引:
| 热度 | 题目 |
|---|---|
hot | Q1, Q2, Q3, Q5, Q6, Q7, Q14, Q15 |
common | Q4, Q8, Q10, Q11, Q13 |
occasional | Q9, Q12 |
advanced | — |
Q1. 为什么 Rust 不像 Go 那样在标准库里内建 net/http?
Tags: hot beginner stdlib ecosystem
适用版本: Rust 1.0+(生态选型;与具体 crate 版本无关)
一句话答案: Rust 标准库刻意保持「最小可用」:提供 TCP/UDP、部分 TLS 相关能力的基础,但不绑定某一套 HTTP 客户端/服务端 API;社区用 reqwest(客户端)、axum / hyper(服务端)等 crate 组合,由 Cargo 选版本与 feature。
解答:
Go 把 HTTP 当成「语言自带电池」:net/http 从 std 直接 import,API 稳定、文档统一。Rust 的哲学更接近「语言 + 包管理器」:
| 层级 | Rust 常见选择 | Go |
|---|---|---|
| 字节流 / 套接字 | std::net、tokio::net | net |
| HTTP 语义 | http crate(类型)+ hyper | net/http |
| 好用的 Client | reqwest | http.Client |
| 好用的 Server 框架 | axum、Actix、Rocket… | 多半仍基于 net/http |
| |
这意味着:没有「唯一官方 HTTP」,但有「事实上的主流组合」。选型成本换来的是:feature 可裁剪(JSON、rustls/native-tls、blocking)、版本可升级、不把重量级协议绑进 std。
异步运行时(Tokio 等)也不在 std 里——HTTP 栈几乎都建立在 async 之上,详见 31-async-programming。
Go 对比:
| |
- Go 怎么做:stdlib 提供 Client、Server、ServeMux、默认 Transport。
- Rust 为什么不同:避免把协议细节与 TLS/异步模型永久钉死在 std;用生态迭代。
- Go 程序员易踩的坑:搜「Rust http 标准库」无果就以为不能写 HTTP——其实是
cargo add reqwest/axum。
记忆点:
- std ≈ 套接字与基础类型;HTTP 在 crates.io。
- 入门默认:客户端 reqwest,服务端 axum。
Q2. 用 reqwest 怎么发一个 GET?
Tags: hot beginner reqwest GET client
适用版本: reqwest 0.11+/0.12.x;需 Tokio 等 async runtime
一句话答案:
reqwest::get(url).await? 或先建 Client 再 .get(url).send().await?;响应用 .text() / .bytes() / .json() 取正文。记得这是 async,要在 #[tokio::main] 里跑。
解答: 最小依赖:
| |
一次性 GET:
| |
可复用的 Client(连接池、默认超时更好管):
| |
reqwest::get 适合脚本;应用里更常持有一个 Client 克隆共享(内部是 Arc)。TLS、代理、cookie 等用 Client::builder()。
Go 对比:
| |
- Go 怎么做:
http.Get/client.Do;别忘关Body。 - Rust 为什么不同:默认走 async Future;阻塞版要用
reqwest的blockingfeature。 - Go 程序员易踩的坑:在非 async 上下文直接
.await;或忘了error_for_status,把 404 当成功读 body。
记忆点:
- 脚本:
reqwest::get;长期:Client::new()。 - 先看
status(),再读 body。
Q3. 怎么 POST JSON?状态码怎么处理?
Tags: hot reqwest POST JSON status
适用版本: reqwest 0.12(需 json feature);serde 1.x
一句话答案:
打开 reqwest 的 json feature,用 .json(&value) 发 body;用 resp.status() / error_for_status() 处理状态码;反序列化用 .json::<T>().await。类型侧依赖 serde(见 36-serde-and-serialization)。
解答:
| |
| |
状态码习惯:
status.is_success()/is_client_error()/is_server_error()- 希望「非 2xx 直接失败」→
error_for_status()(消费响应,变Err) - 要读错误正文再决定 → 先看 status,再
.text()/.json()
Content-Type:.json(...) 会设为 application/json。
Go 对比:
| |
- Go 怎么做:自己 Marshal + 设 Content-Type;或第三方客户端封装。
- Rust 为什么不同:
.json(&T)把 serde 序列化与 Header 绑在一起。 - Go 程序员易踩的坑:忘开
features = ["json"],.json方法不存在。
记忆点:
- POST JSON =
.post(url).json(&body).send().await。 - 状态码用
status()或error_for_status()。
Q4. 超时和自定义 Header 怎么设?
Tags: common reqwest timeout header
适用版本: reqwest 0.12.x
一句话答案:
在 Client::builder().timeout(...) 设默认超时;单次请求用 .timeout(...) / .header(...);需要「连接超时 vs 整体超时」时用 builder 的更细选项或自己包一层 tokio::time::timeout。
解答:
| |
| |
超时到了会返回 reqwest::Error(可 is_timeout() 判断)。Header 名用常量(如 AUTHORIZATION)或字符串;非法字符会在 HeaderValue 构造时失败。
需要「最多等 2 秒,不管 client 默认」时,也可:
| |
Go 对比:
| |
- Go 怎么做:
Client.Timeout+Header.Set;更细可用context.WithTimeout。 - Rust 为什么不同:同样分「Client 默认」与「单次覆盖」;async 下还可用
tokio::time::timeout。 - Go 程序员易踩的坑:只设了连接相关超时却以为覆盖了读 body 全程——先读清 reqwest 文档里 timeout 的范围。
记忆点:
- 默认超时挂在
Client::builder;单次用.timeout/.header。 - 超时错误用
err.is_timeout()分支。
Q5. axum 最小 hello 服务怎么写?
Tags: hot beginner axum server hello
适用版本: axum 0.7+/0.8.x;tokio 1.x
一句话答案:
Router::new().route("/", get(handler)),再 axum::serve(listener, app).await;handler 可以是返回 impl IntoResponse 的 async 函数。
解答:
| |
| |
浏览器或 curl:
| |
要点:
- handler 签名由 extractor(提取器)决定参数、由返回值决定响应。
serve需要先bind出TcpListener(较新 axum 的常见写法)。- 生产还要日志、超时、关停——见后面几题。
Go 对比:
| |
- Go 怎么做:
HandleFunc+ListenAndServe。 - Rust 为什么不同:路由树是
Router,服务跑在 async runtime 上。 - Go 程序员易踩的坑:找
http.ListenAndServe同名 API——在 axum 里是TcpListener+axum::serve。
记忆点:
route + get(handler) + serve(listener, app)。- handler 就是 async 函数。
Q6. 路由、路径参数、query 怎么写?
Tags: hot axum route path query
适用版本: axum 0.8.x
一句话答案:
.route("/users/{id}", get(...)) 用 Path 抽路径参数;Query<T> 抽查询串;可用 Router 嵌套 nest / merge 组织模块。
解答:
| |
| |
路径语法随 axum 版本可能是 {id} 或 :id——以你锁定的版本文档为准;类型不匹配(非数字 id)会变成 400 一类失败响应。
多段参数:Path((a, b)): Path<(u64, String)> 或自定义结构体。嵌套:
| |
Go 对比:
| |
- Go 怎么做:stdlib 弱路由;常用第三方 mux。
- Rust 为什么不同:axum 把 Path/Query 做成类型化 extractor。
- Go 程序员易踩的坑:把 query 当 path;或
Page字段名与 query key 不一致却怪框架。
记忆点:
- 路径 →
Path;查询 →Query+ Deserialize。 - 用
nest拼前缀。
Q7. 怎么返回 JSON?和 serde 什么关系?
Tags: hot axum JSON serde IntoResponse
适用版本: axum 0.8(通常已带 json);serde 1.x
一句话答案:
返回 Json(value)(axum 的 JSON 响应包装);value 需实现 serde 的 Serialize。请求体用 Json<T> extractor,T: Deserialize。序列化细节见 36-serde-and-serialization。
解答:
| |
| |
也可返回 (StatusCode, Json(T)) 控制状态码。反序列化失败时,axum 默认会变成客户端错误响应(具体状态码/body 可自定义 rejection)。
Go 对比:
| |
- Go 怎么做:手写 Encoder/Decoder,或框架绑定。
- Rust 为什么不同:
Json<T>把 HTTP 与 serde 粘在类型上。 - Go 程序员易踩的坑:结构体没
Serialize/Deserialize就包Json——编译期直接拒绝。
记忆点:
- 出站:
Json(T)+Serialize。 - 入站:
Json<T>+Deserialize。
Q8. State 和中间件怎么加?
Tags: common axum State middleware
适用版本: axum 0.8.x
一句话答案:
用 .with_state(s) 注入共享状态,handler 参数写 State(s);横切逻辑用 Router::layer(...) 挂 middleware(中间件),常见来自 tower-http(trace、cors、timeout)。
解答:
| |
中间件示例(依赖 tower-http):
| |
| |
State 类型必须 Clone(框架会克隆进每个请求);重资源放进 Arc。中间件顺序:后 layer 的更靠外(与 tower 惯例一致,写的时候对一下文档)。
Go 对比:
| |
- Go 怎么做:闭包捕获依赖;
Handler包装做中间件。 - Rust 为什么不同:
Stateextractor +Service/Layer类型栈(tower)。 - Go 程序员易踩的坑:
State里塞未Clone的池子;应Arc<Pool>+#[derive(Clone)]外层。
记忆点:
- 共享依赖 →
with_state+State。 - 日志/超时/CORS →
layer+ tower-http。
Q9. 优雅关停怎么简述?
Tags: occasional graceful-shutdown axum signal
适用版本: axum 0.8;tokio 1.x
一句话答案:
监听 Ctrl+C / SIGTERM,对 axum::serve(...).with_graceful_shutdown(signal) 传入一个「等到信号就完成」的 Future;进程停接新连接,尽量等在途请求结束后再退出。
解答:
思路与 Go 的 Shutdown 相同:graceful shutdown(优雅关停)= 不再接受新请求 + 等待已有请求完成(可带超时)。
| |
| |
生产上常再加:关停超时(到期强制停)、把信号接到 K8s 的 preStop、把客户端的 idle 连接也清掉。细节因部署环境而异,本篇只要求记住「信号 Future + with_graceful_shutdown」这条主线。
Go 对比:
| |
- Go 怎么做:
signal.Notify+Server.Shutdown。 - Rust 为什么不同:关停挂在 serve 的 Future 上,而不是另起一个阻塞
Shutdown调用那么「命令式」。 - Go 程序员易踩的坑:只
ctrl_c退出整个进程,不挂 graceful——在途请求被掐断。
记忆点:
- 信号 Future →
with_graceful_shutdown。 - 先停接入,再等在途。
Q10. 和 Go 的 Client / ServeMux 怎么对照?
Tags: common go Client ServeMux 对照
适用版本: 概念对照;reqwest / axum 主流版本
一句话答案:
http.Client ≈ reqwest::Client;http.ServeMux / HandleFunc ≈ axum::Router + route;中间件在 Go 常是 Handler 包装,在 Rust 常是 tower Layer;JSON 在 Go 靠 encoding/json,在 Rust 靠 serde + Json。
解答:
| 职责 | Go | Rust(本篇默认) |
|---|---|---|
| 发请求 | http.Client / http.Get | reqwest::Client |
| 超时 | Client.Timeout / context | Client::builder().timeout / tokio::time::timeout |
| Header | Header.Set | .header / HeaderMap |
| 听端口 | ListenAndServe | TcpListener + axum::serve |
| 路由 | ServeMux / chi / gin | Router::route / nest |
| 共享依赖 | 闭包 / 结构体 | State |
| 中间件 | 包装 Handler | layer(tower) |
| JSON | encoding/json | serde + axum Json |
| 关停 | Server.Shutdown | with_graceful_shutdown |
| |
| |
心智转换:Go 是「一个包打天下」;Rust 是「runtime + client crate + server crate + serde」拼装。拼装换来的是 feature 裁剪与类型化 extractor。
Go 对比:
- Go 怎么做:stdlib 一条链学完就能上线原型。
- Rust 为什么不同:把协议实现留给生态,用类型系统表达提取与中间件。
- Go 程序员易踩的坑:按包名一一对应去找
net/http,而不是按「Client / Router / JSON」三张表选型。
记忆点:
- Client → reqwest;Mux → axum Router。
- 先对照职责表,再记 API 名。
Q11. 怎么测 handler?
Tags: common test axum handler
适用版本: axum 0.8;建议 tower::ServiceExt;tokio test
一句话答案:
把 Router 当 Service,用 oneshot 发组装好的 Request,断言 status 与 body;或抽纯函数测业务,handler 只做薄封装。不必真的 bind 端口。
解答:
| |
| |
策略:
- 路由/extractor/状态码:
oneshot集成测 Router。 - 纯业务:
fn logic(...)单测,handler 一行调用——更快、少依赖 HTTP 类型。 - 需要真 TCP 时再用
TcpListener::bind("127.0.0.1:0")起临时端口(更重)。
带 State 时:Router::with_state(test_state) 再 oneshot。
Go 对比:
| |
- Go 怎么做:
httptest+ResponseRecorder。 - Rust 为什么不同:axum 走 tower
Service,用oneshot最省事。 - Go 程序员易踩的坑:为每个单测都
ListenAndServe——慢且易端口冲突。
记忆点:
- 默认:
Router+oneshot,不绑端口。 - 业务逻辑尽量纯函数化。
Q12. 什么时候才需要碰 tower?
Tags: occasional tower Service Layer advanced-lite
适用版本: tower 0.4/0.5;与 axum 配套使用
一句话答案:
写普通 CRUD 时,用 axum 的 route / State / tower-http 现成 Layer 就够;当你要自定义中间件、限流、连接池式 Service 组合,或读懂 axum「一切都是 Service」时,再深入 tower 的 Service / Layer。
解答: tower 定义了异步服务的核心抽象:
Service:poll_ready+call(Request) -> Future<Response>Layer:把一个Service包成另一个(中间件)
axum 的 Router 本身就是 Service;你 .layer(TraceLayer::...) 时已经在用 tower,只是不直接写 trait。
该碰 tower 源码/文档的信号:
- 要写「通用中间件」而
from_fn不够用 - 要理解
Buffer、RateLimit、Retry、负载均衡等 tower 工具层 - 要把非 HTTP 的请求也统一成
Service接口 - 调试时错误信息出现
tower::Service/Layer边界
| |
入门路径建议:先 axum 官方中间件示例 → tower-http → 再读 tower 的 Service 教程。过早手写 Service 实现收益低。
Go 对比:
- Go 怎么做:中间件就是
func(http.Handler) http.Handler;没有强制统一的Servicetrait 生态(但有类似模式)。 - Rust 为什么不同:tower 把「可组合服务」做成跨 HTTP/gRPC/自定义协议的共同语言。
- Go 程序员易踩的坑:一上来就实现
Service,其实axum::middleware::from_fn已覆盖大多数需求。
记忆点:
- 日常:axum + tower-http。
- 定制组合/读透栈:再学 tower。
Q13. reqwest 该选 rustls 还是 native-tls?
Tags: common reqwest rustls native-tls TLS
适用版本: reqwest 0.12.x(feature 名以当前文档为准)
一句话答案: 默认优先 rustls(纯 Rust TLS):跨平台行为更一致、依赖链更清晰;只有需要对接系统证书库/企业定制 OpenSSL、或平台强制原生栈时,再开 native-tls。二者是 Cargo feature 二选一(或显式指定),不是运行时开关。
解答:
| |
用法本身不变:
| |
怎么选:
| 诉求 | 更合适 |
|---|---|
| Linux 容器、CI、行为可复现 | rustls |
| 必须用系统根证书/企业中间件注入的 CA | native-tls(或 rustls + 自定义根) |
| Windows/macOS 想跟 OS 信任库完全一致 | 常选 native-tls |
| 静态链接、少动态库 | 倾向 rustls |
「❌ 错误思路」——在代码里 if cfg!(windows) { ... } 换 API:TLS 后端是编译期 feature,不是两套 Client 类型。
Go 对比:
- Go 怎么做:默认用自己的 TLS 栈 + 系统根;很少在应用里「二选一 crate」。
- Rust 为什么不同:reqwest 把 TLS 实现做成可裁剪 feature。
- Go 程序员易踩的坑:两个 feature 同时胡开导致冲突,或容器里缺系统 CA 却选了 native-tls。
记忆点:
- 默认想 rustls;有系统 TLS 硬需求再 native-tls。
- 换后端改
Cargo.toml,不改业务调用。
Q14. Authorization / 鉴权头怎么挂?
Tags: hot Authorization Bearer auth header
适用版本: reqwest 0.12.x;axum 侧同属 HTTP Header 概念
一句话答案:
客户端用 .bearer_auth(token) / .basic_auth(user, pass),或 .header(AUTHORIZATION, ...);需要每个请求都带时,放进 Client::builder().default_headers(...)。服务端从 Header 读取并校验——别把 token 塞进 URL query。
解答:
| |
| |
服务端(axum)读头示意:
| |
注意:token 不要打进日志;轮换凭证时重建 Client 或按请求覆盖 Header(见 Q4)。
Go 对比:
| |
- Go 怎么做:
Header.Set;第三方客户端也有SetBasicAuth。 - Rust 为什么不同:reqwest 提供
bearer_auth/basic_auth糖,底层仍是 Header。 - Go 程序员易踩的坑:
Authorization拼写错、或Bearer后少空格。
记忆点:
- 优先
.bearer_auth/.basic_auth。 - 全局默认头挂 Client;单次覆盖用
.header。
Q15. CORS / 上传 / WebSocket 去哪看?
Tags: hot CORS WebSocket multipart 进阶
适用版本: 导航题;细节见专题
一句话答案:
浏览器跨域、Cookie/Session、multipart 上传、WebSocket/SSE、限流 Layer、httptest 式测 handler——请看 52-http-advanced-and-realtime。本篇(40)停在 JSON API 入门,避免和进阶挤在一章。
解答:
| |
| |
| |
Go 对比:
- Go 常把 CORS/WS 和
net/http教程写在一起;本系列拆成入门/进阶两篇。 - Go 程序员易踩的坑:在 40 里找不到 CORS 就以为 axum 不支持。
记忆点:
- JSON API 入门 → 40。
- 浏览器与实时 → 52。