简介:这是一份 Droiyan Online 在线服务端的 2012 年版源代码工程,面向使用 C++ 和 Visual Studio 的中高级开发者,主要解决旧版网络服务端工程散佚、难以编译和模块关系不清的问题,也可作为二次开发的基础。项目围绕目录管理、数据库操作、消息通信、错误日志、服务主程序等核心模块展开,各模块之间有清晰的接口划分;压缩包内保留了可编译的 Visual Studio 2012 工程配置,可直接打开构建并跟踪调试,便于理解在线应用从启动、接收请求、处理数据到记录日志的完整路径。整个压缩包共 86 个文件,大小约 79.89MB,主要包含 C++ 源文件与头文件、Visual Studio 工程和编译配置、编译中间文件,以及少量可执行程序、图标和说明文档,能覆盖从源码阅读、环境还原、构建编译到运行调试的完整学习链路。目前已有 427 人学习下载,适合想研究旧版游戏服务端模块拆分、数据层设计、通信与日志处理机制的 C++ 开发者。
1. droiyan neo 目录服源码:一份能编译的 2012 年 C++ 在线服务端
做 Windows 在线游戏服务端的人,手里多少有几个老项目,Droiyan Online 就是这类典型——C/S 架构,服务端跑在 Windows 上,Visual Studio 2012 工程,代码里能看到 BadukDir 服务模块、消息分发、数据库记录集、错误日志一整套链路。这份 v2012_Dir 源码包不是残缺的片段,BadukDirCom.cpp、Msg.cpp、ServiceMain.cpp、Database.cpp、Recordset.cpp 这些核心文件都在,能直接开箱编译。
它解决的是什么问题?如果你想找一份完整的 VC++ 服务端源码参考,或者你要接手一个 2012 年前后的在线游戏/平台服务端,这份代码能让你看到老一代 MFC + Socket + 数据库服务的真实组织方式:服务怎么注册、消息怎么分发、记录集怎么操作、日志怎么落地。适合的人群是:用 VS2012 做维护开发的 C++ 工程师、想从零看一遍服务端骨架的进阶学习者。它的价值在于「能编译、能跑、链路完整」,不在于代码风格有多现代。
2. 源码结构拆解:从 vcxproj 到 ServiceMain,先看懂入口再谈编译
2.1 文件清单里哪些是要的,哪些是 VS 自动生成的垃圾
拿到压缩包,别急着双击 sln。先把文件分成三类:工程入口、源码本体、VS 副作用产物。工程入口是 BadukDir.sln、BadukDir.vcxproj、BadukDir.vcxproj.filters,这三个文件决定了 VS 2012 能否正确加载整个项目。源码本体是 .cpp/.h 文件,BadukDir.cpp、Msg.cpp、Net.cpp、Database.cpp、Recordset.cpp、ServiceMain.cpp、ErrorLog.cpp、Userset.cpp、BadukDirCom.cpp、StdAfx.cpp,这些才是你要读和改的东西。剩下的 .sdf、.suo、.ipch、.tlog、.pdb、Debug/Release 目录里的 .obj 和 .log,全是 IDE 和编译器的缓存产物,可以直接删掉,不影响重新编译。
有个文件值得单独说:Msg.cpp.bak。这是自动生成的备份副本,.bak后缀说明编码者曾经在编辑过程中被 IDE 坑过一次,主动留了后悔药。你整理代码时不要删它,但也不要把它加进工程——VS2012 只认 .cpp 后缀的文件,.bak加进去反而会触发编译错误。
2.2 工程配置里埋着几个关键开关:字符集、MFC 库、预编译头
打开 BadukDir.vcxproj,右键属性页,第一件事看「常规」选项卡里的字符集。2012 年的 MFC 项目常见的坑是「使用 Unicode 字符集」和「使用多字节字符集」二选一,如果代码里大量用了 CString 和 TCHAR 宏,字符集选错会编译出一堆 C2664 错误(无法将参数从 const char* 转换为 LPCWSTR)。我会直接看 StdAfx.h 里有没有#define _UNICODE或#define _MBCS,有就按它来,不要自己拍脑袋改。
第二件事是「常规」里的 MFC 使用方式,选「在共享 DLL 中使用 MFC」还是「在静态库中使用 MFC」。服务端程序如果不考虑部署到没有 MFC 运行库的机器,用共享 DLL 就行,Debug 版本编译出来的 exe 体积小很多。第三件事是 C/C++ → 预编译头,工程里既然有 StdAfx.h,就必须选「使用 /Yu”,否则每编译一个 cpp 都会重编一次 stdafx,浪费时间不说,还容易出 LNK2005 重复定义错误。这三个开关改完之后,再去动业务代码。
2.3 入口函数在 ServiceMain.cpp:Windows 服务模式和控制台模式
看源码先找 main 或 WinMain。ServiceMain.cpp 这个文件名已经暗示了它的定位——服务主程序。2012 年的在线游戏服务端有两套跑法:注册成 Windows 服务(ServiceMain + Service Control Manager)和直接控制台运行。代码里往往用_tWinMain做入口,然后根据启动参数决定是否调用StartServiceCtrlDispatcher。
常见做法是:
// 伪代码,示意入口分发逻辑 int _tWinMain(int argc, TCHAR* argv[]) { // 带 -console 参数时走控制台模式,方便调试 if (argc > 1 && _tcscmp(argv[1], _T("-console")) == 0) { // 直接跑主循环,输出日志到 stdout return RunServerConsole(); } else { // 注册服务分发器,由 SCM 启动 SERVICE_TABLE_ENTRY st[] = { { _T("BadukDir"), ServiceMain }, { NULL, NULL } }; return StartServiceCtrlDispatcher(st); } }这里逻辑很直白:控制台模式是为了开发调试,服务模式是为了服务器上无人值守。启动参数-console就是切换开关,这是老 MFC 服务端最常见的调试通道。如果编译出来运行时发现没有窗口也没有日志,先检查是不是走了 StartServiceCtrlDispatcher 分支,在服务列表里找 BadukDir 这个名字。
3. VS2012 编译与重建:把 v2012_Dir 跑起来的关键步骤
3.1 用官方 IDE 编译:sln 双击之后的三步检查
如果你本机装的是 VS2012(或 VS2013,工程是 vcxproj 格式,VS2013 也能打开),步骤应该是这样:
- 双击 BadukDir.sln,等待加载完成。如果弹出版本升级提示,选「不升级」优先,老项目升级 VC++ 工具集容易引发一堆链接错误,能编译就先跑原版。
- 打开「生成 → 配置管理器」,确认活动解决方案配置。默认如果是 Debug,先切到 Release,服务端代码一般要 Release 才有性能意义。
- 右键 BadukDir 项目 → 属性 → 配置属性 → 常规,把「平台工具集」设为 Visual Studio 2012 (v110)(VS2013 环境下是 v120)。这一步不设对,编译器版本和 STL 实现不一致,下面会报一堆 link 错。
然后生成解决方案。如果顺利,输出目录里会出 Dir.exe(注意 exe 名叫 Dir.exe,不是 BadukDir.exe,这是工程设置的输出名,后面会讲)。
3.2 命令行编译的另一条路:给没有 VS IDE 的服务器环境
2012 年的工程文件是 vcxproj,命令行编译理论上用 MSBuild 即可。我在没有完整 IDE 的 CI 环境里常用的方式是打开「VS2012 开发人员命令提示」,直接调 MSBuild:
# 先清理缓存再编译,避免 .tlog 残留干扰 MSBuild BadukDir.vcxproj /t:Clean /p:Configuration=Release /p:Platform=Win32 MSBuild BadukDir.vcxproj /t:Build /p:Configuration=Release /p:Platform=Win32注意两点:一是 /p:Platform 必须和工程里定义的平台一致,这份工程是 Win32 平台,不要传 x64,否则会报「找不到 vcxproj 中定义的平台」;二是 MSBuild 版本要匹配,VS2012 要用对应版本的 MSBuild,直接用系统里的新版本 MSBuild 编译老工程,很多时候会因工具集版本不匹配导致 cl.exe 找不到 v110_xp 工具集。命令行编译在排错时很有用,能直接看到完整错误流,不像 IDE 里错误列表偶尔会吞信息。
3.3 编译期最常遇到的三个链接错:LNK2005、LNK2019、LNK1120
链接错误是这个项目最大的坎,我拆过的 2012 年老项目十个里有八个倒在链接阶段。第一个是 LNK2005 重复定义,十有八九是某一个 .cpp 没有包含 StdAfx.h,导致预编译头不生效,MFC 的某些符号在两个编译单元里各生成一份。
把报错的 .cpp 文件顶部加上#include "StdAfx.h",挪到所有其他头文件之前。第二个是 LNK2019 无法解析的外部符号,搜索符号名是哪个类的方法,然后去对应 .cpp 文件检查这个方法是否被注释或者写了#ifdef分支排除掉了。第三个是 LNK1120,这是前两个问题的汇总,不用单独处理,解决完上面的原因自然会消失。
还有个 vc120.pdb 文件,别被它误导。如果你用 VS2013 重新编译,会生成 vc120.pdb,而 VS2012 生成的是 vc110.pdb。如果链接器报「fatal error LNK1313: 检测到 .netmodule 或混合模块」,那是编译器版本和链接器版本不匹配,整个解决方案统一工具集版本即可解决。
4. 消息、网络与数据库链路:Msg / Net / Recordset 三条线的配合
4.1 网络层 Net.cpp:Socket 收发和粘包处理是老项目的重灾区
Net.cpp 负责网络通信,这份代码里网络层是典型的 select 模型还是完成端口模型,打开文件看第一屏就能判断。2012 年的目录服一般用的是 select + 多线程,因为连接数不会特别大,选 select 在 Windows 上维护成本低。你看到代码里有select()调用和 fd_set 结构,就是这一类。
粘包问题是网络层最关键的处理逻辑。常见做法的拆包思路是:包头固定 4 字节存长度,后面跟消息体。代码里一定有类似这样的逻辑:
// 伪代码:拆包逻辑示意 bool HandleReceive(SOCKET s) { char header[4]; int ret = recv(s, header, 4, 0); if (ret != 4) return false; // 没收到完整包头,等下次 int bodyLen = *(int*)header; // 小端解释长度 if (bodyLen <= 0 || bodyLen > MAX_PACKET_SIZE) return false; // 长度异常,直接断开 char* body = new char[bodyLen]; int received = 0; while (received < bodyLen) { int n = recv(s, body + received, bodyLen - received, 0); if (n <= 0) { delete[] body; return false; } received += n; } // 整包完整,交给消息分发器 DispatchMessage(s, body, bodyLen); delete[] body; return true; }这里我一般会特别关注两个参数:MAX_PACKET_SIZE的上限,老代码里如果这个值是 64KB 或 1MB,能间接看出当初的业务包规模。还有 recv 的返回值判断,只看 n == 0 就断,n < 0 不做 errno 处理,这是常见的健壮性问题,但不影响了解主逻辑。你接手后如果要改协议,一定要改包头长度字段和拆包循环的对应关系,别只改发的不改收的。
4.2 消息分发 BadukDirCom.cpp:命令字到处理函数的映射表
有网络收包就得有分发。BadukDirCom.cpp 这个文件名里的 Com 不是 COM 组件,而是 Communication 的缩写,它负责把收到的消息体按命令字路由到具体处理函数。老项目的分发逻辑普遍是 switch-case 或函数指针数组,打开文件后找DispatchMessage或者HandleCommand,里面一定是几千行的 case。
整理分发关系时,最实用的做法是画一张命令字对应表。表格形式如下:
| 命令字 | 处理函数 | 功能 | 是否涉及数据库 |
|---|---|---|---|
| 0x0001 | OnLogin | 登录验证 | 是(Recordset) |
| 0x0003 | OnGetDir | 获取目录列表 | 是(Database) |
| 0x0010 | OnPing | 心跳 | 否 |
| 0x00FF | OnDisconnect | 断线清理 | 否 |
拿到代码后第一步先做这个表,再往函数体里钻。没有这个映射关系,你读消息处理代码会非常痛苦——每个 case 都是几十行业务逻辑,看到后面忘了前面。做这张表的过程也是检验代码是否有重复命令字的过程,老代码里经常出现两个 case 分支调同一个函数,这种是历史遗留,不要纠结为什么,记下来就行。
4.3 Database.cpp 与 Recordset.cpp:MFC 的 CDatabase 和 CRecordset 怎么配合
数据库层是这份源码的另一个重点。Recordset.cpp 对应 MFC 的 CRecordset 派生的记录集类,Database.cpp 对应 CDatabase 连接管理。MFC 的数据库操作套路非常固定:先 CDatabase::Open 建立连接,再构造 CRecordset 子类,调用 Open 执行 SQL,然后遍历记录集取字段。
// 伪代码:MFC 记录集查询示意 CDatabase db; db.Open(_T("DSN=BadukDirDB;UID=sa;PWD=***")); // 数据源连接串 CRecordset rs(&db); rs.Open(CRecordset::snapshot, _T("SELECT dir_id, dir_name FROM dir_info WHERE status=1")); while (!rs.IsEOF()) { long dirId; CString dirName; rs.GetFieldValue((short)0, dirId); rs.GetFieldValue((short)1, dirName); // 处理一行目录数据 rs.MoveNext(); } rs.Close(); db.Close();老代码里常见的坑是连接串直接写死在代码里,还有 DSN 依赖本机 ODBC 配置。你如果要在自己机器上跑起来,需要先到「管理工具 → ODBC 数据源」里建一个同名 DSN,否则 Database.cpp 里的CDatabase::Open会直接抛异常。另一个常见问题是CRecordset::snapshot和CRecordset::dynaset的选择,snapshot 是静态快照,适合目录查询;dynaset 实时同步,适合频繁更新的业务。实现里如果混用,注意打开记录集之前要保证 CDatabase 是 Open 状态,否则 CRecordset 构造时直接断言失败。
4.4 ErrorLog.cpp 与 Userset.cpp:日志系统和配置模块为什么会影响启动
ErrorLog.cpp 是日志模块,写法一般是封装一个写文件的静态方法,带时间戳和错误级别。我拆过的老项目里这个模块往往最简单但也最关键——服务端挂了之后,能还原现场的就是这一个日志文件。打开 ErrorLog.cpp 看它的写文件方式:声明为static void WriteLog(const CString& msg, int level)这类接口,内部用CFile::Write还是fprintf,决定了它是 MFC 风格还是纯 C 风格。
Userset.cpp 是用户配置加载,大概率是读取一个 ini 文件。压缩包里确实有dir.ini,这就是服务端的核心配置文件。看看代码里读取 ini 的键名,比如[Server]节下的Port、MaxUser,这些参数直接影响服务端启动后监听哪个端口、最多容纳多少连接。如果你编译完运行发现端口对不上,先去 dir.ini 里改配置,不要动代码。
5. 避坑记录:老项目复现的五个典型翻车现场
5.1 现象:双击 Dir.exe 后闪退,进程消失了
- 原因:最可能是没有以管理员权限运行。2012 年的服务端程序要绑定端口和写日志文件,Windows 7/10 下 UAC 拦截会导致绑定失败,而代码里又没有写清楚错误提示,直接进程退出。
- 解决:右键 exe → 属性 → 兼容性 → 勾选「以管理员身份运行此程序」。这属于运行环境问题,和代码无关。如果还退,检查 dir.ini 里配置的端口是否被占用,
netstat -ano | findstr 端口号看输出。
5.2 现象:编译报错 error C2664:无法将参数从“const char [N]”转换为“LPCTSTR”
- 原因:工程字符集设置和代码里字符串常量不匹配。代码里写了
"some string"这种窄字符串直接传给 LPCTSTR(Unicode 下是 LPCWSTR),MSVC 直接报硬错误。 - 解决:把字符串常量改成
_T("some string"),或者统一把工程字符集切到「使用多字节字符集」。我一般先看 StdAfx.h 里有没有定义_UNICODE,有就全改成 _T 包裹,没有就切多字节,二选一,不要混着来。
5.3 现象:链接报错 LNK2001:无法解析的外部符号 _WinMain@16
- 原因:入口函数不对。这是控制台子系统和窗口子系统的经典冲突:如果工程配置里「链接器 → 系统 → 子系统」设成了
CONSOLE,而代码里是_tWinMain,链接器找不到 WinMain 就对不上。 - 解决:把子系统改成
WINDOWS,或者在代码里加#pragma comment(linker, "/SUBSYSTEM:WINDOWS")。反过来,如果你是想看控制台日志,就把子系统设成 CONSOLE,同时确保代码入口是_tmain或main。不要既想弹窗口又想打 stdout,2012 年老代码没这么灵活。
5.4 现象:运行时报“未找到 MFC120.DLL”或“未找到 MSVCR110.DLL”
- 原因:编译用的是共享 MFC 和共享 CRT,目标机器上没装对应版本的运行库。VS2012 对应的是 VC11 运行库,VS2013 对应 VC12,很多服务器为了省事没装。
- 解决:要么编译时改成「在静态库中使用 MFC」,C/C++ → 代码生成 → 运行库改成「多线程 (/MT)」,这样 exe 体积大但免依赖;要么把对应的 vcredist_x86.exe 一起部署到目标服务器。我做服务端部署一般选静态编译,省得以后为运行库版本扯皮。
5.5 现象:数据库操作抛异常,提示“连接失败”或“驱动不支持”
- 原因:代码里用的是 ODBC 连接,依赖本机的 ODBC 数据源名(DSN),而这个 DSN 在部署机器上没建。这不是代码 bug,是环境配置缺失。
- 解决:打开「控制面板 → 管理工具 → ODBC 数据源管理器(32 位)」,添加系统 DSN,驱动选「SQL Server」,名称填代码里连接串对应的 DSN 名(看 Database.cpp 里的
db.Open第一个参数),服务器和数据库名按实际环境填。填好后用代码里同样的连接串写个小程序先测一遍,通了再跑服务端,节约排查时间。
6. 动态调试与日志定位:从 ServiceMain 到 ErrorLog 的验证手法
6.1 用 -console 参数启动,把日志拉到眼前
这份工程既然有 ServiceMain 和 -console 的切换,调试时我永远先用控制台模式。把 Dir.exe 放到 Release 目录下,命令行执行Dir.exe -console,这样日志会直接打到当前窗口,配合 dir.ini 里的日志级别开关(如果代码里有),能实时看到连接、消息、错误的完整流转。用这种方式先确认三件事:监听端口起来没有、数据库连接成功没有、收到第一个网络包时消息分发器有没有走到对应 case。服务模式下的日志反馈太慢,不适合对代码还陌生时的第一轮验证。
6.2 用 VS 附加进程方式打断点
如果控制台模式下某个消息处理分支一直没触发,需要用调试器。打开 VS2012 → 调试 → 附加到进程 → 选中 Dir.exe,然后在 OnGetDir 这类处理函数入口下断点。这里有个老项目特有的麻烦:Release 编译默认优化开了内联,函数调用可能被优化调,断点下不进去。碰到这种情况别怀疑代码,直接把工程的优化关掉重新编一次 Debug 版本再附加。Debug 版本相关源码路径对不上时,VS 会提示「找不到源文件」,指定到 BadukDir.cpp 实际路径即可。
6.3 验证数据库层:临时改连接串打出记录集数量
如果怀疑 Recordset 查询有问题,我常用的手法是在查询后临时加一行日志输出,把记录集行数打出来。方向是先确认 SQL 语句在查询分析器里能查到数据,再确认 MFC 参数绑定没问题。这一步如果发现记录集数量为 0,大概率是代码里的表名/字段名和实际数据库不一致,老项目的 SQL 都是硬编码的,换库要全局搜表名。SQL 语句里如果有FROM dir_info WHERE status=1这种条件,就把整个 SQL 串复制到数据库客户端里原样执行一次,立刻能定位是数据问题还是连接问题。
6.4 从 ErrorLog 文件追溯崩溃现场
有 ErrorLog 模块的工程,崩溃后第一件事一定是打开日志文件的最后百行。ErrorLog.cpp 里的写日志函数如果带了时间戳和错误码,按时间倒序找最后一个时间点对应的动作——是网络收包、数据库查询还是消息分发。2012 年的服务端没有现代链路追踪,日志只有顺序文本,追溯方式就是「最后一个成功动作往下就是崩溃点」。我见过一个崩溃案例,日志最后一行停在 OnLogin 查询数据库之后、返回包发送之前,最终定位是发送缓冲区分配失败没做空指针判断。这个习惯我一直保留到现在:
每个新接手的服务端项目,先跑步起来,再看日志格式,然后模拟一次登录流程走全链路,确认日志覆盖到每一步。这个流程走完之后,你才算真正接住这份代码。希望帮到你。
本文还有配套的精品资源,点击获取