- ProxySessionBenchmark 新增 --profile modded(仿大型整合包的双向包组与 2 MiB 登录突发,逐字节校验)、 --websocket-tls 1(keytool 一次性证书的 wss)和 --client-coalesce 1。 - wss 监听改用随包附带的 BoringSSL(netty-tcnative-boringssl-static 2.0.84):TLS 占 wss 代理 I/O CPU 的 55%, 换用后每帧 CPU 在每一对配对测量中都更低(窗口 1 -22%,窗口 8 -36%),帧率 +31%/+34%;其他平台回落 JDK, 启动日志标明所用实现。jar 由 10.4 MB 增至 16.5 MB。 - 新增 websocket.pingIntervalSeconds(默认 30,0 关闭):升级后代理静默不超过该时长,期间无写出就发空 Ping, 避免 CDN/负载均衡在安静的登录、选服阶段按空闲超时断开。IdleStateHandler 版本在窗口 8 多耗 7.6% CPU, 未达规则;改为写时置位标志、每半个间隔检查的实现,JFR 中几乎不可见。 - 文档:WebSocket 接入新增“经 CDN 部署”,更正消息大小描述;记录见 testing/benchmarks/results/2026-10-23-websocket-cdn.md。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
11 KiB
WebSocket 接入
MoonBridge 可以在 TCP listen 之外再开一个 WebSocket 玩家监听。这样玩家流量可以经 CDN(如 Cloudflare)或 HTTP 反向代理转发,从而换一条线路。两种入口同时工作:原版客户端照常连 TCP 端口;装了 MoonBridge WebSocket 客户端 mod 的客户端填 ws:// 或 wss:// 地址走 WebSocket。两种连接进入同一套会话、选服、转服和插件逻辑,共用 maxConnections。
CDN 能否降低延迟或丢包,取决于玩家到 CDN 边缘、以及边缘到源站这两段线路。本仓库只验证了协议正确性,没有做延迟或容量测量;是否值得启用应在实际线路上测量。
协议约定
客户端 mod 与代理按以下约定实现,其他客户端或网关也可以照此接入:
- 标准 RFC 6455 WebSocket。客户端向配置的
path发送 HTTP/1.1GET升级请求,不需要子协议,也不协商扩展(不压缩)。 - 升级完成后,每条消息都是二进制帧,载荷是原始 Minecraft TCP 字节流的一段。这些字节与直连 TCP 时完全相同:带 VarInt 长度前缀,在线模式下还经过 Minecraft 自己的 AES/CFB8 加密。
- 消息边界没有含义。一条消息可以只含半个 Minecraft 帧,也可以含多个帧;接收方必须按字节流拼接。代理把每次 flush 之前写出的数据合并为一条消息(单条上限 64 KiB,超过则拆成多条);代理接收的单条消息载荷上限为一个 1.7.10 帧的最大值(2 MiB + 3 字节)。
- 收到文本帧视为协议错误,代理直接关闭连接。Ping 由代理回复 Pong;关闭帧关闭连接。代理关闭会话时先发送关闭帧。
- 升级完成后,代理保证朝客户端方向的静默不超过
pingIntervalSeconds:期间没有数据时发送一个空 Ping,客户端须回复 Pong(RFC 6455 本就要求)。这让 CDN 和负载均衡在登录、选服等安静阶段也能看到流量,不会按空闲超时切断连接。 - 请求其他路径、非升级请求返回
404并关闭。连接建立后,若在handshakeTimeoutSeconds内没有完成升级,代理关闭连接;尚未升级的连接同样占用maxConnections名额。
配置
listen: "0.0.0.0:25565" # 原生 TCP,保持不变
authentication: ONLINE_BUNGEE
websocket:
listen: "127.0.0.1:8080" # WebSocket 监听;通常只对本机反向代理开放
path: "/mc" # 默认 "/"
# tls: { certificateChain: "certs/fullchain.pem", privateKey: "certs/privkey.pem" }
trustedProxies: ["127.0.0.1"] # 只有这些来源可以提供真实 IP 头
realIpHeader: "X-Forwarded-For" # 默认值;也可用 CF-Connecting-IP、X-Real-IP
handshakeTimeoutSeconds: 10 # 1–60,默认 10
pingIntervalSeconds: 30 # 0–300,默认 30;0 不发送 Ping
| 键 | 说明 |
|---|---|
listen |
必填,host:port,端口须大于 0。OFFLINE 模式下与 TCP listen 相同,必须是字面量回环 IP,除非设置 allowOfflinePublicAccess: true。 |
path |
升级请求的路径,须以 / 开头,不含查询串和空白。 |
tls |
可选。客户端 mod 只用 TLS 1.2(Minecraft 自带的 Netty 无法完成 TLS 1.3 握手),因此任何 TLS 终端都不能只开放 TLS 1.3。配置后监听端直接提供 wss://;PEM 证书链与未加密的 PKCS#8 私钥,路径相对于配置文件。PKCS#1 私钥(BEGIN RSA PRIVATE KEY)可用 openssl pkcs8 -topk8 -nocrypt -in key.pem -out key.pk8.pem 转换。不配置时为明文 ws://,由 CDN 或反向代理终结 TLS。发行包附带 BoringSSL(Linux x86_64/aarch64、macOS、Windows x86_64),监听端用它处理 TLS,CPU 开销明显低于 JDK 实现;其他平台回落到 JDK,启动日志的 TLS via OPENSSL/JDK 标明实际所用。 |
trustedProxies |
IP 或 CIDR 列表。仅当 TCP 对端属于该列表时才读取真实 IP 头;为空时始终使用 TCP 对端地址。 |
realIpHeader |
真实 IP 头名称,大小写不敏感。X-Forwarded-For 从右往左取第一个不属于 trustedProxies 的地址(全部可信时取最左项);其他头必须恰好出现一次且是 IP 字面量。可信来源发送了格式错误的头时拒绝连接,不会回退。 |
handshakeTimeoutSeconds |
从建连到升级完成的期限。 |
pingIntervalSeconds |
代理朝客户端方向的静默不超过这个时长:每半个间隔检查一次,上半个间隔内没有写出就发送一个空 WebSocket Ping。须小于玩家与代理之间所有 CDN、负载均衡和反向代理的空闲超时;默认 30 秒低于常见的 60–100 秒。连接不可写时不发送(数据已在排队)。 |
--validate-config 同样会加载并校验 TLS 证书和 trustedProxies。启动日志会打印 WebSocket 监听的地址与路径。
玩家地址
代理按上表确定的地址就是该玩家的地址,用于 ConnectionAdmissionEvent、PlayerAdmissionEvent、Mojang hasJoined 的 ip 参数和 Bungee 旧版转发写给后端的 IP。来源不可信时使用 TCP 对端地址,即 CDN 边缘或反向代理的地址,这时按 IP 封禁会作用于整个边缘节点。经 CDN 接入时必须正确配置 trustedProxies,同时不能把源站端口暴露给 CDN 以外的来源:任何能直连源站、且来源在 trustedProxies 内的人都可以伪造真实 IP 头。
经 WebSocket 接入的连接在升级完成后才触发 ConnectionAdmissionEvent,这时已经确定了真实地址。
经 CDN 部署
以下适用于任何能转发 WebSocket 的 CDN 或负载均衡,Cloudflare 的具体设置见下一节示例。
- 空闲超时。 CDN 和负载均衡会切断一段时间没有流量的连接,常见值为 60–100 秒,也有更短的。PLAY 阶段后端的 Keep Alive 足以保持连接;大型整合包登录、等待选服或后端卡顿时则可能长时间没有数据,
pingIntervalSeconds用来覆盖这些阶段。若链路上某一跳的空闲超时短于 30 秒,把它调到更小的值。nginx 等自建反向代理的proxy_read_timeout也须大于该值。 - 真实 IP。 在
trustedProxies中列出 CDN 公布的回源 IP 段,realIpHeader设为 CDN 写入客户端地址的头(X-Forwarded-For、CF-Connecting-IP、True-Client-IP等,以 CDN 文档为准)。 - 回源走 ws 还是 wss。 CDN 到源站走 TLS(
tls配置或前置 nginx 终结)时,这一段的 TLS 开销落在源站;回源走明文 ws 时,须用防火墙确保只有 CDN 能连到该端口。客户端到 CDN 边缘始终建议用wss://。 - TLS 版本。 客户端 mod 只用 TLS 1.2,CDN 边缘不能设为仅 TLS 1.3。
- 不压缩。 代理和客户端都不协商 permessage-deflate:在线模式下数据在分帧前已经 AES 加密,无法压缩;1.7.10 的区块数据本身已是 zlib 压缩;客户端使用的 Netty 4.0.10 也没有该扩展。
- 延迟。 每个连接要多走客户端到边缘和边缘到源站两段,加上 TLS 与 HTTP 升级各一到两次往返;登录与 Forge 握手的往返次数由协议决定。CDN 是否改善体验取决于实际线路,应在玩家所在地实测。
部署示例
Cloudflare + 本机 nginx
server {
listen 443 ssl;
server_name mc.example.com;
# ssl_certificate / ssl_certificate_key:源站证书(Cloudflare Origin CA 或公网证书)
location /mc {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
}
}
对应的 MoonBridge 配置:websocket.listen: "127.0.0.1:8080"、path: "/mc"、realIpHeader: "X-Forwarded-For"。trustedProxies 同时列出 127.0.0.1 和 Cloudflare 公布的 IP 段:Cloudflare 把客户端地址追加到 X-Forwarded-For,nginx 再追加 Cloudflare 边缘地址,代理从右往左跳过这两类可信地址后得到玩家 IP。nginx 本身也应只接受 Cloudflare IP 段的连接。
Cloudflare 侧:
- DNS 记录开启代理(橙色云朵),在 Network 设置中开启 WebSockets。
- 只有 Cloudflare 支持的 HTTP/HTTPS 端口能经它代理,例如 HTTPS 的 443、2053、2083、2087、2096、8443 和 HTTP 的 80、8080、8880、2052、2082、2086、2095;Minecraft 默认的 25565 不在其中。
- SSL/TLS 模式建议用 Full (strict),回源也走 TLS。
- Cloudflare 会在边缘维护或升级时断开长连接,玩家会掉线后需要重连;这是 CDN 行为,代理无法保持连接。具体套餐对 WebSocket 时长和流量的限制以 Cloudflare 当前文档和条款为准。
玩家在客户端填 wss://mc.example.com/mc。
不经 nginx,Cloudflare 直接回源
websocket.listen 绑定公网地址,并开启 tls(Cloudflare Origin CA 证书即可,Cloudflare 选 Full (strict));trustedProxies 填 Cloudflare IP 段,realIpHeader: "CF-Connecting-IP"。防火墙只放行 Cloudflare IP 段访问该端口。
直连 WebSocket(无 CDN)
开发或测试时可以把 websocket.listen 设为可直连的地址并留空 trustedProxies,客户端填 ws://host:port/path。
客户端
客户端 mod 的安装、地址格式和排错见 client-mod/README.md。服务器列表 ping 和加入服务器都会走 WebSocket;普通 host:port 地址行为与原版一致,未安装 mod 的玩家不受影响。
验证范围
自动化测试(WebSocketListenerTest、WebSocketFrameAdapterTest、ClientAddressPolicyTest)在真实 socket 上覆盖:
- TCP 与 WebSocket 玩家同时登录同一代理并转发 PLAY 帧;
- 握手跨消息拆分、多帧合并为一条消息、PLAY 帧跨消息拆分且中间夹 Ping;
- 在线模式在 WebSocket 内的加密登录,Mojang 验证 IP 与 Bungee 转发 IP 取自可信头;
- 不可信来源不能伪造地址;
- 错误路径返回 404、文本帧关闭连接;
- 未升级连接占用并归还
maxConnections名额,以及升级期限; - 由配置加载 PEM 证书后提供
wss://。 - 升级后空闲的连接收到代理的 Ping;有写出时推迟 Ping,不可写时不发送(
WebSocketKeepaliveTest)。
另用客户端 mod 自己的管线(Netty 4.0.10,Java 8 与 21)对已安装的 MoonBridge 发行包实测过 ws:// 与 wss:// 的状态查询和登录阶段断开消息,以及错误路径的 404。
没有覆盖:真实 CDN 链路、真实客户端游戏内体验、经 WebSocket 的延迟与吞吐。