Skip to content

Commit 4c8a77f

Browse files
feat(quic): 合并 QUIC/HTTP/3 文档与示例到 main (#141)
* fix(server): 启用 tokio macros 支持 select * refactor(service): 调整证书加载逻辑 * refactor(service): 支持连接downcast能力 * feat(quic): 新增quic特性与模块,提供HTTP/3/WebTransport与Alt-Svc集成,以及统一run_server接口 * refactor(quic): 移除quic_available动态检测机制 - 移除AltSvcMiddleware中的quic_available参数,始终添加alt-svc头 - 简化HybridService,移除quic_available状态管理 - 清理QuicEndpointListener中未使用的quic_available字段 - 移除service.rs中的状态更新逻辑 - 添加Listener::tls_with_cert便捷方法 后台运行成功时h3必然生效,无需动态检测 * refactor(quic): 重构QUIC启动方式,统一HTTP和QUIC接口 - Route新增with_quic_port()方法,自动添加AltSvcMiddleware - Route的ConnectionService实现支持自动识别和处理QUIC连接 - AltSvcMiddleware改为公开API,可手动hook使用 - 移除run_server封装函数和HybridService - 简化service.rs,移除quic_port参数传递 - 新增Route::handle_http_connection辅助方法 BREAKING CHANGE: 移除quic::run_server函数,现在使用Server::new().listen().serve()统一接口 * feat(quic): add HybridListener for automatic HTTP fallback - Add HybridListener struct containing QUIC and HTTP TLS listeners - Add with_http_fallback() method to QuicEndpointListener - Listen on UDP(QUIC) and TCP(HTTP) on the same port - Use tokio::select! to accept both connection types - Export HybridListener to public API Now only one listener is needed to support both QUIC and HTTP fallback * refactor(quic): store CertificateStore in QuicEndpointListener - Add Clone derive to CertificateStore - Store CertificateStore in QuicEndpointListener - Remove store parameter from with_http_fallback() - Simplify API usage - no need to pass store twice Breaking Change: with_http_fallback() now takes no parameters * feat(examples): 添加QUIC/HTTP3示例与文档 (#140) * feat(examples): 添加QUIC/HTTP3示例并启用同端口HTTPS回退 提供最小可运行示例:with_quic_port下发Alt-Svc,QuicEndpointListener混合监听 运行方式:cargo run -p example-quic,curl --http3 验证 * docs(requirements): 增加QUIC示例需求整理 记录示例目标、范围、依赖与运行方式,确保需求对齐 * style: 优化代码格式和导入顺序,移除未使用的文档
1 parent 7698c21 commit 4c8a77f

File tree

18 files changed

+1059
-12
lines changed

18 files changed

+1059
-12
lines changed

deny.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ exceptions = [
117117
# Each entry is the crate and version constraint, and its specific allow
118118
# list
119119
#{ allow = ["Zlib"], crate = "adler32" },
120+
{ allow = ["CDLA-Permissive-2.0"], crate = "webpki-root-certs" },
120121
]
121122

122123
# Some crates don't have (easily) machine readable licensing information,

docs/quic-webtransport.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# QUIC / HTTP/3 / WebTransport 使用与编写说明
2+
3+
> 状态:实验性(feature 可选,默认未启用)。当前已支持基于 `quinn + h3` 的 HTTP/3 与 WebTransport 基础能力,推荐在测试或内网环境先行验证。
4+
5+
## 功能概览
6+
7+
- QUIC 监听与同端口回退:UDP QUIC 与 TCP HTTPS 共享同一端口(混合监听)。
8+
- HTTP/3(h3):常规请求-响应模型已打通,可与现有路由/中间件协同。
9+
- Alt-Svc:通过中间件向客户端宣告 h3 可用,浏览器可自动升级。
10+
- WebTransport:已开启握手与数据帧收发,内置 Echo 示例(会话 ID 使用 `scru128` 生成)。
11+
12+
## 前置要求
13+
14+
- Rust 1.77+ 与 Tokio 运行时。
15+
- 证书(PEM 或 DER),建议准备一套本地开发证书(仓库已提供示例)。
16+
- 客户端需支持 HTTP/3(如 curl --http3、现代浏览器)。
17+
18+
## 启用 QUIC 特性
19+
20+
- 在你的可执行包或示例中,启用 `silent``quic` feature:
21+
22+
- Cargo.toml
23+
```toml
24+
[dependencies]
25+
silent = { path = "../../silent", features = ["quic"] }
26+
```
27+
28+
> 工作区示例可参考:`examples/quic/Cargo.toml`
29+
30+
## 证书准备
31+
32+
- 使用 `CertificateStore` 构建 rustls 服务器配置,并设置 ALPN:库内部会自动为 QUIC 配置 `h3`/`h3-29`
33+
- 你可以直接复用示例证书:`examples/tls/certs/localhost+2.pem``localhost+2-key.pem`
34+
35+
- 代码示例
36+
```rust
37+
fn certificate_store() -> anyhow::Result<silent::CertificateStore> {
38+
let builder = silent::CertificateStore::builder()
39+
.cert_path("./examples/tls/certs/localhost+2.pem")
40+
.key_path("./examples/tls/certs/localhost+2-key.pem");
41+
Ok(builder.build()?)
42+
}
43+
```
44+
45+
> 若使用自签证书,浏览器需要手动信任根证书;curl 可使用 `-k` 跳过校验进行快速验证。
46+
47+
## 启动服务(QUIC + HTTPS 回退)
48+
49+
- 使用 `QuicEndpointListener::new(...).with_http_fallback()` 创建混合监听;
50+
- 通过 `Route::with_quic_port(port)` 自动添加 Alt-Svc;
51+
52+
- 最小示例(与 `examples/quic/src/main.rs` 一致):
53+
```rust
54+
use anyhow::{Result, anyhow};
55+
use silent::prelude::*;
56+
use silent::QuicEndpointListener;
57+
use tracing_subscriber::EnvFilter;
58+
59+
#[tokio::main]
60+
async fn main() -> Result<()> {
61+
init_tracing();
62+
install_rustls_provider()?;
63+
64+
let routes = build_routes().with_quic_port(4433); // 自动添加 Alt-Svc
65+
66+
let bind_addr: std::net::SocketAddr = "127.0.0.1:4433".parse().unwrap();
67+
let store = certificate_store()?;
68+
69+
let listener = QuicEndpointListener::new(bind_addr, &store).with_http_fallback();
70+
Server::new().listen(listener).serve(routes).await;
71+
Ok(())
72+
}
73+
74+
fn install_rustls_provider() -> Result<()> {
75+
rustls::crypto::ring::default_provider()
76+
.install_default()
77+
.map_err(|_| anyhow!("初始化 Rustls 加密提供者失败"))
78+
}
79+
80+
fn build_routes() -> Route {
81+
async fn index(_req: Request) -> silent::Result<&'static str> {
82+
Ok("Hello from HTTP/3")
83+
}
84+
let mut root = Route::new_root();
85+
root.push(Route::new("").get(index));
86+
root
87+
}
88+
```
89+
90+
## Alt-Svc(HTTP/3 升级宣告)
91+
92+
- `with_quic_port(port)` 会自动挂载 Alt-Svc 中间件,效果等价于:
93+
`Alt-Svc: h3=":4433"; ma=86400`
94+
95+
> 注意:浏览器通常会先通过 HTTPS 访问一次,收到 Alt-Svc 后在后续请求中升级到 H3。
96+
97+
## 客户端验证
98+
99+
- curl:
100+
```bash
101+
curl --http3 -k https://127.0.0.1:4433/
102+
```
103+
104+
- 浏览器:
105+
- 访问 `https://127.0.0.1:4433/`
106+
- 打开 DevTools 网络面板,确认协议列为 `h3`
107+
- 首次访问可能仍为 `http/2`,刷新后升级。
108+
109+
## WebTransport 使用说明(实验)
110+
111+
- 能力:
112+
- 服务端已启用 WebTransport 握手(扩展 CONNECT)、DATAGRAM;
113+
- 内置 Echo 处理器用于演示:收到数据即回发,便于验证链路。
114+
115+
- 设计要点:
116+
- 会话标识使用 `scru128` 生成,满足高可用 ID 需求;
117+
- API 当前未开放自定义 `WebTransportHandler` 的对外注册接口(默认 Echo)。
118+
119+
- 客户端验证思路:
120+
- 使用支持 WebTransport 的浏览器/JS API 或测试工具,发起 CONNECT + 发送数据帧;
121+
- 期望收到 `echo(webtransport): <your_message>` 响应。
122+
123+
- 可扩展建议(未来 API 方向):
124+
-`Route` 上提供 `with_webtransport(path, handler)` 用于注册自定义处理器;
125+
- 增加鉴权、中止条件、并发/帧大小/会话上限等保护;
126+
- 暴露 QUIC/H3 参数(如并发流、超时、拥塞控制)并纳入配置。
127+
128+
> 如需自定义 Handler,目前需要修改库内部 `quic::service` 中默认的 Echo 注册逻辑。
129+
130+
## 性能与安全建议
131+
132+
- 请求体大小与内存:HTTP/3 示例实现会将请求体聚合到内存,生产环境建议改为流式处理并设置上限。
133+
- 证书与信任:本地自签证书仅用于开发测试,生产环境请使用可信 CA 证书并妥善保管私钥。
134+
- 观测:配合 `tracing`/metrics 输出关键事件(握手、升级、错误、会话数量)。
135+
136+
## 常见问题
137+
138+
- 浏览器未升级到 H3:
139+
- 确认已返回 Alt-Svc;
140+
- 刷新后查看协议列;
141+
- 证书是否被信任(或使用域名而非 IP)。
142+
- QUIC 无法建立:
143+
- 端口是否被占用;
144+
- 防火墙/UDP 是否放行;
145+
- 证书/私钥路径是否正确。
146+
147+
## 参考示例
148+
149+
- 示例入口:`examples/quic/src/main.rs`
150+
- 证书样例:`examples/tls/certs/`

examples/quic/Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "example-quic"
3+
version = "0.1.0"
4+
edition.workspace = true
5+
publish = false
6+
7+
[dependencies]
8+
anyhow = "1"
9+
silent = { path = "../../silent", features = ["quic"] }
10+
tokio = { version = "1", features = ["full"] }
11+
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
12+
rustls = { version = "0.23" }

examples/quic/src/main.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use anyhow::{Result, anyhow};
2+
use silent::QuicEndpointListener;
3+
use silent::prelude::*;
4+
use tracing_subscriber::EnvFilter;
5+
6+
#[tokio::main]
7+
async fn main() -> Result<()> {
8+
init_tracing();
9+
install_rustls_provider()?;
10+
11+
let routes = build_routes().with_quic_port(4433); // 自动添加 Alt-Svc 中间件
12+
13+
// 端口与绑定地址由用户显式设置
14+
let bind_addr: std::net::SocketAddr = "127.0.0.1:4433".parse().unwrap();
15+
let store = certificate_store()?;
16+
17+
// QUIC listener with HTTP fallback (自动附加 HTTP/1.1 + TLS listener)
18+
let listener = QuicEndpointListener::new(bind_addr, &store).with_http_fallback();
19+
20+
Server::new().listen(listener).serve(routes).await;
21+
22+
Ok(())
23+
}
24+
25+
fn init_tracing() {
26+
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
27+
tracing_subscriber::fmt()
28+
.with_env_filter(env_filter)
29+
.with_target(false)
30+
.with_thread_ids(true)
31+
.compact()
32+
.init();
33+
}
34+
35+
fn install_rustls_provider() -> Result<()> {
36+
rustls::crypto::ring::default_provider()
37+
.install_default()
38+
.map_err(|_| anyhow!("初始化 Rustls 加密提供者失败"))
39+
}
40+
41+
// 为示例提供一个证书加载方法:复用 tls 示例的本地证书
42+
fn certificate_store() -> Result<silent::CertificateStore> {
43+
let builder = silent::CertificateStore::builder()
44+
.cert_path("./examples/tls/certs/localhost+2.pem")
45+
.key_path("./examples/tls/certs/localhost+2-key.pem");
46+
builder.build().map_err(Into::into)
47+
}
48+
49+
fn build_routes() -> Route {
50+
async fn index(_req: Request) -> silent::Result<&'static str> {
51+
Ok("Hello from HTTP/3")
52+
}
53+
54+
let mut root = Route::new_root();
55+
root.push(Route::new("").get(index));
56+
root
57+
}

silent/Cargo.toml

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ full = [
3737
"scheduler",
3838
"grpc",
3939
"tls",
40+
"quic",
4041
]
4142
multipart = [
4243
"tokio/fs",
@@ -46,7 +47,13 @@ multipart = [
4647
"dep:textnonce",
4748
]
4849
security = ["dep:argon2", "dep:pbkdf2", "dep:aes-gcm", "dep:aes", "dep:rsa"]
49-
server = ["tokio/fs", "tokio/net", "tokio/rt-multi-thread", "tokio/signal"]
50+
server = [
51+
"tokio/fs",
52+
"tokio/net",
53+
"tokio/rt-multi-thread",
54+
"tokio/signal",
55+
"tokio/macros",
56+
]
5057
session = ["cookie", "dep:async-session"]
5158
sse = ["dep:pin-project", "dep:tokio-stream"]
5259
static = ["tokio/fs", "dep:urlencoding", "dep:async-compression"]
@@ -62,7 +69,21 @@ grpc = [
6269
]
6370
scheduler = ["dep:cron"]
6471
test = ["tokio/macros", "tokio/rt"]
65-
tls = ["dep:tokio-rustls"]
72+
tls = [
73+
"dep:tokio-rustls",
74+
"dep:rustls",
75+
"dep:rustls-pemfile",
76+
"dep:rustls-pki-types",
77+
"server",
78+
]
79+
quic = [
80+
"tls",
81+
"dep:quinn",
82+
"dep:h3",
83+
"dep:h3-quinn",
84+
"dep:scru128",
85+
"dep:rand",
86+
]
6687

6788
[dependencies]
6889
# Basic dependencies
@@ -121,7 +142,9 @@ cookie = { version = "0.18", features = [
121142
], optional = true }
122143

123144
# Grpc
124-
tonic = { version = "0.14", optional = true, default-features = false, features = ["codegen"] }
145+
tonic = { version = "0.14", optional = true, default-features = false, features = [
146+
"codegen",
147+
] }
125148

126149
# Security
127150
aes = { version = "0.8", optional = true }
@@ -136,6 +159,18 @@ tokio-rustls = { version = "0.26", optional = true, default-features = false, fe
136159
"logging",
137160
"tls12",
138161
] }
162+
rustls = { version = "0.23", optional = true, default-features = false, features = [
163+
"logging",
164+
"ring",
165+
"std",
166+
] }
167+
rustls-pemfile = { version = "2", optional = true }
168+
rustls-pki-types = { version = "1", optional = true }
169+
quinn = { version = "0.11", optional = true }
170+
h3 = { version = "0.0.8", optional = true }
171+
h3-quinn = { version = "0.0.10", optional = true }
172+
scru128 = { version = "4.0.0-beta.1", optional = true, package = "scru128" }
173+
rand = { version = "0.8", optional = true }
139174

140175
# Cloudflare Workers
141176
worker = { version = "0.6", optional = true }

silent/src/lib.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,13 @@ pub use crate::middleware::{MiddleWareHandler, middlewares};
4747
#[cfg(feature = "server")]
4848
pub use crate::protocol::Protocol;
4949
#[cfg(feature = "server")]
50-
pub use crate::service::connection::Connection;
50+
pub use crate::service::connection::{BoxedConnection, Connection};
5151
#[cfg(feature = "server")]
52-
pub use crate::service::listener::{Listen, Listener, Listeners, ListenersBuilder};
52+
pub use crate::service::listener::{AcceptFuture, Listen, Listener, Listeners, ListenersBuilder};
5353
#[cfg(feature = "server")]
54-
pub use crate::service::{BoxError, BoxedConnection, ConnectionFuture, ConnectionService, Server};
54+
pub use crate::service::{BoxError, ConnectionFuture, ConnectionService, Server};
55+
#[cfg(all(feature = "server", feature = "tls"))]
56+
pub use crate::service::{CertificateStore, CertificateStoreBuilder};
5557
pub use error::SilentError;
5658
pub use error::SilentResult as Result;
5759
pub use handler::Handler;
@@ -60,3 +62,7 @@ pub use headers;
6062
pub use hyper::{Method, StatusCode, header};
6163
#[cfg(feature = "scheduler")]
6264
pub use scheduler::{ProcessTime, SCHEDULER, Scheduler, SchedulerExt, Task};
65+
#[cfg(feature = "quic")]
66+
pub mod quic;
67+
#[cfg(feature = "quic")]
68+
pub use quic::{HybridListener, QuicEndpointListener};

silent/src/quic/connection.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
use std::pin::Pin;
2+
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
3+
4+
pub struct QuicConnection {
5+
connecting: Option<quinn::Incoming>,
6+
}
7+
impl QuicConnection {
8+
pub(crate) fn new(connecting: quinn::Incoming) -> Self {
9+
Self {
10+
connecting: Some(connecting),
11+
}
12+
}
13+
pub(crate) fn into_incoming(mut self) -> quinn::Incoming {
14+
self.connecting.take().expect("connecting available")
15+
}
16+
}
17+
impl AsyncRead for QuicConnection {
18+
fn poll_read(
19+
self: Pin<&mut Self>,
20+
_cx: &mut std::task::Context<'_>,
21+
buf: &mut ReadBuf<'_>,
22+
) -> std::task::Poll<std::io::Result<()>> {
23+
buf.clear();
24+
std::task::Poll::Ready(Ok(()))
25+
}
26+
}
27+
impl AsyncWrite for QuicConnection {
28+
fn poll_write(
29+
self: Pin<&mut Self>,
30+
_cx: &mut std::task::Context<'_>,
31+
_buf: &[u8],
32+
) -> std::task::Poll<std::io::Result<usize>> {
33+
std::task::Poll::Ready(Ok(0))
34+
}
35+
fn poll_flush(
36+
self: Pin<&mut Self>,
37+
_cx: &mut std::task::Context<'_>,
38+
) -> std::task::Poll<std::io::Result<()>> {
39+
std::task::Poll::Ready(Ok(()))
40+
}
41+
fn poll_shutdown(
42+
self: Pin<&mut Self>,
43+
_cx: &mut std::task::Context<'_>,
44+
) -> std::task::Poll<std::io::Result<()>> {
45+
std::task::Poll::Ready(Ok(()))
46+
}
47+
}

0 commit comments

Comments
 (0)