wastnet 反向代理实战:一个端口统一托管前端与后端 API
wastnet 是一款零依赖、自研的 Java Web 服务器,核心基于 JDK 原生 NIO 构建 Reactor 多路复用模型,不依赖 Netty、Tomcat 等任何第三方网络库。其 HTTP/2(h2 / h2c)协议栈从 HPACK、Huffman 到 ALPN 均为完全自研实现,是框架的核心特色之一;在基准测试中吞吐对标 Undertow。
本文介绍 wastnet 的路由分发组件 HttpRouterHandler。它是一个高性能、支持链式配置的 HTTP 路由 Handler,其「静态资源托管」与「反向代理」能力可收敛到同一个链式 API 中,是搭建轻量网关的核心能力。本文以一个最常见的真实场景——前端 SPA 单页应用 + 后端 REST API 共用同一个域名和端口——演示如何用 wastnet 搭一个反向代理网关:浏览器访问 http://host:8080/ 拿到前端页面,/api/* 请求被透明转发到后端服务。全程零额外组件、零第三方依赖。
场景
假设我们有一套前后端分离的系统:
- 前端是 Vue/React 之类的 SPA,构建产物在
./dist 目录(含 index.html、JS/CSS);
- 后端是 REST 服务,监听
http://127.0.0.1:9090,对外暴露 /users、/health 等接口;
- 需求:只开一个 8080 端口,根路径
/ 托管前端静态资源,/api/* 反向代理到后端,并自动把 /api 前缀剥离(后端不需要感知 /api)。
用 Nginx 当然也可以,但 Nginx 只负责反向代理与静态资源,后端 REST 服务还得单独起一个进程(至少 Nginx + 后端两个进程);用 wastnet 则前端托管与后端反代都在同一个 Java 进程内完成,且代理、缓存、WebSocket 升级等能力开箱即用。
引入依赖
<dependency>
<groupId>io.github.wycst</groupId>
<artifactId>wastnet-core</artifactId>
<version>1.0.1</version>
</dependency>
零第三方依赖,仅依赖 JDK 本身。
核心实现
1. 静态资源路由:托管前端
HttpResourceRoute 负责静态文件服务,ETag、Last-Modified、304 协商、防目录穿越等由框架处理(GZIP 需显式开启,见本节末尾):
// routePath 为资源挂载路径(会自动叠加 contextPath 作为 base path),docBase 是磁盘根目录
router.resource(new HttpResourceRoute("/", "./dist"));
常用配置(可选):
new HttpResourceRoute("/", "./dist")
// 自定义默认缓存策略为强缓存 1 小时(框架默认是 max-age=0, must-revalidate,即弱缓存、每次 304 校验)
.defaultCacheControl("public, max-age=3600")
// 图片类单独走强缓存:1 年且不可变
.cacheControl("image/*", "public, max-age=31536000, immutable");
GZIP 压缩(默认关闭):wastnet 的响应 GZIP 是「全局开关」,默认 false。开启后,框架会对请求头 Accept-Encoding 含 gzip、且响应体不小于 wastnet.http.gzip-min-size(默认 2KB)的响应自动压缩——静态资源与代理转发响应走同一套逻辑,统一生效。
当前版本(0.0.1)通过 JVM 系统属性开启:
// 启动参数加上:
// -Dwastnet.http.gzip=true 开启 GZIP 响应压缩
// -Dwastnet.http.gzip-min-size=1024 可选:低于 1KB 不压缩(默认 2KB)
下个版本(0.0.2)将支持在代码中通过 HttpOptions 链式配置,例如 HTTPServer.of(8080).option(HttpOptions.GZIP, true).requestHandler(router).start();,无需依赖 JVM 参数。
2. 反向代理:把 /api 转给后端并剥离前缀
HttpRouterHandler.proxy 把指定前缀下的请求转发到后端服务。配合 HttpProxyConfig 可以做路径重写、协议升级、超时与转发头:
何时才需要 proxy:proxy 解决的是「后端是另一个独立进程 / 端口」时的转发问题。如果后端接口也由同一个 wastnet 实例(同一个 8080 端口)直接提供,那么 /api/* 并不需要代理,直接用 HttpRouterHandler.get(...) / post(...) 等路由 handler 处理即可——它们都在同一个进程内,没有跨进程转发。本文演示的是典型的「前端 SPA + 独立后端进程」网关模式,所以才用 proxy 把外部 8080 的 /api/* 转到内部 9090。
// 后端地址,支持 http:// 与 https://
String backend = "http://127.0.0.1:9090";
// 核心:路径重写 /api/users -> /users(剥离 /api 前缀,后端无需感知 /api)
HttpProxyConfig apiProxy = HttpProxyConfig.target(backend)
.replacePrefix("/api", "");
// 注册代理路由:匹配 /api 及其子路径
router.proxy("/api", apiProxy);
关键点:proxy 与 resource 都是「前缀匹配」,按注册顺序命中即返回。代理路由 /api 必须注册在静态资源 / 之前,否则静态资源的 / 前缀会优先匹配 /api/xxx 并返回 404。上面的完整示例遵循了这个顺序。
下个版本(0.0.2)会对此做自动排序优化:注册路由时框架会按前缀的具体程度(specificity)自动排序,更具体的代理前缀(/api)自动排在静态资源(/)之前,开发者无需再手动保证注册顺序。
3. 路径重写的三种姿势
除 replacePrefix 外,HttpProxyConfig 还提供两种重写方式,按需选择:
// 方式一:前缀剥离(最常用)
HttpProxyConfig.target(backend).replacePrefix("/api", ""); // /api/users -> /users
// 方式二:正则替换
HttpProxyConfig.target(backend).replaceRegex("^/api/(.*)$", "/$1"); // 效果同上
// 方式三:整体开关 / 自定义函数
HttpProxyConfig.target(backend).rewrite(true); // 完整透传路径(不改写)
HttpProxyConfig.target(backend).rewrite(path -> path.replaceFirst("^/api", "")); // 等价方式一
关于 contextPath(重要):以上三种重写(含 rewrite(true))操作的对象,都是已经过 HttpRouterHandler 上下文路径匹配、被剥离掉 contextPath 之后的子路径(subPath),而不是客户端请求的原始完整 URI。这一点尤其影响 rewrite(true):
- 如果你的网关配置了非根的
contextPath(例如 new HttpRouterHandler("/app")),那么传入重写函数或被透传的 path 都不含 /app;
rewrite(true) 的「完整透传」指的是透传这个 subPath——它会把发往后端的请求行 URI 覆写为 subPath,从而把 contextPath 一起丢掉(后端收到的是 /api/... 而非 /app/api/...);
- 相比之下,不写
rewrite(...)(默认) 时,框架转发的是含 contextPath 的原始 URI,后端能收到 /app/api/...;
replacePrefix("/api","") / replaceRegex 同样工作在 subPath 上,处理的是 /api 这一段,与 contextPath 无关。
一句话:路径重写只对「去掉 contextPath 之后的路径」生效;需要把 contextPath 也带给后端时,不要使用 rewrite(true),保持默认即可。
4. 转发真实客户端信息
反向代理常见诉求是把客户端 IP、原始协议、原始 Host 带给后端。addHeader 支持 Nginx 风格变量,按请求动态解析:
| 变量 |
含义 |
$remote_addr |
客户端 IP |
$remote_port |
客户端端口 |
$host |
客户端原始 Host 头 |
$scheme |
请求协议(http/https) |
$request_uri |
原始请求 URI(不含 query) |
$server_addr / $server_port |
网关自身 IP / 端口 |
HttpProxyConfig.target(backend)
.addHeader("X-Real-IP", "$remote_addr")
.addHeader("X-Forwarded-For", "$remote_addr")
.addHeader("X-Forwarded-Proto", "$scheme")
.addHeader("X-Forwarded-Host", "$host");
5. 升级、超时与 HTTPS 后端
upgrade(true):开启 WebSocket、h2c 等协议升级代理;
readTimeout(ms) / connectionTimeout(ms):后端读与连接超时;
changeOrigin(true)(默认):把 Host 头改写为后端地址;
- 代理到自签 / 私有 CA 的 HTTPS 后端时:默认
trustManagers=null 即 TRUST_ALL(信任任何证书),且主机名校验仅在同时配置了自定义 trustManagers(...) 时才会启用。因此自签 / 私有 CA 后端在默认配置下即可直接代理,无需额外设置;若你指定了自定义 trustManagers(如只信任某 CA)又想跳过主机名校验,再追加 verifyHostname(false)。
HttpProxyConfig.target("https://127.0.0.1:9443") // 自签 / 私有 CA:默认 TRUST_ALL,可直接代理
.upgrade(true);
6. 404 兜底
router.notFoundHandler((request, response) ->
response.status(404).body("Not Found".getBytes()));
完整可运行示例
把上面的片段拼起来就是一个完整网关。HttpRouterHandler 自身实现 HttpRequestHandler,直接挂到 HTTPServer 即可:
import io.github.wycst.wastnet.http.HTTPServer;
import io.github.wycst.wastnet.http.handler.HttpResourceRoute;
import io.github.wycst.wastnet.http.handler.HttpRouterHandler;
import io.github.wycst.wastnet.http.proxy.HttpProxyConfig;
public class ReverseProxyDemo {
public static void main(String[] args) {
String backend = "http://127.0.0.1:9090"; // 后端 REST 服务
String docBase = "./dist"; // 前端构建产物目录
HttpRouterHandler router = new HttpRouterHandler();
// 1) 反向代理:/api/* 转发到后端,并剥离 /api 前缀(必须在静态资源之前注册)
HttpProxyConfig apiProxy = HttpProxyConfig.target(backend)
.replacePrefix("/api", "")
.upgrade(true)
.readTimeout(5000)
.addHeader("X-Real-IP", "$remote_addr")
.addHeader("X-Forwarded-For", "$remote_addr")
.addHeader("X-Forwarded-Proto", "$scheme")
.addHeader("X-Forwarded-Host", "$host");
router.proxy("/api", apiProxy);
// 2) 静态资源:前端 SPA 挂在根路径
router.resource(new HttpResourceRoute("/", docBase)
.defaultCacheControl("public, max-age=3600")
.cacheControl("image/*", "public, max-age=31536000, immutable"));
// 3) 兜底 404
router.notFoundHandler((request, response) ->
response.status(404).body("Not Found".getBytes()));
// 启动网关(如需 HTTPS,链式加 .pemSSL("cert/cert.pem", "cert/server.pem") 即可)
// 启动网关(GZIP 用 JVM 参数 -Dwastnet.http.gzip=true 开启;如需 HTTPS,链式加 .pemSSL("cert/cert.pem", "cert/server.pem") 即可)
HTTPServer.of(8080).requestHandler(router).start();
System.out.println("Gateway started: http://localhost:8080/ (API -> " + backend + ")");
}
}
仓库 wastnet-test 模块下已有等价的可运行参考实现:
wastnet-test/src/main/java/io/github/wycst/wastnet/examples/http/MiniNginx.java
启动后端(任意监听 9090 的 REST 服务)和上面的网关后用 curl 验证:
# 1) 前端首页(静态资源,命中 /)
curl -i http://localhost:8080/
# 2) 代理一个接口:/api/health 被转发到后端 /health
curl http://localhost:8080/api/health
# 3) 强缓存头(图片类资源)
curl -I http://localhost:8080/assets/logo.png
# 4) 后端会收到透传的客户端信息头(见第 4 节「转发真实客户端信息」)
# X-Real-IP: <client-ip>
# X-Forwarded-For: <client-ip>
# X-Forwarded-Proto: http
# X-Forwarded-Host: localhost:8080
小结
用 wastnet 的 HttpRouterHandler 搭反向代理网关,核心要点:
- 一个端口两种职责:
proxy 做反向代理、resource 做静态托管,链式组合即可;
- 路径重写优先用
replacePrefix / replaceRegex,比手写函数更直观;
- 透传客户端信息用
$remote_addr、$scheme 等内置变量,无需自己解析;
- 升级与超时开箱即用:
.upgrade(true) 支持 WebSocket/h2c,.readTimeout/.connectionTimeout 控制后端连接。
相关链接
wastnet 基于 Apache 2.0 协议完全开源、免费使用,欢迎体验与反馈。