找回密码
 立即注册
搜索
查看: 93|回复: 5

wastnet 反向代理实战:一个端口统一托管前端与后端 API

[复制链接]

主题

0

回帖

0

积分

积分
0
发表于 2026-9-4 12:30:18 | 显示全部楼层 |阅读模式
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 升级等能力开箱即用。
引入依赖

  1. <dependency>
  2.     <groupId>io.github.wycst</groupId>
  3.     <artifactId>wastnet-core</artifactId>
  4.     <version>1.0.1</version>
  5. </dependency>
复制代码

零第三方依赖,仅依赖 JDK 本身。
核心实现
1. 静态资源路由:托管前端

HttpResourceRoute 负责静态文件服务,ETag、Last-Modified、304 协商、防目录穿越等由框架处理(GZIP 需显式开启,见本节末尾):

  1. // routePath 为资源挂载路径(会自动叠加 contextPath 作为 base path),docBase 是磁盘根目录
  2. router.resource(new HttpResourceRoute("/", "./dist"));
复制代码

常用配置(可选):

  1. new HttpResourceRoute("/", "./dist")
  2.         // 自定义默认缓存策略为强缓存 1 小时(框架默认是 max-age=0, must-revalidate,即弱缓存、每次 304 校验)
  3.         .defaultCacheControl("public, max-age=3600")
  4.         // 图片类单独走强缓存:1 年且不可变
  5.         .cacheControl("image/*", "public, max-age=31536000, immutable");
复制代码

GZIP 压缩(默认关闭):wastnet 的响应 GZIP 是「全局开关」,默认 false。开启后,框架会对请求头 Accept-Encoding 含 gzip、且响应体不小于 wastnet.http.gzip-min-size(默认 2KB)的响应自动压缩——静态资源与代理转发响应走同一套逻辑,统一生效

当前版本(0.0.1)通过 JVM 系统属性开启:

  1. // 启动参数加上:
  2. //   -Dwastnet.http.gzip=true                 开启 GZIP 响应压缩
  3. //   -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。

  1. // 后端地址,支持 http:// 与 https://
  2. String backend = "http://127.0.0.1:9090";

  3. // 核心:路径重写 /api/users -> /users(剥离 /api 前缀,后端无需感知 /api)
  4. HttpProxyConfig apiProxy = HttpProxyConfig.target(backend)
  5.         .replacePrefix("/api", "");

  6. // 注册代理路由:匹配 /api 及其子路径
  7. router.proxy("/api", apiProxy);
复制代码
关键点:proxy 与 resource 都是「前缀匹配」,按注册顺序命中即返回。代理路由 /api 必须注册在静态资源 / 之前,否则静态资源的 / 前缀会优先匹配 /api/xxx 并返回 404。上面的完整示例遵循了这个顺序。

下个版本(0.0.2)会对此做自动排序优化:注册路由时框架会按前缀的具体程度(specificity)自动排序,更具体的代理前缀(/api)自动排在静态资源(/)之前,开发者无需再手动保证注册顺序。
3. 路径重写的三种姿势

除 replacePrefix 外,HttpProxyConfig 还提供两种重写方式,按需选择:

  1. // 方式一:前缀剥离(最常用)
  2. HttpProxyConfig.target(backend).replacePrefix("/api", "");     // /api/users -> /users

  3. // 方式二:正则替换
  4. HttpProxyConfig.target(backend).replaceRegex("^/api/(.*)$", "/$1"); // 效果同上

  5. // 方式三:整体开关 / 自定义函数
  6. HttpProxyConfig.target(backend).rewrite(true);                // 完整透传路径(不改写)
  7. 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 / 端口


  1. HttpProxyConfig.target(backend)
  2.         .addHeader("X-Real-IP", "$remote_addr")
  3.         .addHeader("X-Forwarded-For", "$remote_addr")
  4.         .addHeader("X-Forwarded-Proto", "$scheme")
  5.         .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)。


  1. HttpProxyConfig.target("https://127.0.0.1:9443")   // 自签 / 私有 CA:默认 TRUST_ALL,可直接代理
  2.         .upgrade(true);
复制代码
6. 404 兜底

  1. router.notFoundHandler((request, response) ->
  2.         response.status(404).body("Not Found".getBytes()));
复制代码
完整可运行示例

把上面的片段拼起来就是一个完整网关。HttpRouterHandler 自身实现 HttpRequestHandler,直接挂到 HTTPServer 即可:

  1. import io.github.wycst.wastnet.http.HTTPServer;
  2. import io.github.wycst.wastnet.http.handler.HttpResourceRoute;
  3. import io.github.wycst.wastnet.http.handler.HttpRouterHandler;
  4. import io.github.wycst.wastnet.http.proxy.HttpProxyConfig;

  5. public class ReverseProxyDemo {
  6.     public static void main(String&#91;&#93; args) {
  7.         String backend = "http://127.0.0.1:9090";   // 后端 REST 服务
  8.         String docBase = "./dist";                   // 前端构建产物目录

  9.         HttpRouterHandler router = new HttpRouterHandler();

  10.         // 1) 反向代理:/api/* 转发到后端,并剥离 /api 前缀(必须在静态资源之前注册)
  11.         HttpProxyConfig apiProxy = HttpProxyConfig.target(backend)
  12.                 .replacePrefix("/api", "")
  13.                 .upgrade(true)
  14.                 .readTimeout(5000)
  15.                 .addHeader("X-Real-IP", "$remote_addr")
  16.                 .addHeader("X-Forwarded-For", "$remote_addr")
  17.                 .addHeader("X-Forwarded-Proto", "$scheme")
  18.                 .addHeader("X-Forwarded-Host", "$host");
  19.         router.proxy("/api", apiProxy);

  20.         // 2) 静态资源:前端 SPA 挂在根路径
  21.         router.resource(new HttpResourceRoute("/", docBase)
  22.                 .defaultCacheControl("public, max-age=3600")
  23.                 .cacheControl("image/*", "public, max-age=31536000, immutable"));

  24.         // 3) 兜底 404
  25.         router.notFoundHandler((request, response) ->
  26.                 response.status(404).body("Not Found".getBytes()));

  27.         // 启动网关(如需 HTTPS,链式加 .pemSSL("cert/cert.pem", "cert/server.pem") 即可)
  28.         // 启动网关(GZIP 用 JVM 参数 -Dwastnet.http.gzip=true 开启;如需 HTTPS,链式加 .pemSSL("cert/cert.pem", "cert/server.pem") 即可)
  29.         HTTPServer.of(8080).requestHandler(router).start();
  30.         System.out.println("Gateway started: http://localhost:8080/  (API -> " + backend + ")");
  31.     }
  32. }
复制代码

仓库 wastnet-test 模块下已有等价的可运行参考实现:

• wastnet-test/src/main/java/io/github/wycst/wastnet/examples/http/MiniNginx.java


启动后端(任意监听 9090 的 REST 服务)和上面的网关后用 curl 验证:

  1. # 1) 前端首页(静态资源,命中 /)
  2. curl -i http://localhost:8080/

  3. # 2) 代理一个接口:/api/health 被转发到后端 /health
  4. curl http://localhost:8080/api/health

  5. # 3) 强缓存头(图片类资源)
  6. curl -I http://localhost:8080/assets/logo.png

  7. # 4) 后端会收到透传的客户端信息头(见第 4 节「转发真实客户端信息」)
  8. #    X-Real-IP: <client-ip>
  9. #    X-Forwarded-For: <client-ip>
  10. #    X-Forwarded-Proto: http
  11. #    X-Forwarded-Host: localhost:8080
复制代码
小结

用 wastnet 的 HttpRouterHandler 搭反向代理网关,核心要点:

一个端口两种职责:proxy 做反向代理、resource 做静态托管,链式组合即可;
路径重写优先用 replacePrefix / replaceRegex,比手写函数更直观;
透传客户端信息用 $remote_addr、$scheme 等内置变量,无需自己解析;
升级与超时开箱即用:.upgrade(true) 支持 WebSocket/h2c,.readTimeout/.connectionTimeout 控制后端连接。

相关链接

• Gitee:https://gitee.com/xiaoch0209/wastnet
• GitHub:https://github.com/wycst02/wastnet


wastnet 基于 Apache 2.0 协议完全开源、免费使用,欢迎体验与反馈。

原文链接

打赏作者

当前余额:0 Token,打赏后立即到账

主题

0

回帖

0

积分

积分
0
发表于 2026-9-4 12:45:01 | 显示全部楼层
写得不错,wastnet那部分说到点子上了,顶一个。

主题

0

回帖

0

积分

积分
0
发表于 2026-9-4 15:45:01 | 显示全部楼层
先马后看,API的资料太散了,这篇梳理得挺清楚。

主题

0

回帖

0

积分

积分
0
发表于 2026-9-4 18:45:01 | 显示全部楼层
深度好文,就是反向代理实战那块还想看个具体例子,期待下篇。

主题

0

回帖

0

积分

积分
0
发表于 2026-9-4 20:45:01 | 显示全部楼层
看完受益,已转给同事,让他们也看看API这块。

主题

0

回帖

0

积分

积分
0
发表于 2026-9-4 21:45:01 | 显示全部楼层
看完受益,已转给同事,让他们也看看反向代理实战这块。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

{ template common/footer}