【免费下载链接】wreq
An ergonomic, privacy-aware Rust HTTP Client
wreq是一个注重易用性与隐私的 Rust 异步 HTTP 客户端,支持 HTTP/1、HTTP/2 与 HTTP/3。它的核心设计是"运行时无关":库本身不绑定任何具体异步运行时,而是通过Executor、Timer、Transport三大可插拔接口,让你自由选择 tokio、compio 或其他运行时来驱动它。本文带你从零理解这套接口,并学会快速接入自己的运行时。
为什么要做可插拔运行时?
传统 HTTP 客户端通常和特定运行时深度耦合,而 wreq 选择"协议与运行时解耦":
- Executor(执行器):负责 spawn 后台任务,比如 HTTP/2 的连接驱动、HTTP/3 的请求体管道;
- Timer(定时器):负责
sleep/sleep_until等时间操作,驱动 keep-alive ping、超时等机制; - Transport(传输层):负责底层字节读写,HTTP/1 与 HTTP/2 基于
tokio::io::AsyncRead+AsyncWrite抽象,HTTP/3 则基于 QUIC 传输契约。
这样设计的好处是:你既可以跑在 tokio 上,也可以跑在 compio、Smol 甚至自研运行时上,wreq 的测试套件就同时包含了 Tokio 适配器 和 Compio 适配器 两套实现,是极好的参考样板。
运行时组件的总入口在 src/rt.rs,模块注释写得很清楚:
"This module provides traits and types that allow netty to be runtime-agnostic. By abstracting over async runtimes, netty can work with different executors, timers, and IO transports."
接口一:Executor —— 一个方法就够了
Executor是三个接口中最简单的,定义见 src/rt.rs:
pub trait Executor<Fut> { /// Place the future into the executor to be run. fn execute(&self, fut: Fut); }要点:
- 它是泛型 trait:
Executor<Fut>按未来(Future)类型参数化。实践中只需为所有Future做一次泛型实现(impl<F: Future + Send + 'static> Executor<F>),即可满足 HTTP/2 与 HTTP/3 各自需要的任务类型; - 实现里只需一行"spawn 语义",例如 tokio 版就是调用
tokio::spawn(fut),compio 版则是compio::runtime::spawn(fut).detach(); - HTTP/2 内部通过 Http2ClientConnExec 这个"边界 trait"自动适配——只要你实现了通用的
Executor且类型可Clone,就自动获得该实现,无需额外编码。
接口二:Timer —— 支持重置的睡眠抽象
Timer定义在 src/rt/timer.rs,提供 4 个方法:
| 方法 | 作用 | 是否有默认实现 |
|---|---|---|
sleep(duration) | 返回duration后就绪的Sleep未来 | 必须实现 |
sleep_until(deadline) | 返回到达deadline时就绪的Sleep未来 | 必须实现 |
now() | 获取当前时间 | 默认Instant::now() |
reset(sleep, new_deadline) | 修改已有睡眠未来的截止点 | 有默认实现,建议覆盖 |
两个容易踩坑的细节:
- 返回的是
Pin<Box<dyn Sleep>>:Sleep是一个Future<Output = ()> + Send + Sync的 trait(见 Sleep 定义),它额外要求实现reset,让库能原地修改超时而不重建未来; - 忘记传 timer 会直接 panic:如果未向 Builder 提供 timer,
Time::Empty在真正sleep时会 panic 出"You must supply a timer."(见 Time 枚举)。所以启用 keep-alive ping 等定时功能时,记得调用Builder::timer()。
接口三:Transport —— IO 抽象与 QUIC 契约
Transport 层按协议分为两种形态:
HTTP/1 与 HTTP/2:AsyncRead + AsyncWrite
握手方法handshake(io)只要求 IO 对象满足tokio::io::AsyncRead + AsyncWrite + Unpin。如果你的底层流来自其他运行时,写一个薄包装即可。官方测试里的 CompioIO 就是典型例子:把 compio 的AsyncStream包一层,实现poll_read/poll_write/poll_flush/poll_shutdown四个方法,即可无缝接入。
HTTP/3:QUIC 传输契约
启用http3feature 后,wreq 不再创建 UDP socket 或做 TLS,而是要求调用方完成 QUIC/TLS 握手后,把连接交给 rt::quic 模块 定义的契约:
Connection:接收单向/双向流、提供 stream 句柄(定义);OpenStreams:按需打开出站流,等待对端流额度;SendStream/RecvStream/BidiStream:无帧语义的字节读写,带显式的 FIN(poll_finish)、STOP_SENDING 观测(poll_stopped)与 reset。
如果你已有实现http3::quictrait 的传输(如http3-quic),无需重写——Compat 适配器 一行Compat::new(inner)即可完成转换,这也是 HTTP/3 客户端模块 文档中推荐的接入方式。
完整示例:用 tokio 驱动 HTTP/2 客户端
下面把三个接口串起来(精简自 tests/support/rt.rs 与 tests/support/mod.rs):
use std::future::Future; use std::time::{Duration, Instant}; use std::pin::Pin; use wreq::rt::{Executor, Sleep, Timer}; // 1. Executor:一行 spawn #[derive(Clone)] struct TokioExecutor; impl<F> Executor<F> for TokioExecutor where F: Future + Send + 'static, { fn execute(&self, fut: F) { tokio::spawn(fut); } } // 2. Timer:包装 tokio::time::sleep #[derive(Clone)] struct TokioTimer; impl Timer for TokioTimer { fn sleep(&self, d: Duration) -> Pin<Box<dyn Sleep>> { Box::pin(TokioSleep { inner: tokio::time::sleep(d) }) } fn sleep_until(&self, deadline: Instant) -> Pin<Box<dyn Sleep>> { Box::pin(TokioSleep { inner: tokio::time::sleep_until(deadline.into()) }) } fn reset(&self, sleep: &mut Pin<Box<dyn Sleep>>, new_deadline: Instant) { sleep.as_mut().reset(new_deadline); } } // 3. Transport:tokio 的 TcpStream 本身满足 AsyncRead + AsyncWrite // 4. 组装:Builder 同时接收 executor 与 timer let (mut sender, conn) = wreq::conn::http2::Builder::new(TokioExecutor) .timer(TokioTimer) .handshake(stream) .await?; tokio::spawn(conn); // 连接驱动交给执行器运行 let res = sender.try_send_request(req).await?;对应关系一目了然:Builder::new(exec)注入执行器,Builder::timer() 注入定时器(内部包成Arc<dyn Timer + Send + Sync>),handshake(io)接收传输层对象。
三个接口与协议的对应关系
| 协议 | Executor | Timer | Transport 要求 |
|---|---|---|---|
| HTTP/1 | 不需要 | 不需要 | AsyncRead + AsyncWrite即可 |
| HTTP/2 | 需要(驱动连接与升级流) | keep-alive ping 需要 | AsyncRead + AsyncWrite |
| HTTP/3 | 需要(连接任务、body 管道、CONNECT 隧道) | 可选 | 实现 rt::quic::Connection 或用Compat适配 |
自定义运行时时的 3 条经验
- Executor 保持可 Clone、尽量轻量:HTTP/2/HTTP/3 会在每个连接、每个后台任务间传递它的克隆,官方实现都是零成本单位结构体(见 TokioExecutor 定义);
Sleep必须是Send + Sync:compio 的 future 天生!Send,其测试适配器用SendWrapper做了线程检查包装(见 CompioSleep),如果你的运行时也是线程绑定的,需要同样处理;- poll 返回
Pending必须安排唤醒:QUIC 契约明确要求每次Pending后保存后端挂起操作,保证"进度或错误可观测时"当前任务被唤醒(见 rt::quic 模块注释),这是自定义 Transport 最容易出错的地方。
总结
wreq 把"运行时怎么跑、时间怎么算、字节怎么走"这三件事完全交给了你:
- 一个方法的
Executor负责任务调度; - 四个方法的
Timer负责时间推进; - 按协议形态选择的
Transport负责网络字节。
三个接口边界清晰、文档注释详尽(#![deny(missing_docs)]强制全量文档),配合 Cargo.toml 中可选的http3、http3-datagramfeature,你可以用约百行代码把自己熟悉的运行时完整接入这套 Rust HTTP 客户端,真正做到"协议归库、运行时归你"。
【免费下载链接】wreq
An ergonomic, privacy-aware Rust HTTP Client
相关推荐
GraphQL.NET 接口(Interface)完整指南:从接口定义到运行时类型解析
GraphQL.NET 接口(Interface)完整指南:从接口定义到运行时类型解析 导读 本文基于 GraphQL for .NET(graphql dot
后端embassy-executor-timer-queue 源码解析:executor 集成定时器队列的存储与访问接口
embassy executor timer queue 源码解析:executor 集成定时器队列的存储与访问接口 embassy executor time
嵌入式物联网异步编程为 Apereo CAS 打造自定义 Surrogate 认证账户存储:从接口到运行时的完整扩展指南
为 Apereo CAS 打造自定义 Surrogate 认证账户存储:从接口到运行时的完整扩展指南 本文面向需要为 Apereo CAS 设计专属代理认证(S
后端认证鉴权单点登录
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考