简介:《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 / main |
| CClientManager.cpp | 玩家连接的 accept、收发 | RecvMsg / SendMsg |
| ProtocolCore.cpp | 按协议号分发封包 | ProtocolCore() |
| CUser.cpp | 玩家对象、移动、技能、背包 | MoveReq / SkillUse |
| CMonster.cpp | 怪物 AI、刷新、掉落 | MonsterMove / MonsterDie |
| DB_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 Insight,Project -> 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,等进度条走完。同步完成后,在任意函数名上按 Ctrl+Click 就能跳到定义处。如果遇到符号跳不过去,不要慌,先用 Ctrl+F 全文搜索。老代码里有些函数在宏里定义,Source Insight 默认识别不了,这不是工具不行,是代码本身就用了宏封装。
参数说明:如果源码里用了 MFC 的 CString、AfxMessageBox,Source 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。这些调整能解决大部分报错。
编译报错有新手最容易碰到的三个,我都遇到过,直接给你避坑素材。
现象 1:fatal error C1083: Cannot open include file 'stdafx.h'。原因:工程依赖预编译头,但编译器在源码目录里找不到该头文件。解决:在 Project Settings -> C/C++ -> Precompiled Headers 里选择“不使用预编译头”,或者确认当前源文件包含 #include "stdafx.h",且它就在源码根目录。很多老源码在 VC6 下默认开启预编译头,如果改了设置依然报错,就逐个检查 .cpp 开头是不是都带了 stdafx.h。
现象 2:error C2065: 'CString' : undeclared identifier。原因:MFC 头文件没有正确包含。解决:在 stdafx.h 顶部加上 #include <afxwin.h>。这会让 MFC 库被加载,CString、CMap 这些类型才能被识别。有些精简版源码故意去掉 MFC 依赖,如果你遇到一堆 CString 报错,也可以直接把 CString 替换成 std::string,但改起来工作量大,不如加头文件来得快。
现象 3:LNK2001 unresolved external symbol __WSAStartup@8。原因:缺少 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 存角色。老代码没有密码哈希,账号密码都是明文存储,这也是当初那个时代常见做法。如果你想手动插一个测试账号,用这条 SQL:
USE 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.ini:IP、端口、经验倍率等参数怎么调
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.port==55901,然后登录游戏、移动一步、停止抓包。你会找到一个长度为 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 参数,把地址和代码一直翻到凌晨,最后发现只是把装备代码和顺序弄反了。从那以后,我改任何封包前都会先在抓包里描一次结构,再动手。这份源码值得你花两三个晚上,希望帮到你。
本文还有配套的精品资源,点击获取