简介《GameServer97d-Source_muonline_》是一套面向经典网游《奇迹MU Online》的服务器端源代码适合想深入研究老牌MMORPG服务器架构、学习C服务端开发或计划搭建私人服务器的开发者与游戏爱好者。资源包共103个文件压缩后大小仅149KB主体为49个.h头文件与46个.cpp源文件另附Visual Studio工程配置、资源文件等整体精简适合直接阅读、编译和二次修改。代码覆盖玩家连接与指令处理、战斗伤害计算、角色属性成长、物品交互、服务器配置、协议解析与反外挂防御等模块从基础通信到高阶逻辑均有体现同时还有针对决斗、重置等玩法的独立处理单元可作为理解游戏状态同步与服务器并发模型的学习样本。目前已有393人浏览学习对自建服务器或研究旧式游戏服务端代码的读者而言这个小巧的源码包提供了清晰的项目骨架和可运行的逻辑参考能帮助快速上手并延伸出个性化改造。1. GameServer97d-Source_muonline_ 是什么一份不该被遗忘的服务端源码值得读懂它很多人第一次看到 GameServer97d-Source_muonline_ 这个名字会以为它不过是一堆过时的 C 老代码。但真正打开过 0.97d 的 GameServer 源码后你会发现它几乎没有现代框架的遮罩Winsock 裸接口、内存池、存储过程、还有一张张写满协议号的 switch 表。这套诞生于 2003 年前后的东西用最直接的方式告诉你一个商业 MMO 的服务端是怎么跟客户端对话的。对于想理解网络游戏服务端原理、或者自己动手搭一个可研究环境的人来说它是全网最容易读懂的起点之一。它能解决什么问题一是“看懂”源码里从 accept 到协议分发再到数据库读写链路非常干净二是“跑通”按照编译、建库、配置的步骤能在单机上搭出一套可进游戏的环境三是“扩展”比如自己加装备、加地图、改掉落规则。适合的人群很清晰刚入门服务端开发的新手、想了解老式 C/S 架构的人还有想在本地研究 MMO 数据流与怪物 AI 的从业者。我不会给你复述“源码阅读神器”的感受我会直接把源码拆干先建立对结构的认知然后给出可复现的编译、建库、调参步骤再列出最容易踩的坑最后教你用抓包和调试器验证自己的理解。这趟走完你手里这套 GameServer 不再是一个黑匣子而是一台可以拆装的机器。2. 读懂 0.97d GameServer 源码结构先搞清楚谁在跟谁说话再动手改代码0.97d 的服务端不是只有一个 GameServer。它通常由 ConnectServer连接服务器、JoinServer登录验证、GameServer游戏世界、ExDB扩展数据库和 DataServer数据落地组成。GameServer 是核心它既承担玩家之间的实时交互也要跟数据库和登录服保持长连接。如果你直接打开 GameServer 的 .dsp 工程密密麻麻的文件很容易劝退。我的习惯是先不碰编译先看它和谁的会话关系——把这张关系图装进脑子后续看代码就不会迷路。2.1 从 GameServer 目录里挖出通信骨架拿到源码先把自己变成“地图党”。常见的做法是先把关键文件挑出来。0.97d 的源码包通常按文件散落在根目录不像现在按模块分文件夹。文件名多是以 C 开头或 Protocol 开头。我一般会先全局搜一下 recv 和 send把网络层找出来。CClientManager 负责 accept数据的收发会汇聚在这里如果找不到就搜 WSAStartup那基本就是网络初始化所在。下面这张表是典型的 0.97d 源码结构具体文件名会因不同发布者略有差异但职责不会离开这几块。典型文件职责需要关注的核心函数GameServer.cpp工程入口、初始化、主循环WinMain / mainCClientManager.cpp玩家连接的 accept、收发RecvMsg / SendMsgProtocolCore.cpp按协议号分发封包ProtocolCore()CUser.cpp玩家对象、移动、技能、背包MoveReq / SkillUseCMonster.cpp怪物 AI、刷新、掉落MonsterMove / MonsterDieDB_Manager.cpp与 SQL Server 通信DBQuery / SaveUser拿到源码后建议先用一个小工具建好索引再从 ProtocolCore.cpp 的 switch 表反向追协议比从入口看要快得多。ProtocolCore 是整条链路的“总闸”里面通常有几百个 case每个 case 对应一个封包类型。比如 0x0E 是登录请求0x11 是移动请求0x26 是技能请求。看到这些数字你就知道这份源码离业务逻辑只有一步之遥。还要注意线程模型。0.97d 的 GameServer 实现很直白有的是每客户端一个线程有的是主线程加 select 轮询。源码里经常能看到 char m_szThreadId 或 HANDLE hThread 这种变量。每客户端一个线程的代码理解起来最方便但代价是内存占用高所以客户端连接数上限MAX_USERS本质就是线程池上限。这个参数会在配置阶段重点照顾。2.2 用 Source Insight 建立工程单机看代码的 3 个步骤阅读老 C 代码我推荐 Source Insight它的跳转和符号检索比文本编辑器舒服太多。网上常有人问 Source Insight 4 怎么用其实核心就三步建工程、加文件、同步索引。第 1 步新建工程。打开 Source InsightProject - New Project工程名我习惯叫 muonline_97d源码目录建议放在纯英文路径下比如 C:\GameServer97d。中文路径虽然也能用但老代码里的FILE宏带中文在调试时容易让符号解析抽风。第 2 步添加文件。Project - Add and Remove Project Files点 Add From Tree选择源码根目录。文件类型保持默认的 C/C 源文件把.h、.cpp 加进来即可不用管 .dsp 或 .rc。如果你手动挨个添加通常会有遗漏所以我一般直接整棵树勾选。第 3 步同步。Project - Synchronize Files勾选 Force all files be reloaded等进度条走完。同步完成后在任意函数名上按 CtrlClick 就能跳到定义处。如果遇到符号跳不过去不要慌先用 CtrlF 全文搜索。老代码里有些函数在宏里定义Source Insight 默认识别不了这不是工具不行是代码本身就用了宏封装。参数说明如果源码里用了 MFC 的 CString、AfxMessageBoxSource Insight 可能无法解析所有系统符号。你可以在 Options - Preferences - Languages - Symbol Lookups 里把 VC6 的 include 目录加进去但实际上不需要。我们读核心逻辑看到 CString 就当成字符串类看到 AfxMessageBox 就当输出框不影响理解。还有一个小技巧按 F8 打开符号窗口输入 ProtocolCore能直接定位到所有相关函数比一层层点菜单高效得多。顺便说一句网上关于 Source Insight 4 序列号或破解的讨论非常多其实官方试用版就够用读这份源码完全不需要折腾授权问题。我见过不止一个新手为了找注册码浪费时间最后还没开始读代码就放弃了太不值。2.3 核心数据流登录、选服、进游戏时 GameServer 做了什么理解 GameServer 的最快方式是跟四个关键请求登录、选服、进入游戏、移动。一条完整链路走完你就能把源码里的主要模块串起来。登录阶段客户端启动后先连 ConnectServer 的 44405 端口。ConnectServer 收到“请求服务器列表”封包返回所有 GameServer 的 IP 和端口。客户端选定服务器然后直连 GameServer 的 55901。此时 GameServer 创建 CUser 对象状态为“未验证”。它接收登录请求包协议号 0x0E取出账号和密码发送给 JoinServer 验证。JoinServer 查询数据库返回结果GameServer 再告诉客户端成功或失败。选服与角色登录成功后客户端发送“选角色”请求。GameServer 从数据库里读出角色列表返回给客户端。这里会触发一系列存储过程比如 P_GetUserData。角色数据包含等级、经验、背包、坐标。GameServer 把这些数据放到内存对象然后向客户端下发。如果你在 GameServer.cpp 里看到调用 DB_Manager 的某段代码一定是在做这类“拉用户快照”的操作。移动与广播进入游戏后移动是最频繁的动作。客户端发送移动封包格式是“头部 协议号 坐标 朝向”。GameServer 收到后第一件事不是校验坐标而是把这个位置广播给地图上的其他玩家。0.97d 的做法非常简单转发给“视野范围内的所有在线用户”。如果你把 CUser 类里的 SendAll 相关代码翻出来会发现一路上全是底层广播函数。这里先看一个封包头结构这是所有协议共同的部分#pragma pack(push, 1) typedef struct _MSG_HEADER { BYTE head; // 协议号0xC1短包0xC2长包 BYTE size; // 整个封包长度 BYTE subcode; // 子协议号 } MSG_HEADER, *PMSG_HEADER; #pragma pack(pop)逻辑说明所有从客户端收到的数据前两个字节都是头部。head 是 0xC1 时整个封包长度单独用 size 表示最多 255 字节head 是 0xC2 时后面还会跟两个字节的长度适合更长的数据。ProtocolCore.cpp 里每个 case 分支都会先 memcpy 取出消息头再根据 subcode 做后续处理。你抓包时看到 0xC1 或 0xC2 开头就能立刻判断封包类型。对应的移动处理在源码里大致长这样case 0x11: // 客户端移动请求 if (pUser pUser-IsValid()) { BYTE x packet[3]; BYTE y packet[4]; BYTE dir packet[5]; pUser-Move(x, y, dir); // 这里会把位置广播到附近玩家 } break;这段是示意代码具体变量名和偏移量因版本而异。但注意这里没有校验 x/y 是否越界这就是为什么当年有各种穿墙外挂。阅读源码时看到这类问题你可以顺手补一个边界判断这也是二次开发的一个练手点。理解了这条链路你会明白 GameServer 在整条服务端链路里只是一个“转发 算力”角色真正的数据持久化都在数据库那边。3. 本地搭一套可运行的 0.97d 服务端编译、建库、调配置一次跑通这一章的目标是让 GameServer 真正跑起来。你需要准备Windows 环境虚拟机也行、SQL Server 2005/2008、一份完整的 0.97d 源码包以及对应的客户端。整个过程分四步编译、建库、改配置、启动。每一步都有坑但都能在几分钟内解决。3.1 编译源码VS 版本选择和 3 个常见报错0.97d 源码是 Visual C 6.0 风格。在 Win10/11 上直接编译大概率会撞上 CRT 库不兼容问题。常见做法是把源码放到 Windows XP 虚拟机里装一个 VC6 Enterprise什么都不用改按工程文件直接编译。如果你不想装虚拟机也可以试试用 Visual Studio 2010 打开 .dsp但需要做 3 处调整。第一在 stdafx.h 里去掉某些 MFC 依赖改用 std::vector 替代第二把项目字符集设置为“未设置”第三在链接器 Input 里补上 ws2_32.lib 和 odbc32.lib。这些调整能解决大部分报错。编译报错有新手最容易碰到的三个我都遇到过直接给你避坑素材。现象 1fatal error C1083: Cannot open include file stdafx.h。原因工程依赖预编译头但编译器在源码目录里找不到该头文件。解决在 Project Settings - C/C - Precompiled Headers 里选择“不使用预编译头”或者确认当前源文件包含 #include stdafx.h且它就在源码根目录。很多老源码在 VC6 下默认开启预编译头如果改了设置依然报错就逐个检查 .cpp 开头是不是都带了 stdafx.h。现象 2error C2065: CString : undeclared identifier。原因MFC 头文件没有正确包含。解决在 stdafx.h 顶部加上 #include afxwin.h。这会让 MFC 库被加载CString、CMap 这些类型才能被识别。有些精简版源码故意去掉 MFC 依赖如果你遇到一堆 CString 报错也可以直接把 CString 替换成 std::string但改起来工作量大不如加头文件来得快。现象 3LNK2001 unresolved external symbol __WSAStartup8。原因缺少 Winsock 库。解决在链接器 Input 的 Object/library modules 里添加 ws2_32.lib并把 odbc32.lib 也加上。另外注意老工程有时会默认使用单线程库而 Winsock 需要多线程运行时所以还建议在 C/C 代码生成设置里选择“Multithreaded DLL”即 /MD。编译成功后生成 GameServer.exe。记得把它放在与 GameServer.ini 同级的目录下否则启动时会找不到配置直接退出。老程序的工作目录就是当前目录它不会去别的文件夹找配置。3.2 初始化数据库SQL 脚本执行顺序和账号表说明0.97d 服务端依赖 SQL Server 2000/2005。现代环境我推荐 SQL Server 2005 Express体积小兼容性最好。SQL Server 2008 R2 也能用但需要把数据库兼容模式降到 90。数据库主要有四个MuOnline、Me_MuOnline、Log、Ranking。源码包里通常有一个 sql 或 database 目录里面包含 CreateDatabase.sql、StoredProcedures.sql、Ranking.sql、Log.sql。执行顺序必须遵守先建库建表再建存储过程最后建日志和排行榜。如果顺序颠倒存储过程会因为找不到表而报错。用命令行执行最顺手sqlcmd -S localhost -U sa -P 123456 -d master -i C:\GameServer97d\sql\CreateDatabase.sql sqlcmd -S localhost -U sa -P 123456 -d MuOnline -i C:\GameServer97d\sql\StoredProcedures.sql参数说明-S 指定服务器实例-U 和 -P 是账号密码-d 是默认数据库-i 是脚本文件路径。密码 123456 是我本地测试用的实际请换成你的 sa 密码。如果脚本里有 GO 分隔符sqlcmd 默认支持不用担心。执行完没输出错误就说明成功。创建完的库中最核心的表是 MEMB_INFO 和 Character。MEMB_INFO 存账号Character 存角色。老代码没有密码哈希账号密码都是明文存储这也是当初那个时代常见做法。如果你想手动插一个测试账号用这条 SQLUSE MuOnline; GO INSERT INTO dbo.MEMB_INFO (memb_id, memb__pwd, memb_name) VALUES (test01, 123456, Tester); GO逻辑说明memb_guid 是自增主键你不需要手动填。memb_id 是登录名memb__pwd 注意是两个下划线这是原版字段名写少一个下划线插入会失败。GameServer 验证时会调用存储过程 P_CheckUser参数就是 memb_id 和 memb__pwd。如果你插入了记录但登录仍然失败多半是存储过程没建好。打开 SQL Server Management Studio展开 MuOnline 数据库 - 可编程性 - 存储过程确认能看到 P_CheckUser、P_GetUserData 等一长串名字。3.3 修改 GameServer.iniIP、端口、经验倍率等参数怎么调0.97d 的源码里配置可能放在 GameServer.ini也可能散落在代码中的常量定义里。最典型的 GameServer.ini 内容如下[ConnectServerInfo] IP 127.0.0.1 PORT 44405 [GameServerInfo] IP 127.0.0.1 PORT 55901 MAX_USERS 100 [Version] MAIN_VERSION 0.97.08 SERIAL OMNI3456 [Rate] EXPERIENCE 1 MONEY 1 DROP 1逐项说明ConnectServerInfo 是客户端最先访问的地址如果在同一台机器上联调就用 127.0.0.1。GameServerInfo.PORT 是客户端选服后直连的端口比如 55901。MAX_USERS 是最高在线人数它直接决定线程池大小和内存占用单机测试改 5 或 100 都行不要一开始就设成千人否则老程序可能因申请内存失败而启动崩溃。MAIN_VERSION 和 SERIAL 必须和客户端 main.exe 一致否则客户端会提示版本错误。EXPERIENCE / MONEY / DROP 是倍率但注意这些值并不一定被所有源码读取。有的源码版本里配置不是从 ini 读的而是在 GameServer.cpp 里写死比如#define MAX_EXP_RATE 1。如果你改了 ini 没效果就全局搜 EXPERIENCE 或 RATE 关键字直接改常量并重新编译。配置文件很多时候只是给别人看的门面真实参数在代码里。所以我的建议是先编译完再改配置先用默认倍率跑通再逐项调。还有一个老生常谈的问题GameServer.ini 的编码必须是 ANSI不能是 UTF-8 with BOM。用记事本另存时选择“ANSI”编码。用 VS Code 默认写 UTF-8保存后启动会报配置解析失败我当年在这个坑里浪费过一个晚上。3.4 启动顺序和自检清单把编译好的文件按目录放好参照这个结构C:\GameServer97d ├── ConnectServer\ConnectServer.exe ├── JoinServer\Joinserver.exe ├── GameServer\GameServer.exe ├── GameServer\GameServer.ini └── data\本地单机启动顺序我习惯用一条批处理echo off net start MSSQLSERVER start /D C:\GameServer97d\JoinServer Joinserver.exe /p 55970 timeout /t 2 start /D C:\GameServer97d\ConnectServer ConnectServer.exe start /D C:\GameServer97d\GameServer GameServer.exe逻辑说明先把 SQL Server 拉起来再启动 JoinServer让 GameServer 能注册到数据库端口ConnectServer 何时启动影响不大只要在客户端连接时已经在监听即可。/p参数指定 JoinServer 的监听端口GameServer 启动时会按这个端口去连。start /D是为了指定工作目录因为老程序默认读取当前目录下的配置文件不指定目录就会去系统目录找然后找不到。启动后自检清单SQL Server 的 MSSQLSERVER 服务处于“正在运行”。JoinServer 控制台没有数据库连接报错。GameServer 控制台出现所有模块初始化完成日志停在等待连接状态。netstat 能看到 44405、55901、55970 三个端口在监听。数据库 MuOnline 里 P_CheckUser 存储过程存在。客户端 main.exe 的版本串与 GameServer.ini 一致。如果 GameServer 控制台弹出[Error] DBOperator Connect Fail立刻去查数据库连接字符串而不是去改端口。数据库连接串通常在 GameServer.cpp 里硬编码用 SA 密码写死的。把密码改成你自己的就行。4. 搭建与调试中的 5 个典型坑现象、原因和解决方式老源码调试最考验耐心因为报错信息少很多问题要靠猜。下面这 5 个坑是我自己跑 0.97d 服务端时反复遇到的写出来帮你少走弯路。4.1 GameServer 启动报“JoinServer Connect Fail”现象启动 GameServer 后不到一秒钟控制台弹出 JoinServer Connect Fail然后进程退出。原因JoinServer 没启动或者 GameServer 里配置的 JoinServer 端口与/p参数不一致。解决先把 JoinServer 启动起来确认它监听了 55970再去 GameServer.cpp 里搜索 JOIN_PORT 或 WaitSock把端口改成一样。如果两边都在本机IP 就用 127.0.0.1不要用机器名避免 DNS 解析失败。还有一个容易忽略的点JoinServer 也要能连上数据库否则它虽然启动但 GameServer 去握手时会被拒绝表现同样是 Connect Fail。4.2 客户端能进服务器列表但点确认后提示“连接中断”现象ConnectServer 列表正常能看到服务器名称但一点“确认”或“进入”客户端立刻弹出与服务器的连接中断。原因GameServer 的 MAIN_VERSION 或 SERIAL 与客户端不匹配GameServer 在收到登录包后主动断开。也可能是 MAX_USERS 已满或者登录验证封包解析失败。解决用十六进制编辑器打开客户端 main.exe搜索版本号字符串把 GameServer.ini 里的 MAIN_VERSION 和 SERIAL 改成一致。有些源码包用加密序列号需要在开发端重新生成后再打包 main.exe这个细节要看源码里的注释。如果你确认版本没问题就把 MAX_USERS 临时改成 5排除满员误判。4.3 能进游戏但地图上怪物全部不动现象角色可以移动、说话怪物全部站在原地不攻击也不刷新。原因GameServer 的怪物 AI 线程没启动或者地图的怪物分布数据没有加载成功。解决检查 data\Monster 文件是否存在再看 GameServer 日志里有没有 Map Load Fail 记录。0.97d 的怪物坐标写在文本文件里编码必须是 ANSI。如果编码是 UTF-8读取坐标时会出现负数AI 自然失效。另外要确认 GameServer 的工作目录下确实有 data 目录很多情况下是工作目录不对导致加载失败。4.4 SQL Server 2008 中执行原版脚本报错现象执行 CreateDatabase.sql 时SQL Server 2008 报“sp_addtype”不存在脚本中断。原因老脚本使用了 SQL Server 2000 的兼容语法在 2008 中被移除。解决先开兼容模式ALTER DATABASE MuOnline SET COMPATIBILITY_LEVEL 90;把数据库降到 SQL Server 2005 级别。如果还报错就把脚本里的 sp_addtype 手动替换为 CREATE TYPE。最省事的方案是直接安装 SQL Server 2005 Express这是与 0.97d 源码最匹配的组合没有之一。4.5 倍率修改后没有效果现象把 GameServer.ini 里 EXPERIENCE 改成 100打怪经验没任何变化。原因源码没有读这个配置文件倍率是编译期常量写在 .cpp 里。解决全局搜索 EXP_RATE 或 MAX_EXP 这类宏名找到后直接改常量重新编译。老代码里经常出现这种“配置假象”文件里写着看起来能调的东西实际代码根本没解析它。改完重编译替换 exe 再启动倍率才会生效。这也是为什么我前面强调“先编译完再改配置”因为有些时候你调的参数根本不在 ini 里。5. 深挖 GameServer 的进阶验证抓包、附加调试和自写 GM 命令当你把服务端跑通下一步就是验证自己是否真的看懂了协议。这里给你三个实用技巧每个都能让源码里的逻辑在真实运行中得到印证。5.1 用 Wireshark 验证封包从抓包中反推协议本地抓包最方便。打开 Wireshark选择 Npcap Loopback Adapter过滤条件填tcp.port55901然后登录游戏、移动一步、停止抓包。你会找到一个长度为 8 左右的 TCP 包数据往往是c1 05 11 x y dir。对照 ProtocolCore.cpp 里的 0x11 分支你会发现服务端处理的正是这个结构。这个小实验能快速确认“服务端没有做二次校验”的结论也能验证你改的协议代码是否真的生效。抓包时一定要选 Loopback 接口不要选以太网卡否则本地通信根本看不到。5.2 附加调试在 recv 上下断点如果你有 VC6 调试环境直接在 CClientManager::RecvMsg() 下断点。如果没有用 OllyDbg 附加到 GameServer.exe在 ws2_32.dll 的 recv 函数下断点。收到封包后看一眼缓冲区开头两个字节对照长度。这里有个隐藏坑GameServer 是多线程的断在 recv 后要防止线程切换建议先把游戏客户端和服务端的所有线程暂停再单步执行否则观察窗口会乱跳。用惯了现代调试器再看这种老方式你会明显感觉到原始但有效。5.3 自写一条 GM 命令把猜测变成功能添加 GM 命令是很好的综合练习。在 ProtocolCore.cpp 里找一个未被占用的协议号比如 0x45添加一个 case调用你自己写的函数。比如给玩家背包加一件装备void GM_AddItem(CUser* pUser, BYTE itemCode) { if (!pUser) return; pUser-m_Inventory.AddItem(0, itemCode); // 0号槽添加物品 }注意这是伪代码具体 Item 对象和 AddItem 的签名要以你下载的源码为准。加入后重新编译在客户端用一个包含协议号 0x45 的简单登录器发送请求验证背包出现物品。这样你就完成了一次从封包到逻辑的服务端扩展。整个过程会让你真正理解封包进来协议分发业务处理响应回去——这就是 GameServer 的全部秘密。老代码里夹着很多这样的隐藏练习。当年我为了搞懂 AddItem 参数把地址和代码一直翻到凌晨最后发现只是把装备代码和顺序弄反了。从那以后我改任何封包前都会先在抓包里描一次结构再动手。这份源码值得你花两三个晚上希望帮到你。本文还有配套的精品资源点击获取