游戏网关不能直接用net/http等http框架,因其需处理tcp/udp长连接、二进制协议透传、粘包拆包、低延迟路由等,必须自研连接管理器、协议解析器和转发调度器,并严防goroutine泄漏。

net/http 不适合直接做游戏网关,gorilla/mux 路由太重、中间件不可中断,httputil.NewSingleHostReverseProxy 默认行为对长连接和二进制协议完全不友好——游戏网关不是 Web API 网关的简单复刻,它必须处理 TCP/UDP 长连接、心跳保活、帧粘包、协议透传、连接池复用和低延迟路由。
为什么不能照搬 HTTP 网关那套写法
游戏客户端连的是 tcp://gateway.example:7001 或 udp://gateway.example:7002,不是 https://api.example.com/v1/login。HTTP 中间件链、JSON 解析、TLS 终止这些在游戏网关里要么不存在,要么得重写底层。
-
net/http.Server的Handler接口只吃*http.Request,没法接裸 TCP 连接;想支持 TCP 就得自己写net.Listener+net.Conn循环 -
gorilla/mux的Router.ServeHTTP依赖http.ResponseWriter,而游戏协议返回的是二进制帧(如 Protobuf 编码的Packet{Seq, Cmd, Data}),没法塞进WriteHeader() -
httputil.NewSingleHostReverseProxy内部用http.Transport,它默认启用连接池、重试、超时重置——但游戏连接一旦建立就要保持数小时,重试会破坏状态同步
必须自己实现的三个核心组件
游戏网关没有“开箱即用”的框架,kratos 和 traefik 都不适用:前者是 gRPC/HTTP 混合服务框架,后者是纯 HTTP 反向代理。你要从 net 包开始搭骨架。
-
连接管理器:用
sync.Map存map[uint64]*Conn(key 是 clientID 或 sessionID),而不是map[net.Conn]——连接断开后 key 不销毁,方便踢人、重连复用、广播通知 -
协议解析器:别用
json.Decoder,用binary.Read+ 自定义 header(如 4 字节长度 + 2 字节 cmd + payload),配合bytes.Buffer做粘包拆包;UDP 场景下还要加 sequence check 和丢包标记 - 转发调度器:不是按 path 路由,而是按
CmdID或GameRoomID分发;后端服务发现用consul.Agent.ServiceRegister注册game-battle:7003,网关查到多个实例就用selector.WeightedRoundRobin选一个,不是靠 HTTP Host 头
goroutine 泄漏比内存泄漏更致命
每个 TCP 连接启一个 goroutine 看似自然,但游戏客户端掉线不发 FIN、NAT 超时静默断连、心跳超时未清理,都会让 goroutine 卡在 conn.Read() 或 select{ case 上,积压几百个就 OOM。
- 必须给每个连接配独立
context.WithTimeout(ctx, 30*time.Second),读写都带 deadline,超时后conn.Close()并从sync.Map删除 - 心跳检测不能靠
time.Ticker全局扫——要为每个连接起一个go func() { ... }(),里面select监听conn.Read()和time.After(),任一触发就更新 lastActive 时间 - 禁止在 handler 里调
time.Sleep()或阻塞 channel 操作;限流用golang.org/x/time/rate.Limiter,每帧检查limiter.AllowN(time.Now(), frameCount)
插件系统必须支持二进制协议透传
鉴权、计费、日志这些插件,不能假设输入是 JSON 或 HTTP Header。它们要能拿到原始 []byte、解析出 CmdID 和 UserID,再决定是否 return 或 ctx.Cancel()。
- 插件接口定义必须含
InputType() string(返回"tcp"/"udp"/"ws"),网关根据类型分发原始字节或封装后的*Packet - JWT 验签插件不能依赖
X-Forwarded-User,得从登录包里解出token字段,用golang.org/x/crypto/bcrypt校验签名,并把userID写进ctx.WithValue()供后续插件取用 - 日志插件要记录
conn.RemoteAddr().String()+packet.CmdID+len(packet.Data),而不是req.URL.Path;错误日志必须带conn.LocalAddr()方便定位网关节点
sync.Map 里存的 session 结构体字段来协调,而且字段含义必须被所有插件约定好。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











