AzerothCore学习笔记·架构01:双进程架构——Auth 和 World 为什么是两个服务
你在《魔兽世界》里输入账号密码点登录屏幕闪了一下你看到了服务器列表。选服、进入角色出现在暴风城。这三步背后客户端和两个服务器程序打了两次交道——它们监听不同的端口、加载不同的数据库、运行不同的主循环。一个是 authserver一个是 worldserver。如果你打开 AzerothCore 编译后的bin/目录会看到这两个可执行文件。为什么必须是两个不是一个答案要从两份Main.cpp的代码差异说起。两份 Main.cpp 的核心差异把src/server/apps/authserver/Main.cpp和src/server/apps/worldserver/Main.cpp并排放在一起差异一目了然。Auth Server 的StartDB()boolStartDB(){DatabaseLoaderloader(server.authserver);loader.AddDatabase(LoginDatabase,Login);// 只加载 LoginDatabase即 Auth 库}World Server 的StartDB()boolStartDB(){DatabaseLoaderloader(server.worldserver,...);loader.AddDatabase(LoginDatabase,Login).AddDatabase(CharacterDatabase,Character).AddDatabase(WorldDatabase,World);// 加载全部三个库}Auth Server 只连 Auth 库World Server 连全部三个库。这不是配置问题是代码层面就写死了的。登录流程代码视角玩家点击「登录」后客户端做了两次连接对应两份代码第一次连接Auth Serverauthserver/Main.cpp的main()里int32 portsConfigMgr-GetOptionint32(RealmServerPort,3724);sAuthSocketMgr.StartNetwork(*ioContext,bindIp,port);Auth Server 启动后只做一件事监听 3724 端口等待客户端连接。连接建立后AuthSocketMgr处理登录请求验证账号密码SRP6 协议密码不以明文传输查询realmlist表返回可用服务器列表。验证完成后Auth Server不断开连接——客户端自己断开去连 World Server。第二次连接World Serverworldserver/Main.cpp的main()里uint16 worldPortuint16(sWorld-getIntConfig(CONFIG_PORT_WORLD));sWorldSocketMgr.StartWorldNetwork(*ioContext,worldListener,worldPort,networkThreads);World Server 监听 8085 端口。客户端带着 Auth Server 签发的 Session Token 来连接World Server 验证 Token 合法后允许进入游戏。为什么不合并代码层面的五个原因1. 数据库访问权限不同Auth Server 的代码里根本没有CharacterDatabase和WorldDatabase这两个对象。它只认LoginDatabase。如果把两个进程合并要么World Server 的代码要搬到 Auth 里但 Auth 不需要这些逻辑或者两个进程还是分开跑只是打包成一个可执行文件那还不如不合并2. 主循环模型不同World Server 有一个游戏主循环WorldUpdateLoop()while(!World::IsStopped()){sWorld-Update(diff);// diff 是两帧之间的时间差毫秒}这个循环是 World Server 的心跳每隔几毫秒更新全服所有 Entity 的状态玩家移动、怪物AI、技能冷却、副本计时……。Auth Server没有这个循环。它的main()最后一行是ioContext-run();// 事件驱动有连接来了才处理Auth Server 是纯事件驱动的客户端连上来 → 处理登录 → 返回 Realm 列表 → 断开。没有「持续更新游戏世界」的需求。3. 线程模型不同World Server 支持多线程intnumThreadssConfigMgr-GetOptionint32(ThreadPool,2);for(inti0;inumThreads;i){threadPool-push_back(std::thread([ioContext](){ioContext-run();}));}Auth Server 是单线程的代码注释里写了NOTE: While authserver is singlethreaded you should keep synch_threads 1.——因为登录请求量相对小单线程足够。4. 故障隔离World Server 有一个FreezeDetector冻结检测器if(msTimeDifffreezeDetector-_maxCoreStuckTimeInMs){LOG_ERROR(server.worldserver,World Thread hangs for {} ms, forcing a crash!,msTimeDiff);ABORT(World Thread hangs for {} ms, forcing a crash!,msTimeDiff);}如果 World 主循环卡住超过设定时间直接主动崩溃crash让守护进程重启它。Auth Server没有这个机制。如果 Auth 卡住了已经在游戏里的玩家不受影响——因为他们的连接是跟 World Server 建立的跟 Auth 无关。5. 横向扩展方式不同一个 Auth Server 可以对应多个 World Server。看worldserver/Main.cpp的LoadRealmInfo()QueryResult resultLoginDatabase.Query(SELECT id, name, address, port FROM realmlist WHERE id {},realm.Id.Realm);每个 World Server 进程只加载自己的realm.Id.Realm从配置文件里读。多个 World Server 可以连同一个 Auth 库共享同一份realmlist表。这种架构下Auth 可以只部署一个World 可以按 Realm 横向扩展。进程间通信几乎没有Auth 和 World 之间没有直接通信。严格来说World Server 启动时会修改realmlist表// 启动时设自己为「在线」LoginDatabase.DirectExecute(UPDATE realmlist SET flag flag ~{} WHERE id {},REALM_FLAG_VERSION_MISMATCH,realm.Id.Realm);// 关闭时设自己为「离线」LoginDatabase.DirectExecute(UPDATE realmlist SET flag flag | {} WHERE id {},REALM_FLAG_OFFLINE,realm.Id.Realm);但这是写数据库不是进程间直接通信。Auth Server 读realmlist表来获取 World 的状态中间隔着数据库这一层。这种「无直接通信」设计的好处Auth 和 World 可以部署在不同的机器上只要它们能访问同一个 Auth 数据库。回到开头为什么是两个进程答案已经从代码里找到了数据库访问Auth 只连 Auth 库World 连全部三个库代码层面就分开了主循环World 有游戏主循环Auth 是纯事件驱动模型不同线程模型World 支持线程池Auth 是单线程故障隔离World 有冻结检测主动崩溃重启Auth 崩了不影响已在线玩家横向扩展Auth 可以只部署一个World 可以按 Realm 横向扩展这套设计不是 AzerothCore 独创的而是从 MaNGOS 时代就定下来的——但 AzerothCore 的代码把「职责分离」做得更彻底了Auth Server 的StartDB()里连CharacterDatabase的对象都没有从编译层面就杜绝了越权访问的可能性。下次点开客户端登录时你可以想一下不到三秒的登录过程背后是两个独立进程协作完成的。一个验证你的身份然后把接力棒交出去另一个接过棒子、加载你角色所在的整片天地。