网关(Gateway):游戏服务器的海关、翻译官和前台
2026/9/24 17:58:59 网站建设 项目流程

版本更新日,凌晨 3 点。

公告写着"停机维护 5 分钟"。

运维敲下重启命令的那一秒:

在线人数 128,400 → 0 (2 秒内) 登录 QPS 800 → 47,000 (30 秒后) 登录服 CPU 100%,全线超时 实际恢复 ★ 41 分钟

5 分钟的维护,变成了 41 分钟的事故。

复盘会上,架构图被投到大屏上——客户端的连线直接连到了 6 个后端服务

一个资深工程师看了三秒,说了一句:

“你们的玩家,是直接站在后厨里点菜的。”


第一幕:先分清三个"网关"

中文里"网关"这个词被严重超载了。三个完全不同的东西,用了同一个名字。

名字在哪一层干什么本文讲哪个
默认网关(Default Gateway)网络层你家路由器的 IP,192.168.1.1,帮你把包送出局域网
API 网关(API Gateway)应用层HTTP 请求的统一入口:鉴权、限流、路由✅ 部分
游戏网关服(Gate Server / 接入层)应用层长连接的统一入口,游戏服务器架构的核心组件主角

后两个才是我们要说的。它们的角色可以用三个身份概括:

🛃 海关 —— 检查你的证件,决定放不放行 🗣 翻译官 —— 把外面的语言翻译成里面的语言 🛎 前台 —— 记住你是谁,帮你转接到正确的部门

第二幕:海关——它挡住了什么

没有网关,你的后厨对全世界开放

❌ 直连架构 客户端 ──┬──→ 登录服 (公网端口 8001) ├──→ 大厅服 (公网端口 8002) ├──→ 战斗服 (公网端口 8003) ├──→ 聊天服 (公网端口 8004) ├──→ 商城服 (公网端口 8005) └──→ 好友服 (公网端口 8006)

这张图有六个致命问题:

问题后果
6 个公网暴露点攻击面 ×6,每个都要独立做防护
6 套鉴权逻辑一处写错就是漏洞;改一次要改 6 遍
6 条长连接手机要维持 6 条 TCP,6 份心跳,电池哭了
后端 IP 写死在客户端想换机器?等发版
任何一个服重启玩家立刻掉线 ←序幕的事故
内网协议暴露在公网逆向一下,你的内部结构全公开

加上网关之后

✅ 网关架构 ┌───────────────────────────┐ 客户端 ──→ LB ──→ │ 网关集群(唯一暴露点) │ 1 条连接 │ 鉴权 · 限流 · 路由 · 翻译 │ └────────────┬──────────────┘ 内网(不暴露公网) ┌──────────┬───────┼───────┬──────────┐ 登录服 大厅服 战斗服 聊天服 商城服

海关具体查什么:

① 身份证 —— Token 校验(JWT 验签 / Redis 查会话) ② 违禁品 —— 包长度上限、字段范围、非法 opcode ③ 走私 —— 签名校验、时间戳窗口、nonce 去重(防重放) ④ 人流控制 —— 单 IP 限流、单账号限流、单接口限流 ⑤ 黑名单 —— 封号、封 IP、风控规则

🎯海关最大的价值在这一句:

一次检查,后端全信任。

网关校验通过后,给消息打上可信的playerId标记
后端服务直接相信它,不再重复校验。

内网消息头 = [playerId][sessionId][gatewayId][payload] ↑ 这个 playerId 是网关盖的章,后端不再怀疑

⚠️ 前提是:内网必须真的是内网。
一旦有人能直连后端端口,这套信任体系就全面崩塌。
所以后端服务绝不能监听0.0.0.0,必须绑内网 IP + 安全组限制。


第三幕:翻译官——它翻译了什么

翻译一:外网协议 → 内网协议

外面的世界很乱:

客户端用什么为什么
PC 端TCP + 自研二进制网络稳定,简单可靠
手机端KCP / 可靠 UDP弱网下延迟低 30~40%
H5 / 小游戏WebSocket浏览器只给这个
主机端平台 SDK 的专有通道平台要求

里面的世界很整齐:

统一的内网 RPC(gRPC / 自研 TCP / 消息队列)

网关站在中间,把四种方言翻译成一种官话。

┌── 外网(乱) ──┐ ┌── 网关 ──┐ ┌─ 内网(齐)─┐ TCP + 二进制 ─────→ ─────→ KCP + 二进制 ─────→ 解密 ─────→ 统一 WebSocket ─────→ 解包 ─────→ gRPC 平台通道 ─────→ 转换格式 ─────→

💡这带来一个巨大的解耦收益:
后端逻辑服完全不知道玩家是从哪种客户端来的。

明年要上 H5 版本?只在网关加一个 WebSocket 接入器,后端一行不改。

翻译二:版本兼容

玩家不会同时更新。灰度期间,线上永远有新旧两个版本的客户端。

// 网关做版本适配,后端只认最新协议switch(conn.ProtoVersion){case3:msg=UpgradeV3ToV4(msg);break;// 老客户端,补字段case4:break;// 最新,直接过default:returnReject("版本过低,请更新");}

不这么做的后果:每个后端服务都要写一遍if (version < 4),散落在几十个文件里,没人敢删

翻译三:TLS 终结

客户端 ──[ TLS 加密 ]──→ 网关 ──[ 明文/轻量加密 ]──→ 后端 ↑ ★ 加解密的 CPU 开销集中在这一层 ★ 证书只需要在网关更新 ★ 后端服务完全不用懂 TLS

证书到期是运维事故的高发区。集中在网关,只需要换一个地方


第四幕:前台——它记住了什么

连接与会话,必须分开

这是网关设计里最核心的一个概念。

连接(Connection):一个 TCP socket / 一个 UDP 会话 ★ 脆弱,会断,绑在某台网关上 会话(Session): playerId、当前在哪个房间、权限、状态 ★ 持久,存在 Redis 里,和连接解绑
// 网关本地:只管连接classConnection{Socketsocket;stringsessionId;// ★ 只存一个 IDlonglastRecvTime;}// Redis:存真正的会话// session:{sessionId} = { playerId, roomId, gatewayId, loginTime, ... }

为什么必须分开?

玩家从 WiFi 切到 5G → 连接断了 ❌ → ★ 会话还在 ✅ → 重连时带上 sessionId,0.3 秒恢复,游戏世界毫无感知

如果会话存在网关内存里:网关一重启,所有人的状态就没了——这就是序幕事故的根因之一。

路由表:前台的通讯录

玩家的一条消息进来 → 网关看 opcode → 决定转给谁 opcode 0x1001 (移动) → 该玩家所在的【房间服 #17】 opcode 0x2001 (聊天) → 聊天服(任意一台,无状态) opcode 0x3001 (买东西) → 商城服(任意一台) opcode 0x4001 (组队邀请) → 好友服

两类路由,处理方式完全不同:

类型例子怎么路由
无状态服务聊天、商城、邮件随便挑一台,轮询/最少连接
有状态服务房间服(战斗)必须固定到那一台(房间在谁那儿就发给谁)

扇出(Fan-out):网关最被低估的能力

这是一个能省掉 90% 内网带宽的设计。

❌ 逻辑服直接广播 房间服 ──→ 玩家1 ──→ 玩家2 ... ──→ 玩家100 ★ 发 100 份一模一样的数据 ✅ 网关扇出 房间服 ──→ 网关A("这份发给你这边的 25 人") ──→ 网关B(25 人) ──→ 网关C(25 人) ──→ 网关D(25 人) ★ 只发 4 份 ↓ 网关本地复制 25 份发出去

算一笔账(100 人房间,30Hz,快照 400 字节):

房间服出口带宽
直接广播100 × 30 × 400 B =1.2 MB/s / 房间
网关扇出(4 个网关)4 × 30 × 400 B =48 KB/s / 房间

一台房间服跑 30 个房间:288 Mbps → 11.5 Mbps。降到 1/25。

🔥这个优化的精妙之处:
复制数据这件事,越靠近出口做,越便宜。
在房间服做,要过一次内网;在网关做,直接就出去了。


第五幕:射击游戏里的六个战场

战场一:后端热更新,玩家不掉线(序幕结案)

目标:逻辑服发版,玩家无感知。

关键在于:断开的是"网关↔后端",不是"玩家↔网关"。

① 大厅服要更新 ↓ ② 网关检测到大厅服下线 → ★ 不断开玩家连接 ↓ ③ 玩家发来的大厅请求 → 网关暂存到队列(最多 3 秒) ↓ ④ 新版本大厅服起来,注册到服务发现 ↓ ⑤ 网关把队列里的请求重放过去 ↓ ★ 玩家只感觉"卡了一下",没掉线

但战斗房间不能这么玩。房间服带着完整对局状态,不能中途重启。

房间服的发版策略:

① 新版本房间服上线,开始接收【新房间】 ② 老版本房间服停止接收新房间,★ 把手上的局打完 ③ 最后一局结束(最多 15 分钟)→ 下线

这叫"生命周期对齐发版"——利用对局本身是短生命周期的特点,等它自然结束,而不是打断它

结果:同样的更新,在线人数曲线上看不出任何凹陷

战场二:开服洪峰,网关当闸门

现象:新赛季 0 点,12 万人同时涌入。

没有网关时:所有人直接冲向登录服和大厅服 → 数据库连接池耗尽 → 雪崩。

网关的四道闸门:

① 连接层限流 单 IP 每秒最多 5 个新连接(防单机脚本刷) 全局新连接速率上限 3000/s(超过的直接排队) ② 排队系统 超过承载量 → 不拒绝,发号 返回 { queuePos: 8420, eta: 62s } ★ 玩家能接受排队,不能接受"未知错误" ③ 舱壁隔离 登录路由 / 大厅路由 / 商城路由 各有独立的后端连接池 ★ 登录打满时,商城照常卖 ④ 熔断降级 大厅服超时率 > 50% → 熔断,返回明确的"服务繁忙" ★ 不让客户端盲目重试,火上浇油

特别注意第①条里的一个细节:

// ❌ 只按 IP 限流:网吧/公司出口一个 IP,全被误杀// ✅ IP 限流 + 账号限流 + 设备指纹,多维度if(!ipLimiter.TryAcquire(ip)&&!IsKnownGateway(ip))Reject();

战场三:背压——网关自己别被压垮

现象:网关内存持续上涨,最后 OOM 重启,带走 3 万玩家。

根因:某个玩家的网络极慢(2G 网络),网关往他的 socket 写数据写不出去,发送队列无限堆积

房间服 30Hz 推快照 → 网关往慢速客户端写 → 写不出去,塞进队列 → 队列越来越长 → ★ 一个玩家吃掉几百 MB 内存

三层防御:

// ① 队列上限 + 丢弃策略if(conn.sendQueue.Count>MAX_QUEUE){// ★ 状态快照是【可丢弃】的——丢旧的,保留最新的conn.sendQueue.DropOldestUnreliable();metrics.BackpressureDrops++;}// ② 区分可靠 / 不可靠// 位置快照:队列满了随便丢,下一帧就来了// 结算消息:绝不能丢,宁可断开这个连接// ③ 超限踢人if(conn.sendQueue.Count>HARD_LIMIT){conn.Close("网络状况过差");// 保护整个网关}

💡一条重要原则:
网关必须假设"有些客户端就是很慢",并且保证它拖不垮其他人。
这叫"故障隔离"——一个玩家的问题,不应该变成一万个玩家的问题。

战场四:大厅到战斗的"重定向"

匹配成功了,玩家要从大厅转到战斗房间。

方案 A:换一条连接(不推荐)

网关告诉客户端:"去连 battle-server-17.game.com:9017" ↓ ★ 战斗服暴露公网 → 攻击面扩大 ★ 重新建连 + TLS 握手 → 1~2 秒空白 ★ NAT/防火墙可能连不上那个端口

方案 B:连接不变,只换路由(推荐)

客户端连接保持不变 ↓ 网关收到匹配结果:该玩家分配到【房间服 #17 的 room_8891】 ↓ ★ 网关更新这条连接的路由表:战斗类消息 → 房间服 #17 ↓ 玩家毫无感知,连接从头到尾就一条

收益:

方案 A方案 B
进战斗耗时1.5~3 s< 100 ms
公网暴露点多 N 个仍然只有网关
弱网成功率87%99.6%

战场五:网关也是最好的观测点

所有流量都经过网关 → 它天然是最完整的数据源。

每条消息都能打点: playerId · opcode · 处理耗时 · 后端服务 · 成功/失败 · traceId → 按 opcode 统计 P99 延迟:哪个接口慢了,一眼看出 → 按后端服务统计错误率:哪台机器有问题 → 链路追踪:一个玩家的投诉,能还原他完整的请求路径

一个实战用法:玩家报"卡",5 分钟定位

查 traceId → 发现 opcode 0x1001(移动)的网关→房间服 RTT 是 180ms → 而正常是 2ms → 该房间服跑在一台跨机房的机器上(调度失误) → 迁移,问题解决

没有网关时,你得分别去 6 个服务翻日志,而且它们的日志格式还都不一样。

战场六:DDoS 防护——收敛攻击面

网关是唯一的公网入口,意味着它也是唯一的攻击目标。

分层防护:

┌ 云厂商清洗(流量型攻击:SYN Flood、UDP Flood) │ ★ 这一层必须靠云厂商,自己扛不住 T 级流量 ├ LB 层(四层):连接数限制、SYN Cookie ├ 网关层(七层): │ · 握手前不分配大对象(防资源耗尽) │ · ★ 未认证连接给极短超时(3 秒内不完成握手就踢) │ · ★ 未认证连接的资源配额单独隔离 │ · 包长度硬上限,解析前先检查 └ 业务层:账号级风控、行为异常检测

"未认证连接单独隔离"这一条很关键:

// ❌ 未认证连接和正常玩家共用一个连接池// → 攻击者建 10 万个空连接,正常玩家连不进来// ✅ 分池constintMAX_PENDING=5000;// 未认证连接上限constintAUTH_TIMEOUT_MS=3000;// 3 秒内必须完成认证

第六幕:有状态网关的三个难题

网关保存着连接,所以它天然有状态。这带来三个必须解决的问题。

难题一:优雅下线(Draining)

网关要重启,怎么让玩家少受影响?

① 从负载均衡摘除(不再接新连接) ↓ ② ★ 已有连接继续正常服务 ↓ ③ 向客户端推送"建议重连"信号 客户端在【合适的时机】重连(比如一局结束后、在大厅时) ↓ ④ 等待 N 分钟,或等连接数降到阈值以下 ↓ ⑤ 强制关闭剩余连接(客户端自动重连到别的网关) ↓ ⑥ 进程退出

⚠️第③步是关键主动告诉客户端"该换个地方了",而不是粗暴踢掉。
一个正在战斗中的玩家被踢 vs 一个在大厅挂机的玩家被踢——体验天差地别。

难题二:重连要回到同一个网关吗

答案:不需要,而且最好不要。

只要会话存在 Redis 里,重连到【任意一台网关】都能恢复 ↓ ★ 网关本身变成了"准无状态" ★ 可以随意扩缩容

但有一个例外如果玩家在战斗中,房间服正在往旧网关推数据。

解法:会话里记着gatewayId,重连后:

// 新网关接管后,通知房间服更新路由awaitroomService.UpdatePlayerGateway(playerId,myGatewayId);// 房间服后续的推送,改发到新网关

难题三:网关挂了怎么办

单台网关承载 5 万连接 → 挂了就是 5 万人同时重连 ↓ ★ 必须防惊群

客户端侧:

// 指数退避 + 随机抖动intdelay=Math.Min(500*(1<<retry),15000);delay+=Random.Range(0,delay);// ★ 抖动是关键

服务端侧:

① 单台网关不要承载太多(建议 3~5 万,而非 20 万) ② 网关数量冗余(N+2) ③ 会话在 Redis,重连成本低 ④ 限流闸门保护后端

Checklist

架构

  • 公网只暴露网关,后端服务绑内网 IP
  • 连接与会话分离,会话存 Redis,不存网关内存
  • 无状态服务随机路由,有状态服务(房间)固定路由
  • 大厅→战斗用路由切换,不是重新建连

海关(安全)

  • Token 校验集中在网关,后端信任内网标记
  • 包长度硬上限(解析前检查)
  • 签名 + 时间戳 + nonce 防重放
  • 未认证连接独立配额 + 短超时
  • 多维度限流(IP + 账号 + 设备),注意网吧共享 IP

翻译官(协议)

  • 多端协议(TCP/KCP/WebSocket)在网关统一
  • 版本适配集中在网关,后端只认最新协议
  • TLS 在网关终结,证书集中管理

性能

  • 广播用网关扇出,逻辑服只发一份
  • 背压保护:发送队列上限 + 丢弃不可靠数据 + 超限踢人
  • 小包合并,心跳搭车
  • 零拷贝转发(能不反序列化就不反序列化)

运维

  • 优雅下线:摘 LB → 推送重连建议 → 等待 → 强制关闭
  • 后端发版不断玩家连接;房间服按对局生命周期发版
  • 单网关连接数控制在 3~5 万
  • 客户端重连指数退避 + 随机抖动
  • 网关埋点:opcode 级延迟、后端错误率、traceId 全链路

收尾

回到那句话——“你们的玩家,是直接站在后厨里点菜的。”

没有网关的架构,本质上是让每一个后端服务都去面对整个互联网

各自做鉴权、各自扛攻击、各自处理协议兼容、各自暴露一个端口。
而其中任何一个重启,玩家就掉线。

网关做的事,说白了就是在"混乱的外面"和"整齐的里面"之间,立一道墙,再开一扇门

门口站着海关:查证件、查违禁品、控制人流。
门内站着翻译官:把四种方言,翻译成一种官话。
翻译官旁边是前台:记着你是谁,帮你转接到正确的部门。

于是后端的每一个服务,都可以安心地假设:
“能走到我面前的,都是自己人,说的都是我听得懂的话。”

这份"安心",就是网关全部的价值。

它不产生任何游戏逻辑,不计算一次伤害,不渲染一个像素。

但它决定了——

你发一次版,是 5 分钟,还是 41 分钟。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询