☰
wreq 自定义运行时实战:3 大接口打通 Executor、Timer 与 Transport 的完整指南
2026/10/15 17:15:45 网站建设 项目流程

【免费下载链接】wreq

An ergonomic, privacy-aware Rust HTTP Client

项目地址:https://gitcode.com/gh_mirrors/rq/wreq
点击查看免费下载

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)修改已有睡眠未来的截止点有默认实现,建议覆盖

两个容易踩坑的细节:

  1. 返回的是Pin<Box<dyn Sleep>>:Sleep是一个Future<Output = ()> + Send + Sync的 trait(见 Sleep 定义),它额外要求实现reset,让库能原地修改超时而不重建未来;
  2. 忘记传 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)接收传输层对象。

三个接口与协议的对应关系

协议ExecutorTimerTransport 要求
HTTP/1不需要不需要AsyncRead + AsyncWrite即可
HTTP/2需要(驱动连接与升级流)keep-alive ping 需要AsyncRead + AsyncWrite
HTTP/3需要(连接任务、body 管道、CONNECT 隧道)可选实现 rt::quic::Connection 或用Compat适配

自定义运行时时的 3 条经验

  1. Executor 保持可 Clone、尽量轻量:HTTP/2/HTTP/3 会在每个连接、每个后台任务间传递它的克隆,官方实现都是零成本单位结构体(见 TokioExecutor 定义);
  2. Sleep必须是Send + Sync:compio 的 future 天生!Send,其测试适配器用SendWrapper做了线程检查包装(见 CompioSleep),如果你的运行时也是线程绑定的,需要同样处理;
  3. 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

项目地址:https://gitcode.com/gh_mirrors/rq/wreq
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询