简介:这是一份 Droiyan Online 项目 2012 年版目录服务端源码包,面向熟悉 C++ 与 Visual Studio 的网游服务端开发者。包内围绕 BadukDir 目录服务模块,提供消息处理、数据库操作、错误日志、服务主程序等核心实现,适合研究在线游戏服务端架构,也可在此基础上做更新、修复与功能扩展。压缩包共 86 个文件,大小约 79.89 MB,主要包含 cpp/h 源代码、Visual Studio 工程文件、编译生成的 obj/exe/pdb、日志与备份文件等,源码与工程配置齐全,可直接用 Visual Studio 2012 打开编译。压缩包内目录规划清晰,工程、源码、备份、日志分区明确,便于快速定位所需模块。已有 424 人学习下载,适合具备一定 C++ 基础、希望了解服务端目录管理与网络通信实现的开发者参考。通过学习这份资源,可以掌握 BadukDir 模块的目录管理逻辑、消息通信机制、数据库记录集处理方式,以及服务启动和错误日志记录等工程化写法,对搭建或维护同类在线服务平台有直接借鉴价值。
1. 从 BadukDir 源码看 MMO 目录服务器的骨架
拿到这份 droiyanOnline 源码包,第一眼容易被密密麻麻的 .cpp、.obj 和 .ipch 文件劝退。把 v2012 前缀、BadukDir 工程名和 Dir 服务几个线索拼起来,能确认这是一套在线游戏目录服务器源码,用 Visual Studio 2012 维护。目录服务器是客户端启动后第一个连接的入口:不负责玩法逻辑,只做服务器列表下发、会话注册登出、角色状态归档这三件事。
这套代码的价值在结构。整个工程围绕 BadukDir 组织,ServiceMain 提供 Windows 服务入口,Net 管 socket 收发,Msg 做协议分发,Database 与 Recordset 负责数据落库,ErrorLog 兜底异常。模块划分干净,适合想弄懂"服务如何常驻、会话如何落库"的 C++ 开发者;对要二开私服运维工具的人来说,也能从文件布局里找到改包的准确位置。
2. BadukDir 工程结构:vcxproj、ServiceMain 与 Net/Msg 的边界
2.1 从源码清单还原模块拓扑
打开压缩包能直接看出这是一个典型的 VS2012 单工程解决方案:Dir.sln 是解决方案入口,BadukDir.vcxproj 是主工程文件,Debug 与 Release 目录各自保存了完整构建产物。工程级文件各司其职,BadukDir.clw 是类向导数据库,记录类继承关系;BadukDirCom.cpp 提供与外部组件的交互层;Userset.cpp 负责读 dir.ini 里的用户配置;SessDesc.cpp 维护会话描述结构。把这些文件对齐成一张表,模块边界立刻清楚:
| 源文件 | 职责 | 关键对外接口 |
|---|---|---|
| ServiceMain.cpp | Windows 服务生命周期 | ServiceMain / Handler |
| Net.cpp | socket 监听、收发、连接管理 | RecvPacket / SendPacket |
| Msg.cpp | 协议解析与命令分发 | Dispatch(cmd, body) |
| Database.cpp | ODBC 连接管理与 SQL 执行 | Connect / Exec |
| Recordset.cpp | 结果集封装与迭代 | Query / MoveNext |
| ErrorLog.cpp | 日志输出与错误记录 | Write / WriteError |
| Userset.cpp | dir.ini 配置读取 | LoadConfig |
| SessDesc.cpp | 会话描述与状态结构 | Create / Release |
这层划分的用意是隔离变更:网络线程只把字节流交给 Msg,Msg 不直接碰数据库,数据库操作统一从业务回调发起。好处很直接,数据库句柄不会被两个线程同时使用,网络重连时也不会把悬挂的会话指针传到业务层。接手老工程时如果发现某个模块职责混乱,优先把对 socket 和数据库的访问归到 Net 与 Database 两个文件里,改动面最小。
2.2 ServiceMain.cpp:把 BadukDir 挂进服务控制管理器
ServiceMain.cpp 的目标是把程序变成一个可以被 sc 命令注册和启动的 Windows 服务。标准做法是定义服务入口函数和 Handler 回调,再用 StartServiceCtrlDispatcher 把分发表交给系统。这个函数调用成功之后,主线程阻塞等待 SCM 指令,服务真正的初始化逻辑放到 ServiceMain 内部:
// 服务入口与分发表注册(结构参考传统 MFC 服务工程) SERVICE_TABLE_ENTRY DispatchTable[] = { { (LPSTR)"BadukDir", (LPSERVICE_MAIN_FUNCTION)ServiceMain }, { NULL, NULL } }; int main(int argc, char* argv[]) { // 控制台模式:跳过服务注册,直接跑逻辑,方便断点调试 if (argc > 1 && strcmp(argv[1], "-console") == 0) { ServiceMain(); return 0; } if (!StartServiceCtrlDispatcher(DispatchTable)) { ErrorLog::Write("dispatcher failed: %d", GetLastError()); return 1; } return 0; }DispatchTable 里的 "BadukDir" 就是服务名,之后sc start BadukDir、sc stop BadukDir都用这个名字。StartServiceCtrlDispatcher 只在真正由 SCM 拉起的进程里返回成功,直接双击 exe 会报错,所以 -console 分支是本地调试的必需品。常见的坑是初始化逻辑太重,SCM 默认等待约 30 秒,超时就报"服务未及时响应",解决办法是把数据库连接这类耗时操作丢进工作线程,Handler 收到 STOP 指令时再统一回收。
2.3 Net 与 Msg:从字节流到业务回调的四步流程
Net.cpp 负责监听端口和维护连接列表,收到完整数据包后投递给 Msg.cpp。消息走的是"包头校验 → 命令字查表 → 参数校验 → 业务回调"四个步骤。包格式固定为魔数、版本、命令字、包体长度四个字段,防止粘包时把下一条包的数据当参数解析:
// Msg.cpp 的分发主流程(模块职责示意) void Msg::Dispatch(Session* sess, BYTE* pkt, int len) { PacketHeader* hdr = (PacketHeader*)pkt; if (hdr->magic != kMagic) { // 魔数不对,非本协议包 ErrorLog::Write("bad magic from %s", sess->remote_ip); return; } int cmd = hdr->cmd; auto it = handlers_.find(cmd); // 命令字到函数指针的映射表 if (it == handlers_.end()) { ErrorLog::Write("unknown cmd: 0x%02x", cmd); return; } it->second(sess, pkt + sizeof(PacketHeader), len - sizeof(PacketHeader)); }handlers_ 在初始化时注册,新命令只需要加一行映射。cmd 取值范围建议从 0x10 起步,留出 0x00~0x0F 给内部控制消息。错误处理上,解析失败只记日志并丢弃这条包,不能直接断开连接,网络抖动产生的半包会让客户端无谓重连。真正要断开的是连续 N 次校验失败的对端,这能挡住简单的畸形包攻击。Msg 层严格不碰 socket 句柄,所有发送都回调到 Net 层,这条纪律能避免重连时向已关闭的 fd 写数据导致崩溃。
3. 数据落库:Database 的 ODBC 封装与 Recordset 会话回写
3.1 Database.cpp 连接管理与线程安全
目录服务对数据库的核心诉求是会话状态不能丢:登录前 status=0,登录后 status=1,断线回写 0。这些变更每小时发生几千次,不可能每次请求都新建连接。老工程里常见的是封装一个连接持有类,构造时建立 ODBC 连接,析构时释放,并用临界区保护执行路径:
// Database.cpp 中连接复用与查询执行 bool DBConnection::Exec(const char* sql) { EnterCriticalSection(&cs_); // 重用语句句柄,避免频繁 Alloc/Free if (stmt_ == SQL_NULL_HANDLE) { SQLAllocHandle(SQL_HANDLE_STMT, dbc_, &stmt_); } else { SQLFreeStmt(stmt_, SQL_CLOSE); } SQLRETURN rc = SQLExecDirect(stmt_, (SQLCHAR*)sql, SQL_NTS); LeaveCriticalSection(&cs_); return rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO; }临界区保证同一时刻只有一个线程执行 SQL,这是以少量并发损失换句柄安全。DSN 建议用文件 DSN 而不是机器 DSN,部署时只带一个 .dsn 文件就能迁移环境。连接串里的数据库名和账号不要硬编码,走 dir.ini 里 Userset 解析出的配置段,换库不用重新编译。每次查询结束记得用 SQLFreeStmt 释放语句句柄资源,循环查询时泄露句柄,服务跑几天后数据库连接数会涨到报警阈值。
3.2 Recordset.cpp 的结果集封装与取值
Recordset.cpp 的典型职责是把裸 SQLHSTMT 包装成可迭代结果集,让业务层不用面对 ODBC 底层句柄。常见做法是提供 Query、MoveNext、GetInt、GetString 四个方法,内部记录当前列数和取值缓冲,循环遍历的写法接近 STL 容器:
// Recordset.cpp 中结果集迭代与取值的封装 bool Recordset::MoveNext() { SQLRETURN rc = SQLFetch(stmt_); return rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO; } int Recordset::GetInt(int col) { int val = 0; SQLLEN len = 0; SQLGetData(stmt_, col + 1, SQL_C_SLONG, &val, 0, &len); return val; }注意列号从 1 开始,这是 ODBC 和很多 C++ 容器不一致的地方,封装时内部 +1 可以避免上层到处修补。SQL_C_SLONG 表示按 32 位整数取,字符串列用 SQL_C_CHAR 配合显式长度缓冲区,返回 SQL_SUCCESS_WITH_INFO 通常是有截断,要检查长度参数是否够用。目录服务最常用的两个查询是"按账号查最近登录记录"和"批量拉在线玩家列表",这两个 SQL 都要在 uid 和 status 上建索引,否则玩家量上来之后,全表扫描会让数据库 CPU 居高不下。
3.3 会话表的写入时机与事务边界
会话表字段不多,一般就是 uid、server_id、status、login_time、logout_time 五个字段,关键在写入时机。登录成功 insert 一条,心跳超时或主动退出时 update status,不能每次心跳都写库,磁盘 IO 扛不住。常见做法是在内存里维护一个会话映射,每 5 分钟或状态批量变更时统一回写:
-- 批量回写离线条目,status 置 0 并补登出时间 BEGIN TRANSACTION; UPDATE account_session SET status = 0, logout_time = GETDATE() WHERE uid IN (SELECT uid FROM #offline_list); COMMIT;批量 update 比逐条 update 快一个数量级,代价是数据库和服务内存之间最多有 5 分钟的不一致窗口。对目录服务来说这个窗口可以接受,玩家再次登录时以服务内存态为准,落库数据只供后台统计和审计用。事务边界要显式写出来,任何一条失败整体回滚,避免出现一半账号在线一半离线的脏数据。服务停止时要先停网络线程再跑最后一轮回写,顺序反了会出现进程退出但数据库里全是"在线"的假象。
4. 编译链路:vc120 工具集、Debug/Release 差异与服务调试
4.1 从 vc120.pdb 反推工具集版本
工程目录里的 vc120.pdb、Dir.Build.CppClean.log 暴露了最后一次构建使用的编译器和中间文件状态,vc120 对应 Visual Studio 2013 的 v12 工具集,项目名里的 v2012 则说明源码延续自 VS2012 时代。用 VS2012 打开 Dir.sln 直接 F7 最省事,但新机器上通常没有 VS2012,更常见的路径是用 VS2019 或 2022 打开并触发工程升级。
升级后第一件事是确认平台工具集:右键工程 → 属性 → 常规 → 平台工具集,如果选了 v120 但本机没有对应组件,会直接报 MSB8020。切到 v140 或 v142 通常可以无痛编译,但要注意这个工程依赖 MFC 和 ODBC 头文件,新版 IDE 需要单独勾选"适用于最新 v142 生成工具的 C++ MFC"工作负载,否则编到 BadukDirCom.cpp 会报 C1083 找不到 afxwin.h。StdAfx.cpp 的预编译头设置也要检查,工程文件与源文件的"使用预编译头"选项必须一致,否则报 C1010 强制要求文件头包含 stdafx.h。
4.2 Debug 与 Release 的差异与常见错误
Debug 与 Release 目录并存是传统 MFC 工程的标配。改动代码后经常遇到"Debug 能过、Release 报错",多数是未初始化变量和条件编译分支不一致导致。老代码里还有一类经典问题:#ifdef _DEBUG的调试代码在 Release 下不参与编译,某个静态变量的生命周期变化,启动时就出现随机崩溃。
| 错误现象 | 可能原因 | 处理位置 |
|---|---|---|
| MSB8020 工具集缺失 | 本机没装 v120 组件 | 平台工具集切 v142 |
| C1083 找不到 afxwin.h | 缺少 MFC 组件 | VS Installer 勾选 MFC |
| C1010 意外文件尾 | 源文件漏了 stdafx.h | 检查预编译头设置 |
| unresolved external | 声明与实现不一致 | 逐个核对链接错误列表 |
| 运行时随机崩溃 | 未初始化变量或竞态 | -console 模式加断点 |
Release 构建建议保持默认的 /O2 优化,不要随意改成 /Od。这个工程里存在会话清理逻辑,依赖一定的时间顺序,优化等级变了可能暴露隐藏的竞态条件。如果怀疑优化导致异常,先用 /Od 复现对比,而不是直接换优化等级发布。升级报告写在 _UpgradeReport_Files 目录里,UpgradeReport.xslt 用浏览器打开能看到每条升级警告,逐条过一遍可以避免很多坑。
4.3 用 -console 参数把服务跑成可调试进程
服务程序的调试不能直接按 F5,StartServiceCtrlDispatcher 在非服务上下文里调用会立即返回错误。常见做法是在 main 里加一个 -console 分支,服务模式和控制台模式共用同一个 ServiceMain,调试时直接以控制台进程跑起来:
int main(int argc, char* argv[]) { // 调试模式:以控制台方式运行,可用 VS 断点 if (argc > 1 && strcmp(argv[1], "-console") == 0) { ServiceMain(); return 0; } SERVICE_TABLE_ENTRY DispatchTable[] = { { (LPSTR)"BadukDir", (LPSERVICE_MAIN_FUNCTION)ServiceMain }, { NULL, NULL } }; return StartServiceCtrlDispatcher(DispatchTable) ? 0 : 1; }有了 -console 模式,网络收包和协议分发都能用 VS 的断点逐步跟踪。注册服务用sc create BadukDir binPath= "C:\droiyan\Dir.exe",这里等号后面必须有一个空格,sc 命令对这个格式非常敏感。启动服务后先看日志确认"监听成功"再发测试包。调试协议时在 Msg::Dispatch 入口下断点,配合 Net.cpp 的 recv 断点,就能从原始字节流一路跟踪到业务处理的完整路径。停止调试时注意先把服务 stop 再结束进程,否则会话表里残留的在线状态会影响下一次启动的统计。
5. 二次开发:基于 ErrorLog 的定位与协议扩展
5.1 给 ErrorLog 增加线程 ID 与级别字段
ErrorLog.cpp 是老工程里最被低估的模块,原版通常只有 Write 和 WriteError 两个输出函数。二开后线程一多,日志里就很难分清是谁打的。常见做法是改成Write(level, fmt, ...)的三级接口,行首固定打时间戳、线程 ID 和级别,INFO、WARN、ERROR 三档分开记录。这样多个客户端并发登录时,从日志能直接看出哪个线程在处理哪条会话,不用靠猜。
5.2 扩展一条服务器状态命令字 0x21
目录服务最常见的二开需求,是让客户端周期性看到服务器负载。在 Msg.cpp 的 handlers_ 注册表里加一行即可,例如命令字 0x21:
// 注册 0x21:服务器状态查询 handlers_[0x21] = [this](Session* s, BYTE* body, int len) { ServerStat st = { 0 }; st.online_users = (int)g_session_mgr->OnlineCount(); st.cpu_load = GetProcessCpuUsage(); Net::SendPacket(s, 0x21, (BYTE*)&st, sizeof(st)); };协议扩展必须服务端和客户端同步改:先加命令字与响应结构,再改客户端解包函数。二开项目大量死在字段顺序不一致上,建议包头里加 protocol_version 字段,版本不匹配直接拒连并回错误码,比客户端解析出乱码好查得多。新增结构体按 4 字节对齐并用 memset 清零,防止栈上的残留字节被当成协议内容发出。
5.3 验证顺序与调用栈定位
部署验证按三步走:先用 -console 模式启动并连库,确认监听地址和端口与 dir.ini 一致;再用netstat -ano | findstr 端口核对监听状态;最后写一个最小客户端发包测 0x21 的响应包体。测试停止流程时,观察退出日志是否出现批量回写完成的记录,没出现就说明会话落库的收尾步骤被跳过了。
线上遇到偶发崩溃,可在 ErrorLog 里临时调用 CaptureStackBackTrace 打印最近 16 帧,把栈信息写进 ERROR 日志。日常把日志级别保持在 WARN 以上,排查时临时降到 DEBUG,把 0x21 的响应报文按十六进制打印到日志,对照包结构逐字节核对,是最快的协议排错方式。
本文还有配套的精品资源,点击获取