C++传奇服务端MMO架构拆解:GameSrv与DBSrv网关线程模型及核心模块实战

来源: 作者: 点击:
传奇(Legend of Mir 2 / M2 系)C++ 服务端不是单体程序,而是按“接入—认证—逻辑—持久化”拆成的多进程协同体:LoginGate → LoginSrv → SelGate → GameGate → GameSrv(M2Server)↔ DBSrv ↔ 数据库。各进程通过 TCP / 命名管道 / 内部消息协议通信,CM_(客户端指令)、SM_(服务端下发)、RM_(内部引用消息)、GM_(网关消息)、DB_/DBR_(库消息)五类标识符串起整条链路 。下面按进程边界、线程模型、核心类、地图广播、战斗与脚本、启动顺序逐层拆。

一、进程边界与职责切分
1. LoginGate(端口 7000):客户端首个入口,收账号包做 XOR+位移解密、版本过滤、连接数限制,转发 LoginSrv,不直接碰库 。
2. LoginSrv:验 ID.DB / SQL 账号密码、签发 session、返回服务器列表,无状态可横向扩 。
3. SelGate(端口 7100):角色列表、建/删角,从 DBSrv 拉 _THUMANRCD 快照序列化回客户端 。
4. GameGate(RunGate):游戏期长连接代理,收 CM_WALK/CM_HIT/CM_SPELL/CM_SAY,转发 GameSrv;回包压对应 CUserInfo 发送缓冲,处理 GM_* 前缀 。
5. GameSrv(M2Server / GameSvr):世界逻辑内核,加载 MapInfo/MonGen/Envir、跑 CPlayerObject / CMonsterObject / CNPCObject / CItemObject、战斗公式、技能、NPC 脚本解析、地图单元格广播 。
6. DBSrv:持久层代理,ODBC / SQL 连接池,处理 DB_LOADHUMANRCD / DB_SAVEHUMANRCD / 物品与行会写入,强制异步+内存缓存,不阻塞 GameSrv 主循环 。

二、线程模型(GameSrv 内部分工)
GameSrv 主循环按“流水线”拆线程,避免单循环串行卡死 :
• ProcessLogin 线程:新连接建 CUserInfo + CPlayerObject,发 RM_LOGON → SM_NEWMAP / SM_LOGON / SM_ABILITY / SM_SENDUSEITEMS / SM_SENDMYMAGIC 回流客户端 。

- ProcessUserHuman 线程:遍历全局 UserInfoList,调 pUserInfo->Operate() → m_pxPlayerObject->Operate(),消费命令队列(移动/攻击/施法/拾取/交易),算完调 pGateInfo->xSend() 发回 GameGate;SO_SNDBUF 置 0,WSASend 阻塞式顺序发,避免乱序 。
- ProcessMonster 线程:创建 CMonsterObject 入 xMonsterObjList 与 pMapCellInfo->m_xpObjectList;SearchViewRange() 用 g_SearchTable 算视距格 → 填 m_xVisibleObjectList → Operate() 过滤 RM_DOOPENHEALTH / RM_STRUCK / RM_MAGSTRUCK 事件 。
- ProcessMerchants / ProcessNPC 线程:遍历 g_pMerchantInfo,按地图建商人对象,跑 Market_Def 脚本段 。
• 网络 IO 线程:早期 WSAAsyncSelect,后期 IOCP(AcceptEx + WSASend + 完成端口),LoginGate/GameGate 同构 。

- 同步原语:CIntLock 封装临界区,CWHQueue / CDBManager / CGlobalUserList / CUserInfo / CPlayerObject 均从其派生,Lock()/Unlock() 控共享数据;地图单元格对象列表、可见性列表、发送队列全走这层 。

三、核心类与数据结构(GameSrv/ 目录对照)
• CUserInfo:会话载体,嵌 CGateInfo(sock、xSendBuffQ、GateIdx)、m_pxPlayerObject 指针、命令队列 。

- CPlayerObject(ObjectPlayer.cpp):角色实体,管移动 WalkTo / CanMove / MoveToMovingObject、战斗 AttackTarget、魔法 UseMagic、背包 Inventory、装备 UseItems、技能 MagicList、属性 Ability/SubAbility 。
- CMonsterObject(ObjectMonster.cpp):Race 行为机、AI 状态(巡逻/追击/攻击/召唤/复活)、SearchViewRange、DropLoot 。
• CNPCObject / CMerchantObject:脚本绑定 NPC,解析 #IF/#ACT/#ELSE、CALL QuestDiary 段 。

- CMapCellInfo / TMapCell:地图格子,存可行走/遮挡、m_xpObjectList(本格内玩家/怪/NPC/物品指针)、可见性推导源 。
• _THUMANRCD / _TUSERITEMRCD / _THUMANMAGICRCD:角色、物品、技能落库结构体,DBSrv 序列化读写 。

- TDEFAULTMESSAGE:包头(nRecog / wIdent / wParam / lParam / dwBodySize),客户端与服务端统一解包结构 。

四、地图单元格 + 视距广播(MMO 同步核心)
传奇不用全图广播,用“地图号 + 单元格 + 视距半径”裁剪:
1. 角色移动完,CPlayerObject::SearchViewRange() 按 g_SearchTable 算以自身为中心的 26×26(或配置半径)格矩形;
2. 把范围内 CMapCellInfo->m_xpObjectList 里的玩家/NPC/怪物指针填入 m_xVisibleObjectList;地面物品填 m_xVisibleItemList;事件填 m_xVisibleEventList ;
3. 状态变更(RM_TURN / RM_WALK / RM_HIT / RM_STRUCK / RM_ITEMSHOW / RM_SHOWEVENT)调 AddProcess() → 仅向视距内对象所属 CUserInfo 的 xSendBuffQ 推 SM_*;
4. 离开视距收 SM_CLEAROBJECT / SM_DISAPPEAR,进视距收 SM_TURN + 属性包;
5. 切图(SM_NEWMAP)清空三张可见列表,重载 Map\*.map 块数据,重算 Light/Fog 。
这套机制让单 GameSrv 承载千人同服仍可控,沙巴克攻城靠 GameGate 多开分线 + 皇宫独立单元格压缩广播域 。

五、战斗与消息流转实例(CM_WALK → 广播)
客户端发 CM_WALK(方向) → GameGate → GameSrv ProcessUserHuman 取队列 → CPlayerObject::Operate() 判 m_fIsDead / CheckDoorEvent / m_pMap->CanMove / MoveToMovingObject → 成功发 RM_WALK 给周围单元格 → AddProcess 转 SM_WALK 下发;失败回 CM_FAIL(客户端 OnProcPacketNotEncode 拉回坐标) 。
攻击链:CM_HIT/CM_SPELL → Operate() 算命中(HIT vs AC/MAC)、伤害(DC/MC/SC ± 随机 ± 技能系数)、毒/麻痹/暴击 → RM_STRUCK 广播 SM_STRUCK → 目标 HP≤0 发 RM_DEATH → SM_DEATH + 掉落实例进 m_xVisibleItemList + DBSrv 异步存盘 。

六、脚本系统挂载点(逻辑热更层)
GameSrv 内嵌脚本解析器,NPC 对话/任务/商城/攻城不写死 C++:
• Merchant.txt 注册 NPC → Market_Def\名字-地图号.txt 对话树

- QFunction-0.txt(QF)挂物品使用/死亡/登录/打怪钩子
• QuestDiary\ 状态机脚本,NPC 内 #CALL [\QuestDiary\xxx.txt] @段 触发

- Robot_def\AutoRunRobot.txt 定时线程驱动喊话/刷怪/攻城
脚本解析器把文本指令编译成内部事件塞回 CPlayerObject::Operate 队列,实现不改 C++ 热更玩法 。

七、DBSrv 持久化与 IPC
DBSrv 启动 InitInstance 里 GetDBManager()->Init(LogCb, dbName, user, pwd) 建 ODBC/SQL 长连;收 GameSrv 的 DB_LOADHUMANRCD / DB_SAVEHUMANRCD / DB_ADDITEM 等 → 拼预编译 SQL → 回 DBR_* 结果包;写操作入内存队列异步刷盘,防金币/装备双花靠事务 + GUID 唯一标识 。

八、标准启动顺序与排错
DBSrv → LoginSrv → GameSrv(M2Server) → LoginGate → SelGate → GameGate;任意前置没起,后续进程连不上配置端口直接退 。
常见卡点:
• 卡登录:LoginGate 端口被占 / ID.DB 路径错 / LoginSrv 未起

• 选角空:SelGate 未起或 DBSrv 未回 DBR_LOADHUMANRCD

- 进图瞬断:Map\*.map 缺文件 / MiniMap.txt 错行 / Envir\!Setup.txt 路径中文
• 同屏卡:GameGate 单线,千人攻城需多 RunGate + 地图分块

- 存盘丢:DBSrv ODBC 断连未重连,Hum.DB 锁文件残留

C++ 传奇服务端的架构本质是把“网络 IO、账号认证、世界模拟、数据持久化”拆成独立进程,用 CM/SM/RM/GM/DB 五套消息标识做弱耦合强契约通信;GameSrv 内再以 ProcessUserHuman / ProcessMonster / ProcessMerchant 多线程流水线 + CMapCellInfo 视距广播撑起 MMO 实时性,CPlayerObject 与脚本系统把数值和玩法留在可热更层,C++ 只保底层确定性与吞吐。