首页 > 开源 > wastnet 反向代理实战:一个端口统一托管前端与后端 API

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

OSChina资讯 2026-09-01 20:29 1 阅读 查看原文

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-Encodinggzip、且响应体不小于 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 可以做路径重写、协议升级、超时与转发头:

何时才需要 proxyproxy 解决的是「后端是另一个独立进程 / 端口」时的转发问题。如果后端接口也由同一个 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);

关键点:proxyresource 都是「前缀匹配」,按注册顺序命中即返回。代理路由 /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=nullTRUST_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 协议完全开源、免费使用,欢迎体验与反馈。