跳转到内容

JQuick Hub

JQuick Hub 是基于 Netty 4 的单公网端口协议嗅探 TCP 网关,适用于公网端口受限、需要单端口同时对外提供 Web 服务与 TCP 业务的场景。

JQuick Hub 在同一个监听端口上自动识别 HTTP、HTTPS-TLS 与其他 TCP 流量:HTTP/HTTPS 流量原样转发至内网 Nginx,其余 TCP 流量转发至可配置的目标地址,并可选用固定长度前缀 Token 完成鉴权。

它解决的痛点很直接:公网端口资源有限(或安全策略只允许开放一个端口),但需要同时对外提供 Web 服务与数据库等 TCP 业务。 传统方案需要部署多个反向代理或为每类业务单独协商端口,JQuick Hub 用协议嗅探把多类流量收敛到一个端口上。

技术底座与工程形态:

  • Java 1.8+,Netty 4.1.92.Final,snakeyaml 2.3,slf4j 1.7.36,lombok 1.18.30
  • 单模块 Maven 工程,坐标 com.gateway:hub:1.0.0
  • mvn clean package 产出 fat-jar target/tcp-gateway.jar(Main-Class=com.gateway.JQuickGatewayServer)

典型适配场景:

场景 说明
单端口复用 公网仅开放一个端口,同时承载 Web(HTTP/HTTPS)与数据库等 TCP 业务
内网统一入口 作为内网 Nginx 与内部 TCP 服务的统一公网入口,按协议自动分流
私有 TCP 协议接入 非 HTTP 的私有协议经前缀 Token 鉴权后接入内网服务,公网侧不暴露明文业务端口

环境要求:JDK 8+(源码编译运行);或 Docker(镜像运行时基于 eclipse-temurin:8-jre-jammy)。

  • 单端口多协议嗅探:一个监听端口同时承载 HTTP、HTTPS-TLS 与其他 TCP 流量;首字节 0x16 识别 TLS 握手(RFC 5246),方法名前缀加空格识别 HTTP,16 字节探测窗口兜底判定。
  • 零解析字节透传:不解析业务协议内容,TCP 字节流原样转发,对业务零侵入。
  • 前缀 Token 认证:固定长度 Token 拼在首包最前,MessageDigest.isEqual 常量时间比对防时序侧信道,日志脱敏输出;仅作用于非 HTTP/HTTPS 流量。
  • 零丢包与背压:后端未就绪期间数据暂存 buffered,就绪后一次性刷出;对端不可写时暂停本端读取,可写后自动恢复。
  • 断连联动关闭:任一端 inactive、写失败或异常,双端同时关闭,不留半开连接。
  • 环境变量全覆盖:全部配置项均可由 JQUICK_* 环境变量覆盖,优先级为「环境变量 > YAML > 代码默认值」。
  • 云原生容器化:多阶段 Dockerfile(Maven 构建 + 纯 JRE 运行)、docker-compose.yaml 开箱即用,单条端口映射同时暴露 HTTP/HTTPS 与 TCP。
  • 优雅停机与可观测:ShutdownHook 收到 SIGTERM/SIGINT 后对 bossGroup/workerGroup 执行优雅关闭;Netty 缓冲区泄漏检测级别可配(SIMPLE / ADVANCED / PARANOID / DISABLED)。
┌─────────────┐ HTTP/HTTPS ┌──────────────────────────────┐ ┌───────────────┐
│ Web 客户端 │─────────────▶│ │───────────▶│ 内网 Nginx │
└─────────────┘ │ JQuick Hub 单端口(:18080) │ │ nginx.target │
┌─────────────┐ 其他 TCP │ 协议嗅探 → 可选Token认证→透传 │ └───────────────┘
│ TCP 客户端 │─────────────▶│ │───────────▶┌───────────────┐
└─────────────┘ └──────────────────────────────┘ │ 指定后端服务 │
│ other.target │
└───────────────┘

入站连接依次经过三个 Handler,按嗅探结果分流:

入站连接
│
▼
JQuickProtocolDetectHandler # 协议嗅探:TLS(0x16) / HTTP(方法名+空格) / OTHER(≥16字节无匹配)
│
├── HTTP / TLS ─────────────────────────────▶ JQuickTcpRelayHandler ──▶ nginx.target
│
└── OTHER ──▶ JQuickAuthPrefixHandler ──▶ JQuickTcpRelayHandler ──▶ other.traffic.target
(仅 auth.enable=true 时加入流水线)

JQuickProtocolDetectHandler 的关键常量:

常量 值 说明
TLS_HANDSHAKE_FIRST_BYTE 0x16 TLS 握手首字节(RFC 5246)
PROBE_MAX_BYTES 16 协议识别窗口(字节)
MAX_PROBE_BUFFER 64KB 探测缓冲上限
HTTP_METHODS GET / POST / PUT / DELETE / HEAD / OPTIONS / PATCH / CONNECT / TRACE 内置 HTTP 方法名集合

判定规则(按优先级):

  1. 首字节 == 0x16 → 判定为 TLS(HTTPS),1 字节即可判定;
  2. 出现「方法名前缀 + 紧跟空格 0x20」→ 判定为 HTTP,大小写不敏感;拆包导致方法名不完整时等待后续字节再判;
  3. 累积 ≥ 16 字节 仍无匹配 → 判定为 OTHER(普通 TCP 流量)。

probe_timeout_ms(默认取 so_timeout 的值 5000ms;so_timeout ≤ 0 时回退 5000):

  • 超时前已收到数据 → 按 OTHER 转发;
  • 超时仍未收到任何数据 → 关闭连接。
  • HTTP / TLS → nginx.target(代码默认 127.0.0.1:80);
  • OTHER → other.traffic.target(代码默认 127.0.0.1:9000);鉴权开启时先经 JQuickAuthPrefixHandler 校验;
  • 前缀 Token 认证仅对 OTHER 流量生效,HTTP/HTTPS 永不触发;enable=false 时 OTHER 流量完全透传。
Terminal window
mvn clean package # 产物:target/tcp-gateway.jar(fat-jar)
java -jar target/tcp-gateway.jar # 按默认查找顺序加载配置
java -jar target/tcp-gateway.jar /path/to/gateway.yaml # 命令行参数指定配置文件路径

配置文件查找顺序(优先级从高到低):

  1. 命令行参数 arg
  2. 环境变量 GATEWAY_CONFIG
  3. 工作目录 ./gateway.yaml
  4. classpath 内置 /gateway.yaml

构建镜像:

Terminal window
docker build -t jquick-hub:1.0.0 .

启动容器(环境变量覆盖配置):

Terminal window
docker run -d --name jquick-hub --restart unless-stopped \
-p 18080:18080 \
-e JQUICK_LISTEN_PORT=18080 \
-e JQUICK_NGINX_HOST=host.docker.internal -e JQUICK_NGINX_PORT=52000 \
-e JQUICK_OTHER_HOST=host.docker.internal -e JQUICK_OTHER_PORT=5236 \
-e JQUICK_AUTH_ENABLE=true -e JQUICK_AUTH_TOKEN_PREFIX_LEN=32 \
-e JQUICK_AUTH_TOKENS=0123456789abcdef0123456789abcdef \
jquick-hub:1.0.0

挂载自定义配置:

Terminal window
docker run -d --name jquick-hub -p 18080:18080 -v /path/to/gateway.yaml:/app/gateway.yaml:ro jquick-hub:1.0.0

端口映射规则:

  • 格式为 -p <宿主机端口>:<容器端口>;容器端口必须等于 JQUICK_LISTEN_PORT,宿主机端口随意;
  • 单端口嗅探,一条映射同时暴露 HTTP/HTTPS 与 TCP;
  • 镜像未声明 EXPOSE,端口完全由 -p 决定。

Dockerfile 要点:多阶段构建 —— Stage 1 使用 maven:3.9-eclipse-temurin-8 编译打包,Stage 2 使用 eclipse-temurin:8-jre-jammy 纯 JRE 运行;内置 ENV TZ=Asia/Shanghai LANG=C.UTF-8;ENTRYPOINT ["java","-jar","/app/tcp-gateway.jar"],CMD ["/app/gateway.yaml"]。

仓库自带 docker-compose.yaml(注意扩展名是 .yaml 不是 .yml):

services:
jquick-hub:
build: .
image: jquick-hub:1.0.0
restart: unless-stopped
ports:
- "18080:18080"
environment:
JQUICK_LISTEN_PORT: 18080
JQUICK_NGINX_HOST: host.docker.internal
JQUICK_NGINX_PORT: 52000
JQUICK_OTHER_HOST: host.docker.internal
JQUICK_OTHER_PORT: 5236
JQUICK_AUTH_ENABLE: "true"
JQUICK_AUTH_TOKEN_PREFIX_LEN: 32
JQUICK_AUTH_TOKENS: 0123456789abcdef0123456789abcdef
# Linux 环境需取消注释下一项(Windows/macOS Docker Desktop 不需要)
# extra_hosts:
# - "host.docker.internal:host-gateway"
# 可选:挂载自定义配置文件
# volumes:
# - ./gateway.yaml:/app/gateway.yaml:ro

常用命令:

Terminal window
docker compose up -d --build # 构建并后台启动
docker compose logs -f jquick-hub # 跟踪网关日志
docker compose down # 停止并移除
Terminal window
docker logs -f jquick-hub

转发成功时日志出现关键字:后端连接成功 <host>:<port>,开始透传。

以下为 src/main/resources/gateway.yaml 原文:

gateway:
listen:
host: 0.0.0.0 # 监听网卡 IP(默认所有网卡), 可用环境变量 JQUICK_LISTEN_HOST 覆盖
port: 18080 # 监听端口, 可用环境变量 JQUICK_LISTEN_PORT 覆盖
backlog: 1024 # TCP backlog(未决连接队列大小)
so_timeout: 5000 # 连接超时毫秒: 用于后端建连超时与协议探测等待上限
probe_timeout_ms: 5000 # 协议探测等待上限(可选配置): 首包不足时最多等待的毫秒数
# 等待超时后, 若已收到数据则按"普通TCP流量"转发; 未收到任何数据则关闭连接
nginx:
target:
host: 127.0.0.1 # 内网 Nginx IP
port: 52000 # Nginx 端口
other:
traffic:
target:
host: 127.0.0.1 # 指定转发 IP
port: 5236 # 指定转发端口
auth:
enable: true # 鉴权开关,生产默认开启;false 时完全透传(行为与旧版一致)
token-prefix-len: 32 # 前置token固定长度(字节),必须与下方每个token长度一致
timeout_ms: 5000 # 鉴权超时: 连接建立后未在限定时间内收到完整token则静默关闭
valid-tokens: # 合法token列表(每个必须恰好 token-prefix-len 字节)
- "0123456789abcdef0123456789abcdef" # 示例32位token,生产环境务必更换为随机生成值
netty:
boss:
threads: 1 # boss 线程数(负责接受新连接)
worker:
threads: 0 # worker 线程数(处理 IO), 0 表示自动 = CPU核数*2
buffer:
leak:
detection: SIMPLE # 缓冲区泄漏检测级别: SIMPLE | ADVANCED | PARANOID | DISABLED
# 生产环境建议 SIMPLE; 调试内存泄漏可用 PARANOID(开销大)

配置项 × 环境变量 × 默认值对照表

Section titled “配置项 × 环境变量 × 默认值对照表”
YAML 键 环境变量 代码默认值 校验规则
gateway.listen.host JQUICK_LISTEN_HOST 0.0.0.0 —
gateway.listen.port JQUICK_LISTEN_PORT 8080 1–65535
gateway.listen.backlog JQUICK_LISTEN_BACKLOG 1024 正整数
gateway.listen.so_timeout JQUICK_LISTEN_SO_TIMEOUT 5000ms(兼作后端建连超时) —
gateway.listen.probe_timeout_ms JQUICK_LISTEN_PROBE_TIMEOUT 取 so_timeout;so_timeout ≤ 0 时回退 5000 —
nginx.target.host / nginx.target.port JQUICK_NGINX_HOST / JQUICK_NGINX_PORT 127.0.0.1 / 80 port 1–65535
other.traffic.target.host / other.traffic.target.port JQUICK_OTHER_HOST / JQUICK_OTHER_PORT 127.0.0.1 / 9000 port 1–65535
other.traffic.auth.enable JQUICK_AUTH_ENABLE true(仅当 YAML 存在 auth 段时;无 auth 段则关闭) —
other.traffic.auth.token-prefix-len JQUICK_AUTH_TOKEN_PREFIX_LEN 32 字节 正整数
other.traffic.auth.timeout_ms JQUICK_AUTH_TIMEOUT_MS 5000ms —
other.traffic.auth.valid-tokens JQUICK_AUTH_TOKENS(逗号分隔) 无 开启鉴权时为空则拒绝启动;每个 token 的 UTF-8 字节长度必须恰好等于 token-prefix-len
netty.boss.threads JQUICK_NETTY_BOSS_THREADS 1 正整数
netty.worker.threads JQUICK_NETTY_WORKER_THREADS 0(自动 = CPU 核数 × 2) 不能为负
netty.buffer.leak.detection JQUICK_LEAK_DETECTION SIMPLE 非法值告警并回退 SIMPLE

配置生效优先级:环境变量 > YAML > 代码默认值。

前缀 Token 认证由 JQuickAuthPrefixHandler 实现,仅插入 OTHER 流量的流水线。客户端把固定长度的 Token 拼在第一个业务包的最前面;网关累积字节,只读窥视前 token-prefix-len 字节进行比对:

  • 比对成功:剥离 Token,以 slice + retain 零拷贝方式透传剩余业务数据,后续数据全透传;
  • 比对失败 / 超时:ctx.close() 静默关闭,不返回任何应答(默认超时 5000ms);
  • TCP 粘包 / 拆包统一累积进 authBuffer 处理,半包场景不会误判。
  • 常量时间比对:使用 MessageDigest.isEqual,先比长度再等长比较,防时序侧信道攻击;
  • 静默关闭:失败与超时不返回任何错误应答,不向公网泄露鉴权细节;
  • 日志脱敏:mask() 输出为「前 4 字符 + **** + 后 2 字符」,长度 ≤ 4 时输出 ****,任何情况下不明文打印 Token。
  • other.traffic.auth.enable=false → OTHER 流量完全透传(行为与旧版一致);
  • 默认开启条件:仅当 YAML 存在 auth 段时 enable 默认 true;无 auth 段则默认关闭;
  • 开启鉴权时 valid-tokens 为空 → 拒绝启动;
  • 每个 Token 的 UTF-8 字节长度必须恰好等于 token-prefix-len(默认 32 字节),否则拒绝启动。

GatewayClient(client/GatewayClient.java)是仓库自带的独立本地代理客户端,零第三方依赖,单个 .java 文件即可编译运行。它在本地监听一个或多个端口,把本地连接桥接到远端网关,并在首包前自动拼接认证 Token。关键常量:GW_CONNECT_TIMEOUT_MS=5000、BUFFER_SIZE=16384、ServerSocket backlog=50。

client/client.properties 原文(4 行):

local.ports=15236
gw.host=127.0.0.1
gw.port=18080
token=0123456789abcdef0123456789abcdef

配置优先级:命令行参数(-ports / -host / -port / -token) > -config 指定文件 > 当前目录 client.properties。

参数 说明 默认值
-config <path> 指定配置文件路径 当前目录 client.properties
-ports 本地监听端口,逗号分隔多端口 5236
-host 网关地址 127.0.0.1
-port 网关端口 8080
-token 认证 Token(必填,缺失退出码 1) 无
Terminal window
javac -encoding UTF-8 GatewayClient.java
java -Dfile.encoding=UTF-8 GatewayClient # 读 ./client.properties
java -Dfile.encoding=UTF-8 GatewayClient -config d:\xx\client.properties
java -Dfile.encoding=UTF-8 GatewayClient -ports 5236 -host x.x.x.x -port 8080 -token xxxxxxxx

Windows 环境可直接双击 start-client.bat(首次运行自动编译)。

  • 每个本地端口对应一个监听线程;
  • 本地 → 网关方向在首包前拼接 Token(只拼一次),网关 → 本地方向原样回传;
  • 网关不可达时静默关闭本地连接;
  • Token 日志同样按「前 4 + 后 2」掩码输出,不明文落盘。

开启鉴权(enable=true)时 valid-tokens 为空会拒绝启动。处理:在 other.traffic.auth.valid-tokens 中配置至少一个合法 Token,或通过环境变量 JQUICK_AUTH_TOKENS(逗号分隔)注入。

启动报 Token 长度错误 / 鉴权始终失败

Section titled “启动报 Token 长度错误 / 鉴权始终失败”

每个 Token 的 UTF-8 字节长度必须恰好等于 token-prefix-len(默认 32)。注意中文等多字节字符按 UTF-8 字节数计,不等于字符数;同时确认客户端 -token 与网关 valid-tokens 中的某一项完全一致。

协议识别窗口为 16 字节(PROBE_MAX_BYTES):首包 16 字节内未出现「方法名 + 空格」,累积满 16 字节即判定为 OTHER;探测超时(probe_timeout_ms)到达时已收到数据同样按 OTHER 转发。请确认客户端发送的是标准 HTTP 方法(GET / POST / …,大小写不敏感),且首包及时完整到达。

host.docker.internal 在 Linux 原生 Docker 中默认不存在,需在 docker run 追加 --add-host=host.docker.internal:host-gateway,或在 compose 中取消 extra_hosts 注释;同时确认 -p 映射中的容器端口与 JQUICK_LISTEN_PORT 一致。