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-jartarget/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)。
✨ 核心特性
Section titled “✨ 核心特性”- 单端口多协议嗅探:一个监听端口同时承载 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)。
🏗️ 架构原理
Section titled “🏗️ 架构原理” ┌─────────────┐ 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 时加入流水线)协议识别规则
Section titled “协议识别规则”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 方法名集合 |
判定规则(按优先级):
- 首字节 ==
0x16→ 判定为 TLS(HTTPS),1 字节即可判定; - 出现「方法名前缀 + 紧跟空格
0x20」→ 判定为 HTTP,大小写不敏感;拆包导致方法名不完整时等待后续字节再判; - 累积 ≥ 16 字节 仍无匹配 → 判定为 OTHER(普通 TCP 流量)。
探测超时机制
Section titled “探测超时机制”probe_timeout_ms(默认取 so_timeout 的值 5000ms;so_timeout ≤ 0 时回退 5000):
- 超时前已收到数据 → 按 OTHER 转发;
- 超时仍未收到任何数据 → 关闭连接。
分流与认证生效范围
Section titled “分流与认证生效范围”- 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 流量完全透传。
🚀 快速部署
Section titled “🚀 快速部署”方式一:源码编译
Section titled “方式一:源码编译”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 # 命令行参数指定配置文件路径配置文件查找顺序(优先级从高到低):
- 命令行参数
arg - 环境变量
GATEWAY_CONFIG - 工作目录
./gateway.yaml - classpath 内置
/gateway.yaml
方式二:Docker 部署
Section titled “方式二:Docker 部署”构建镜像:
docker build -t jquick-hub:1.0.0 .启动容器(环境变量覆盖配置):
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挂载自定义配置:
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 部署
Section titled “方式三:Docker Compose 部署”仓库自带 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常用命令:
docker compose up -d --build # 构建并后台启动docker compose logs -f jquick-hub # 跟踪网关日志docker compose down # 停止并移除docker logs -f jquick-hub转发成功时日志出现关键字:后端连接成功 <host>:<port>,开始透传。
⚙️ 完整配置说明
Section titled “⚙️ 完整配置说明”完整 gateway.yaml
Section titled “完整 gateway.yaml”以下为 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 > 代码默认值。
🔐 认证机制详解
Section titled “🔐 认证机制详解”前缀 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 字节),否则拒绝启动。
🔌 本地代理客户端
Section titled “🔌 本地代理客户端”GatewayClient(client/GatewayClient.java)是仓库自带的独立本地代理客户端,零第三方依赖,单个 .java 文件即可编译运行。它在本地监听一个或多个端口,把本地连接桥接到远端网关,并在首包前自动拼接认证 Token。关键常量:GW_CONNECT_TIMEOUT_MS=5000、BUFFER_SIZE=16384、ServerSocket backlog=50。
配置文件 client.properties
Section titled “配置文件 client.properties”client/client.properties 原文(4 行):
local.ports=15236gw.host=127.0.0.1gw.port=18080token=0123456789abcdef0123456789abcdef配置优先级:命令行参数(-ports / -host / -port / -token) > -config 指定文件 > 当前目录 client.properties。
| 参数 | 说明 | 默认值 |
|---|---|---|
-config <path> |
指定配置文件路径 | 当前目录 client.properties |
-ports |
本地监听端口,逗号分隔多端口 | 5236 |
-host |
网关地址 | 127.0.0.1 |
-port |
网关端口 | 8080 |
-token |
认证 Token(必填,缺失退出码 1) | 无 |
javac -encoding UTF-8 GatewayClient.javajava -Dfile.encoding=UTF-8 GatewayClient # 读 ./client.propertiesjava -Dfile.encoding=UTF-8 GatewayClient -config d:\xx\client.propertiesjava -Dfile.encoding=UTF-8 GatewayClient -ports 5236 -host x.x.x.x -port 8080 -token xxxxxxxxWindows 环境可直接双击 start-client.bat(首次运行自动编译)。
- 每个本地端口对应一个监听线程;
- 本地 → 网关方向在首包前拼接 Token(只拼一次),网关 → 本地方向原样回传;
- 网关不可达时静默关闭本地连接;
- Token 日志同样按「前 4 + 后 2」掩码输出,不明文落盘。
❓ 常见问题与排错
Section titled “❓ 常见问题与排错”开启鉴权后网关拒绝启动
Section titled “开启鉴权后网关拒绝启动”开启鉴权(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 中的某一项完全一致。
HTTP 请求被转发到了 other 目标
Section titled “HTTP 请求被转发到了 other 目标”协议识别窗口为 16 字节(PROBE_MAX_BYTES):首包 16 字节内未出现「方法名 + 空格」,累积满 16 字节即判定为 OTHER;探测超时(probe_timeout_ms)到达时已收到数据同样按 OTHER 转发。请确认客户端发送的是标准 HTTP 方法(GET / POST / …,大小写不敏感),且首包及时完整到达。
Linux 上容器连不通宿主机服务
Section titled “Linux 上容器连不通宿主机服务”host.docker.internal 在 Linux 原生 Docker 中默认不存在,需在 docker run 追加 --add-host=host.docker.internal:host-gateway,或在 compose 中取消 extra_hosts 注释;同时确认 -p 映射中的容器端口与 JQUICK_LISTEN_PORT 一致。
- GitHub 仓库:https://github.com/paohaijiao/jquick-hub
- Issue 反馈:https://github.com/paohaijiao/jquick-hub/issues
- 开源协议:Apache License 2.0