Files
mc-gateway/docs/tcp-web-port-reuse-design.md

7.9 KiB
Raw Permalink Blame History

TCP 端口复用 Web 服务设计

背景

当前 gateway 的 Minecraft TCP 入口和 WebSocket/HTTP 入口分别监听端口:

  • runTcp 使用 net.Listen("tcp", :tcp.port) 接收原始 Minecraft TCP 连接。
  • runWebSocket 使用 http.ListenAndServe(:websocket.port) 接收 HTTP/WebSocket 连接。
  • handleRequest 会读取连接的第一段数据,并通过 Minecraft 握手包里的 host 路由到上游。

因此当前不能把 tcp.portwebsocket.port 配成同一个端口;两个监听器会竞争同一个 TCP 地址,后启动的服务会失败。

目标

在同一个 TCP 监听端口上同时提供:

  • Minecraft 原始 TCP 转发服务。
  • Web 服务,包括普通 HTTP 接口和 WebSocket upgrade。

入口连接根据首包内容自动分流:

  • HTTP/WebSocket 首包进入 Web 服务。
  • 其他首包进入现有 Minecraft TCP 转发流程。

非目标

  • 不在本次设计中实现代码。
  • 不改变 KCP、QUIC 的端口与协议行为。
  • 不实现 HTTPS/TLS 复用。HTTPS 需要在 gateway 内终止 TLS或由外部代理终止 TLS 后再转发明文 HTTP。
  • 不改变现有 Minecraft host 路由规则。
  • 不改变现有上游协议选择规则,例如 quic://kcp://haproxy://

配置设计

不新增复用开关,复用行为由端口配置自动决定:

[tcp]
enable = true
port = 25565

[websocket]
enable = true
port = 25566
path = "/"

端口判定规则:

  • tcp.enable = truewebsocket.enable = truetcp.port == websocket.port 时,自动进入端口复用模式。
  • tcp.enable = truewebsocket.enable = truetcp.port != websocket.port 时,保持现有独立监听模式。
  • 只有 tcp.enable = true 时,只启动 Minecraft TCP 服务。
  • 只有 websocket.enable = true 时,只启动 Web 服务。
  • 端口比较应使用默认值归一化后的结果,例如 websocket.port = 0 应先按当前默认值视为 25566

自动复用模式下:

  • Web 服务不再单独监听 websocket.port
  • Web 服务挂载到 TCP listener 的 HTTP 分流分支。
  • 实际对外端口为 tcp.port
  • websocket.porttcp.port 相同,作为触发自动复用的配置表达。

总体架构

复用模式下只有一个 TCP listener

net.Listen(:tcp.port)
        |
        v
Accept
        |
        v
读取首批字节
        |
        +-- HTTP/WebSocket
        |       -> HTTP channel listener
        |       -> http.Server.Serve
        |
        +-- Minecraft TCP
                -> 回放首批字节
                -> handleRequest
                -> mapToHost
                -> proxyConnections

关键点:首包只用于判断协议,不能被消费掉。分流后必须把已经读取的字节重新接回连接流。

协议识别规则

HTTP/WebSocket 都以 HTTP 请求开始,因此只需要识别 HTTP 方法前缀。

建议识别以下前缀:

  • GET
  • POST
  • HEAD
  • PUT
  • PATCH
  • DELETE
  • OPTIONS
  • CONNECT
  • TRACE

WebSocket upgrade 请求通常是 GET /path HTTP/1.1,会自然进入 HTTP 分支,再由现有 WebSocket handler 处理。

未匹配 HTTP 方法前缀的连接全部进入 Minecraft TCP 分支。

首包读取与回放

新增一个包装连接,例如 replayConn

type replayConn struct {
    net.Conn
    reader io.Reader
}

创建时把已读取的首包和原始连接拼接:

reader := io.MultiReader(bytes.NewReader(peeked), conn)

之后 Readreader 读取,WriteClose、deadline、地址信息继续委托给底层 net.Conn

这样 Minecraft 分支仍然可以使用现有 handleRequest(replayConn)mapToHost 读到的内容和没有分流时一致。

HTTP 接入方式

不建议手写 HTTP 解析。建议实现一个 channel listener

type chanListener struct {
    conns  chan net.Conn
    closed chan struct{}
    addr   net.Addr
}

行为:

  • 主 TCP accept 循环识别到 HTTP 后,把 replayConn 投递到 chanListener.conns
  • http.Server.Serve(chanListener) 负责标准 HTTP/WebSocket 处理。
  • 关闭 gateway 时关闭 chanListener,让 HTTP server 退出。

优点:

  • 继续使用 Go 标准库 HTTP server。
  • WebSocket upgrade 流程不需要重写。
  • 可以复用现有 handleWebSocket

运行流程

自动复用模式启动流程:

  1. 加载配置。
  2. 创建 Web handler 和 http.Server,但不调用 ListenAndServe
  3. 创建 TCP listener。
  4. 启动 http.Server.Serve(chanListener)
  5. TCP accept 循环接收所有连接。
  6. 每个连接设置 socket option。
  7. 设置短读超时读取首批字节。
  8. 根据首包分流到 Web 或 Minecraft。

非复用模式保持现有流程:

  • TCP 服务继续由 runTcp 独立监听。
  • WebSocket 服务继续由 runWebSocket 独立监听。

超时与错误处理

首包读取需要短超时,避免空连接或慢连接长期占用 goroutine。

建议策略:

  • 首包读取超时:关闭连接并记录 debug 或 warn 日志。
  • 首包为空:关闭连接。
  • HTTP 分支投递失败:关闭连接。
  • Minecraft 分支继续使用现有错误处理。

读超时只用于首包判断。分流完成后应清除 read deadline避免影响长连接转发。

与现有热路径的关系

Minecraft TCP 分支应尽量保持现有转发路径:

  • handleRequest
  • mapToHost
  • proxyConnections
  • copyForward

复用逻辑只出现在 listener 和首包分流层,不进入双向转发热路径。这样可以避免破坏已有 TCP 转发优化。

测试计划

单元测试:

  • HTTP 方法前缀识别。
  • 非 HTTP 首包进入 Minecraft 分支。
  • replayConn 能先读出已窥探字节,再读底层连接后续字节。
  • 首包读取超时会关闭连接。
  • chanListenerAcceptClose 行为。

集成测试:

  • 复用模式下,同一端口可以访问 HTTP 接口。
  • 复用模式下,同一端口可以完成 WebSocket upgrade。
  • 复用模式下Minecraft 握手包仍能被 protocol.GetMcHost 正确解析。
  • 非复用模式下,现有 tcp.portwebsocket.port 行为不变。
  • tcp.port == websocket.port 时自动复用,不应出现两个 listener 竞争同一端口。

性能验证:

  • 对 Minecraft TCP 转发路径跑现有 benchmark确认复用层没有影响已建立连接后的转发性能。
  • 至少覆盖 loopback TCP 场景,因为该项目的主要热路径是 TCP-to-TCP 转发。

风险与边界

  • HTTP 识别只能覆盖明文 HTTP。TLS 握手首包不会匹配 HTTP 方法,会被送入 Minecraft 分支并失败。
  • 某些非 HTTP 协议如果首包刚好以 GET 等方法前缀开头,会被误判为 Web 请求;对 Minecraft Java 握手来说风险很低。
  • 首包读取缓冲区不能太小,否则可能影响识别;但 HTTP 方法识别只需要很少字节。
  • 复用模式改变 listener 所有权,需要注意进程退出时 TCP listener、HTTP server、channel listener 的关闭顺序。

实施步骤

  1. 增加端口默认值归一化和启动模式判定逻辑。
  2. 新增 HTTP 方法识别函数。
  3. 新增 replayConn
  4. 新增 chanListener
  5. 拆分 WebSocket handler 注册逻辑,让自动复用模式和独立监听模式都能使用同一套 handler。
  6. 新增自动复用模式的 TCP accept 分流入口。
  7. 增加配置校验,避免启动两个 listener 监听同一 TCP 地址。
  8. 补充单元测试、集成测试和 TCP benchmark 验证。

验收标准

  • tcp.port == websocket.port 且两个服务都启用时,只暴露该端口也能同时提供 Minecraft TCP 和 WebSocket/HTTP 服务。
  • Minecraft 客户端连接、host 路由、上游转发行为与复用前一致。
  • WebSocket 客户端通过同一端口能完成 upgrade 并进入现有 gateway 流程。
  • 端口不同或只启用单个服务时,现有配置和行为不变。
  • 测试覆盖首包识别、连接回放、HTTP 分流、Minecraft 分流和配置校验。