热血传奇RunGate源代码分析笔记:网关Socket模型与封包转发实录

来源: 作者: 点击:
RunGate 在传奇服务端体系里对应游戏运行网关(常见目录 MirServer\RunGate\RunGate.exe,对外端口 7200,对内连 M2Server 的 5000 通信口),职责是承接已进入游戏阶段的客户端 TCP 连接,把操作指令(移动、攻击、拾取、交易、技能)转发给 M2,再把 M2 回包编码后送回客户端;账号验证走 LoginGate(7000→LoginSvr 5500),角色创建选择走 SelGate / SelChrGate(7100→DBSvr 5100),RunGate 只管“进图以后”的业务流,不参与账号生命周期 。

源码形态分两支:早期官方泄露版是 Delphi(Pascal)写的 RunGate.exe,单元文件含 Main.pas、GateShare.pas、HUtil32.pas、IPaddrFilter.pas、OnLineHum.pas、GameSpeed.pas;现代重构版(OpenMir2、mirbeta)是 C# 的 GameGate 工程,目录 src/GameGate/GameGate.csproj,配置 appsettings.json,监听 7000/7100/7200 视分支命名而定 。

Delphi 原版核心结构:三个 Socket 对象——ClientSocket(监昕客户端 7200)、M2Socket(连 M2Server 内部口)、ConsoleSocket(连控制台/管理器发保活包,手工独立启动时可无,纯为管理端探活)。实际业务只靠前两个,典型生产者-消费者模型:客户端收包 → 校验 → 入 ReciveMsgList;Timer 定时从列表取消息 → 封 TMsgHeader → 发 M2;M2 回包 → 按 Socket 索引查会话 → 编码下行 。

会话管理不在堆上动态 new,而是编译期常量定长栈数组 TSessionArr[MAXSESSION],结构体含 dwConnTime、nPacketSize、dwLastMoveTick、dwLastCommTick、nSocket、nUserIdx 等;新连接进来扫未用槽位初始化,断开清槽。定长数组换来 O(1) 索引但上限硬顶,超员直接拒连,这是老网关并发上不去的根因 。

封包头 TMsgHeader(packed record,Delphi 里 packed 防对齐填充):

dwCode: LongWord; // 标识码 如 $AA55 或引擎约定魔法数
nSocket: Integer; // M2 侧回包定位客户端用的网关内 Socket 句柄
wGSocketIdx: Word; // 网关会话索引
wIdent: Word; // 消息类型 CM_MOVE/CM_ATTACK/SM_UPDATE 等
wUserListIndex: Integer;
nLength: Integer; // 后续 body 长度

客户端上行原包经 Gate 加头变内部格式发给 M2;M2 下行包 Gate 按 wGSocketIdx/nSocket 反查会话,剥头,做 EnDecode(XOR/表置换,老版解密实际落在 M2,Gate 只做透传与简单校验,这是后来被诟病的一点)后写回客户端 。

主循环不是独立线程跑 recv/send,而是靠 TTimer 周期触发(如 50ms 一次)扫 ReciveMsgList 与 SendBuf,配合 WSAEventSelect 或 select 模型收发包。优点是无锁简单,缺点是 Timer 精度与界面消息队列绑定,客户端量大时延迟抖动明显;社区改法一般是拆 IOCP / 独立收发线程 + 无锁环形队列替代 Timer 。

M2 侧对应结构(C++ 版框架参考):GameServer 维护 m_RunGateMap<SOCKET, CRunGate*>,RunGate 连上来 accept 后 new CRunGate 接管;CRunGate 内维护 m_CPlayerMap<SOCKET, CPlayer*> 与 m_strRecvBuf,ProcessData() 从缓冲切完整包 → ParsePkg() → CPacketParser::ProcessPkgFromClient(pPlayer, data, len) 落到游戏逻辑;客户端上线 RunGate 发通知建 CPlayer,断线发通知删对象,M2 不直接持客户端 TCP 。

配置文件 RunGate.ini / MirGate.ini 关键字段:ServerAddr(M2 IP,本地 127.0.0.1)、ServerPort(内部通信口 5000)、GatePort(对外 7200)、MaxSession、BlockIPList.txt(黑名单)、WordFilter.txt(聊天过滤)、KeepAliveTick(保活间隔)。C# 版改 appsettings.json 的 UpstreamGameSvr、ListenPort、AntiSpeed、PacketLimit 。

辅助模块:IPaddrFilter.pas 做 IP 黑名单/白名单与连接频率限制;GameSpeed.pas 统计客户端移动间隔判定变速器;OnLineHum.pas 维护在线人数与会话列表供控制台读;GateShare.pas 放全局常量、TMsgHeader 定义、EnDecode 表、日志宏。EDoceTool 是运行期日志落地,MakeKey 生成通信密钥,OL验证器做在线合法性探活 。

启动顺序硬约束:DBServer → LoginSvr → M2Server(Mir200) → LoginGate → SelGate → RunGate,最后起登录器;Gate 连不上上游会打日志并周期重连,不会自退 。

数据流一句话串:客户端 TCP 7200 → RunGate.ClientSocket → 校验/入队 → TMsgHeader 封装 → M2Socket → M2Server 解头跑逻辑 → 回包带 nSocket/wGSocketIdx → RunGate 反查会话 → 编码下行 → 客户端。登录前链:Client → 7000 LoginGate → 5500 LoginSvr → 7100 SelGate → 5100 DBSvr → 下发角色列表 → 选定后由 M2 授权 RunGate 建会话 → 切到 7200 主通信 。

读源码建议顺序:GateShare.pas(结构定义)→ Main.pas(Socket 事件、Timer)→ Session 数组管理 → ReciveMsgList/SendList → TMsgHeader 封装拆解 → M2Socket 发送 → Console 保活。Delphi 版变量名保留 dw/n/w 匈牙利前缀,C# 版看 ChannelMessageHandler.cs 与 GateConnection.cs 对照看最快。