ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MMO目录服务器BadukDir源码解析:架构、协议与数据落库

MMO目录服务器BadukDir源码解析:架构、协议与数据落库 简介这是一份 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.cppWindows 服务生命周期ServiceMain / HandlerNet.cppsocket 监听、收发、连接管理RecvPacket / SendPacketMsg.cpp协议解析与命令分发Dispatch(cmd, body)Database.cppODBC 连接管理与 SQL 执行Connect / ExecRecordset.cpp结果集封装与迭代Query / MoveNextErrorLog.cpp日志输出与错误记录Write / WriteErrorUserset.cppdir.ini 配置读取LoadConfigSessDesc.cpp会话描述与状态结构Create / Release这层划分的用意是隔离变更网络线程只把字节流交给 MsgMsg 不直接碰数据库数据库操作统一从业务回调发起。好处很直接数据库句柄不会被两个线程同时使用网络重连时也不会把悬挂的会话指针传到业务层。接手老工程时如果发现某个模块职责混乱优先把对 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 起步留出 0x000x0F 给内部控制消息。错误处理上解析失败只记日志并丢弃这条包不能直接断开连接网络抖动产生的半包会让客户端无谓重连。真正要断开的是连续 N 次校验失败的对端这能挡住简单的畸形包攻击。Msg 层严格不碰 socket 句柄所有发送都回调到 Net 层这条纪律能避免重连时向已关闭的 fd 写数据导致崩溃。3. 数据落库Database 的 ODBC 封装与 Recordset 会话回写3.1 Database.cpp 连接管理与线程安全目录服务对数据库的核心诉求是会话状态不能丢登录前 status0登录后 status1断线回写 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 组件平台工具集切 v142C1083 找不到 afxwin.h缺少 MFC 组件VS Installer 勾选 MFCC1010 意外文件尾源文件漏了 stdafx.h检查预编译头设置unresolved external声明与实现不一致逐个核对链接错误列表运行时随机崩溃未初始化变量或竞态-console 模式加断点Release 构建建议保持默认的 /O2 优化不要随意改成 /Od。这个工程里存在会话清理逻辑依赖一定的时间顺序优化等级变了可能暴露隐藏的竞态条件。如果怀疑优化导致异常先用 /Od 复现对比而不是直接换优化等级发布。升级报告写在 _UpgradeReport_Files 目录里UpgradeReport.xslt 用浏览器打开能看到每条升级警告逐条过一遍可以避免很多坑。4.3 用 -console 参数把服务跑成可调试进程服务程序的调试不能直接按 F5StartServiceCtrlDispatcher 在非服务上下文里调用会立即返回错误。常见做法是在 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 的响应报文按十六进制打印到日志对照包结构逐字节核对是最快的协议排错方式。本文还有配套的精品资源点击获取
返回列表