尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

c++游戏后端开源框架学习——wukong(十、lua热更新)

c++游戏后端开源框架学习——wukong(十、lua热更新) Lua 热更新原理详解澄清一个常见误解Lua热更新不是C里有一段Lua逻辑在跑而是C在运行时动态切换消息处理函数的调用入口。一、你的理解哪里对了哪里错了你的原理解“在C中有一段是利用lua运行的逻辑才能使用lua热更”这个理解部分正确C确实嵌入了Lua虚拟机来执行Lua脚本。但关键不在有没有Lua在跑而在于C如何决定走哪条代码路径消息到达 │ ├── needHotfix false → 调用C编译好的handler函数编译时就固定了 │ └── needHotfix true → 调用Lua函数 fix_消息号() 运行时从文件加载可替换热更新的本质是C在消息分派时有一个if/else分支决定走C原函数还是走Lua函数。这个分支标志needHotfix可以在运行时通过Redis动态修改。更准确的理解不是C代码 ← → Lua代码互相调用模糊边界 而是C是宿主Lua是被嵌入的脚本引擎C决定何时调用Lua二、整体架构┌─────────────────────────────────────────────────────────────────┐ │ Lobby Server (C进程) │ │ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ 1. 消息分派层 (MessageTarget::handleMessage) │ │ │ │ │ │ │ │ 消息到达 → 查注册表 → needHotfix? │ │ │ │ ├── false → 调用C handler编译时绑定 │ │ │ │ └── true → 调用 callHotfix() │ │ │ └───────────────────────────┬───────────────────────────┘ │ │ │ │ │ ┌───────────────────────────▼───────────────────────────┐ │ │ │ 2. C→Lua桥接层 (MessageTarget::callHotfix) │ │ │ │ │ │ │ │ 从Lua VM池取一个lua_State │ │ │ │ lua_getglobal(L, fix_1000) ← 找Lua函数 │ │ │ │ lua_pushlightuserdata(L, this) ← 压入C对象指针 │ │ │ │ lua_pcall(L, ...) ← 调用Lua函数 │ │ │ │ 归还lua_State到池中 │ │ │ └───────────────────────────┬───────────────────────────┘ │ │ │ │ │ ┌───────────────────────────▼───────────────────────────┐ │ │ │ 3. Lua脚本层 (hotfix/fix_1000.lua) │ │ │ │ │ │ │ │ local fixecho require libfixecho ← 加载C库 │ │ │ │ function fix_1000(a, b, c) │ │ │ │ return fixecho.lua_echo(a, b, c) │ │ │ │ end │ │ │ └───────────────────────────┬───────────────────────────┘ │ │ │ │ │ ┌───────────────────────────▼───────────────────────────┐ │ │ │ 4. C动态库层 (libfixecho.so) │ │ │ │ │ │ │ │ lua_echo(L) { │ │ │ │ // 从Lua栈取回C指针 │ │ │ │ MyLobbyObject *gobj (MyLobbyObject*) │ │ │ │ lua_touserdata(L, 1); │ │ │ │ gobj-send(...); ← 直接操作C对象 │ │ │ │ } │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘三、逐层拆解第一层C消息分派——决定走哪条路文件share/msghdl/message_target.cpp:30-75voidMessageTarget::handleMessage(int32_tmsgType,uint16_ttag,conststd::stringrawMsg){// 查注册表获取这个消息的处理信息boolneedHotfix;MessageHandle handle;// C处理函数指针g_MessageHandleManager.getMessageInfo(msgType,proto,needCoroutine,needHotfix,handle);// 解析protobuf消息google::protobuf::Message*msgproto-New();msg-ParseFromString(rawMsg);#ifdefUSE_MESSAGE_HOTFIXif(needHotfix){callHotfix(msgType,tag,targetMsg);// ← 走Lua路径}else{handle(getPtr(),tag,targetMsg);// ← 走C原路径}#elsehandle(getPtr(),tag,targetMsg);// ← 没有编译热更功能#endif}关键点needHotfix标志是运行时可变的存在MessageHandleManager的注册表中这个标志由MessageHotfixManager通过Redis动态修改#ifdef USE_MESSAGE_HOTFIX控制是否编译热更代码只有Lobby服开启第二层热更新管理器——运行时修改标志文件share/msghdl/message_hotfix_manager.cppvoidMessageHotfixManager::init(){// 1. 启动时从Redis加载初始热更配置RoutineEnvironment::startCoroutine([](void*arg)-void*{auto*manager(MessageHotfixManager*)arg;manager-resetHotfix();returnNULL;},this);// 2. 订阅Redis的WK_Hotfix主题收到通知就重新加载PubsubService::Subscribe(WK_Hotfix,true,[this](conststd::stringtopic,conststd::stringmsg){resetHotfix();// ← 任何时候收到PubSub消息都会触发});// 3. 启动清理协程定期回收空闲的Lua VMRoutineEnvironment::startCoroutine(updateRoutine,this);}resetHotfix()做了什么voidMessageHotfixManager::resetHotfix(){// 从Redis读取热更消息列表JSON数组如 [1000, 1001]redisContext*cacheg_RedisPoolManager.getCoreCache()-take();std::string hotfixData;RedisUtils::GetHotfixData(cache,hotfixData);// GET HotfixMsgs// hotfixData [1000]Document doc;doc.Parse(hotfixData.c_str());// 先清除所有消息的hotfix标志g_MessageHandleManager.clearNeedHotfix();// 遍历需要热更的消息列表for(SizeType i0;idoc.Size();i){intmsgTypedoc[i].GetInt();// 如 1000// 只处理本服注册了的消息if(!g_MessageHandleManager.isRegistedMessage(msgType)){continue;}// 设置这个消息的needHotfixtrue ← 核心操作g_MessageHandleManager.setNeedHotfix(msgType);// 加载对应的Lua脚本文件// 约定文件名hotfix/fix_消息号.luastd::ostringstream oss;osshotfix/fix_msgType.lua;std::string content;Utility::loadFileToString(oss.str().c_str(),content);hotfixMap_[msgType]content;// 存到内存}// 版本号1旧的Lua VM全部关闭hotfixVersion_;for(autoinfo:luaStates_){lua_close(info.L);}luaStates_.clear();}这个函数是热更新的核心它把哪些消息走Lua这个决策动态写入了C的内存不需要重新编译。第三层C调用Lua——桥接层文件share/msghdl/message_target.cpp:103-151voidMessageTarget::callHotfix(int32_tmsgType,uint16_ttag,std::shared_ptrgoogle::protobuf::Messagemsg){// 1. 从池中取一个Lua VMLuaStateInfo lsInfo;g_MessageHotfixManager.getLuaStateInfo(lsInfo);// 用unique_ptr保护确保异常时也能归还或关闭std::unique_ptrlua_State,decltype(lua_close)L(lsInfo.L,lua_close);// 2. 构造函数名fix_消息号如 fix_1000std::ostringstream oss;ossfix_msgType;std::string luaFunoss.str();// 3. 从Lua全局表中找这个函数lua_getglobal(L.get(),luaFun.c_str());if(lua_isnil(L.get(),-1)){ERROR_LOG(Lua function %s not found\n,luaFun.c_str());return;}// 4. 压入参数3个lua_pushlightuserdata(L.get(),this);// 参数1: C对象指针lua_pushlightuserdata(L.get(),msg.get());// 参数2: protobuf消息指针lua_pushnumber(L.get(),tag);// 参数3: 消息tag// 5. 调用Lua函数if(lua_pcall(L.get(),3,1,0)!LUA_OK){ERROR_LOG(Lua call failed: %s\n,lua_tostring(L.get(),-1));return;}// 6. 读取返回值intresultlua_tonumber(L.get(),-1);lua_pop(L.get(),1);// 7. 归还Lua VM到池中L.release();// 阻止unique_ptr关闭lua_Stateg_MessageHotfixManager.backLuaStateInfo(lsInfo);}关键理解lua_getglobal从Lua的全局变量表中查找名为fix_1000的函数lua_pushlightuserdata把C的裸指针压入Lua栈Lua侧可以取回lua_pcall实际调用Lua函数期间Lua虚拟机执行Lua字节码C对象指针直接传给Lua没有序列化/反序列化开销第四层Lua脚本——胶水层文件demo/hotfix/fixEcho/lua/fix_1000.lua-- 设置C动态库的搜索路径package.cpathhotfix/?.so;-- 加载C编译的动态库localfixechorequirelibfixecho-- 定义消息处理函数名字必须叫 fix_消息号functionfix_1000(a,b,c)-- a C的MessageTarget指针lightuserdata-- b C的protobuf::Message指针lightuserdata-- c tag数字returnfixecho.lua_echo(a,b,c)end关键理解Lua脚本本身可以很简单只是把参数转发给C动态库也可以在Lua中写纯逻辑如修改数值、做条件判断不需要C库Lua脚本是文本文件修改后不需要编译重启Lua VM即可生效第五层C动态库——真正的业务逻辑文件demo/hotfix/fixEcho/src/fix_echo.cpp// 这个函数注册为Lua可调用的C函数staticintlua_echo(lua_State*L){// 从Lua栈取回C指针就是callHotfix压入的那些MyLobbyObject*gobj(MyLobbyObject*)lua_touserdata(L,1);wukong::pb::StringValue*msg(wukong::pb::StringValue*)lua_touserdata(L,2);uint16_ttag(uint16_t)lua_tonumber(L,3);// 直接操作C对象gobj-setExp(gobj-getExp()1);gobj-send(S2C_MESSAGE_ID_ECHO,tag,*msg);lua_pushnumber(L,0);// 返回值return1;}// 注册函数表staticconststructluaL_RegmyLib[]{{lua_echo,lua_echo},{NULL,NULL}};// Lua require时的入口externCintluaopen_libfixecho(lua_State*L){luaL_newlib(L,myLib);return1;}关键理解动态库.so文件可以被Lua的require在运行时加载替换.so文件后重启Lua VM就会加载新版本动态库中可以直接操作C对象通过传入的指针性能与原生C几乎一样这就是热更新C代码的真正含义不是修改正在运行的C代码而是替换被Lua加载的动态库四、热更新的完整流程触发热更新运维人员修改Redis SET HotfixMsgs [1000] ← 告诉服务器1000号消息走Lua PUBLISH WK_Hotfix reload ← 通知所有服务器重新加载服务器内部流程1. Redis PubSub收到WK_Hotfix消息 └→ MessageHotfixManager::resetHotfix() 被调用 2. 从Redis读取热更消息列表 └→ hotfixData [1000] 3. 遍历消息列表 ├── 对消息1000setNeedHotfix(1000, true) ├── 加载文件 hotfix/fix_1000.lua 到内存 └── 存入 hotfixMap_[1000] Lua文件内容 4. hotfixVersion_ 版本号递增 └── 关闭所有旧的Lua VM 5. 下一条消息1000到达时 ├── handleMessage() 查注册表 → needHotfixtrue ├── callHotfix() 被调用 ├── 创建新的Lua VM因为旧的都关了 ├── 在新VM中加载 fix_1000.lua ├── 调用 fix_1000(this, msg, tag) └── Lua内部调用 libfixecho.so 的 lua_echo()取消热更新运维人员修改Redis SET HotfixMsgs [] ← 空数组没有消息需要热更 PUBLISH WK_Hotfix reload ← 通知重新加载服务器收到后clearNeedHotfix()清除所有标志消息1000重新走C原函数。五、Lua VM池化机制每次调用Lua函数都创建/销毁Lua VM开销很大框架用了对象池// message_hotfix_manager.hstructLuaStateInfo{lua_State*L;// Lua虚拟机time_t lastUsedAt;// 最后使用时间intversion;// 创建时的hotfixVersion};std::listLuaStateInfoluaStates_;// VM池inthotfixVersion_;// 当前版本号工作流程取VM ├── 池非空 → 取最后一个返回 └── 池为空 → 新建lua_State加载所有hotfix脚本 归还VM ├── 版本号匹配 → 放回池中可复用 └── 版本号不匹配 → lua_close关闭是旧版本丢弃 定期清理每60秒 └── 关闭超过1分钟未使用的VM但最少保留10个版本号的作用热更后hotfixVersion_池中所有旧VM的版本号都不匹配了。归还时检测到版本号不一致直接关闭不会复用旧逻辑的VM。这保证了热更后不会有残留的旧Lua逻辑在跑。六、关键问题解答Q1为什么不能直接修改C代码来热更C是编译型语言代码编译成机器码后无法在运行时替换。要修改C逻辑必须重新编译并重启进程。Lua是解释型语言代码在运行时由Lua虚拟机解释执行。修改Lua文件后重新加载到Lua VM即可生效不需要重启进程。Q2Lua热更后C原函数还在吗还在。C原函数是编译进二进制文件的永远不会消失。needHotfix标志只是决定了调用哪个函数if(needHotfix){callHotfix(msgType,tag,msg);// 走Lua}else{handle(obj,tag,msg);// 走C原函数}取消热更needHotfixfalse后消息重新走C原函数。Q3Lua不经过C能直接操作游戏对象吗不能直接操作必须通过C提供的接口。在本框架中有两种方式方式一lightuserdata传递指针// C侧把指针压入Lua栈lua_pushlightuserdata(L,this);// C对象指针lua_pushlightuserdata(L,msg.get());// protobuf消息指针// Lua侧转发给C动态库functionfix_1000(a,b,c)returnfixecho.lua_echo(a,b,c)--a,b是指针c是数字 end// C动态库侧从Lua栈取回指针直接操作C对象MyLobbyObject*gobj(MyLobbyObject*)lua_touserdata(L,1);gobj-setExp(gobj-getExp()1);// 直接调用C方法方式二在Lua中写纯逻辑functionfix_1000(a,b,c)-- 不调C库纯Lua逻辑-- 但a是指针Lua不能直接操作它-- 只能做数值计算、条件判断等return0end所以Lua热更新的性能取决于Lua做多少C动态库做多少。纯Lua做复杂逻辑会慢但修改简单参数/条件分支就很快。Q4C动态库(.so)怎么热更新编译新的.so文件替换服务器上的旧文件触发Redis PubSub的WK_Hotfix消息服务器关闭所有旧Lua VMhotfixVersion_下次消息到达时创建新Lua VM新Lua VM执行require libfixecho时加载新的.so文件注意Linux的.so文件如果被进程映射已加载直接覆盖可能失败。需要先dlclose旧库通过关闭旧Lua VM间接实现或者用不同的文件名如libfixecho_v2.so修改Lua脚本中的require路径或者用mv替换Linux允许mv覆盖已加载的so旧进程仍用旧的新加载用新的Q5这个框架的热更新和传统Lua热更新有什么区别对比项传统Lua框架如skynet本框架业务逻辑全部用Lua写C为主Lua仅做热更补丁热更粒度整个Lua文件/模块单个消息处理函数性能Lua执行较慢Lua调C动态库接近原生触发方式修改文件信号Redis PubSub适用场景快速迭代开发线上紧急修复本框架的设计思路是C为主Lua为补丁正常运行时所有逻辑走C只有需要紧急修复的消息才走Lua。修复完后可以取消热更标记重新走C。七、数据流完整路径以消息1000ECHO为例展示从客户端到响应的完整路径1. 客户端发送 C2S_MESSAGE_ID_ECHO (消息号1000) │ ▼ 2. Gateway收到根据消息ID高16位判断是LOBBY消息 └→ forwardIn RPC 转发到 Lobby服 3. Lobby服的 MessageTarget::handleMessage(1000, tag, rawMsg) │ ├── 查注册表消息1000的 needHotfix true │ ▼ 4. callHotfix(1000, tag, msg) │ ├── 从VM池取lua_State ├── lua_getglobal(L, fix_1000) ├── lua_pushlightuserdata(L, this) ← LobbyObject指针 ├── lua_pushlightuserdata(L, msg.get()) ← StringValue指针 ├── lua_pushnumber(L, tag) ← tag值 │ ▼ 5. Lua执行 fix_1000(a, b, c) │ ├── require libfixecho ← 加载C动态库 └── return fixecho.lua_echo(a, b, c) │ ▼ 6. C动态库执行 lua_echo(L) │ ├── lua_touserdata(L, 1) → MyLobbyObject* gobj ├── lua_touserdata(L, 2) → StringValue* msg ├── lua_tonumber(L, 3) → tag │ ├── gobj-setExp(gobj-getExp() 1) ← 修改玩家经验值 └── gobj-send(S2C_MESSAGE_ID_ECHO, tag, *msg) ← 发回包给客户端 │ ▼ 7. lua_echo返回1push返回值到Lua栈 │ ▼ 8. Lua fix_1000 返回 │ ▼ 9. callHotfix 读取返回值归还lua_State到池 │ ▼ 10. gobj-send() 通过Gateway的forwardOut发给客户端八、安全注意事项8.1 指针安全// C把裸指针传给Lualua_pushlightuserdata(L,this);// LobbyObject*lua_pushlightuserdata(L,msg.get());// protobuf::Message*风险如果Lua VM存活时间超过C对象生命周期指针就悬空了如果Lua脚本把指针存到全局变量下次调用时对象可能已销毁框架的防护Lua VM池化每次调用后立即归还不会长期持有指针hotfixVersion_机制保证热更后旧VM全部关闭但如果Lua脚本自己存指针到全局表框架无法防护8.2 Lua脚本安全Lua脚本可以执行任意Lua代码包括os.execute()执行系统命令如果加载了os库文件读写死循环导致协程卡死框架的防护只加载了package库luaopen_package没有加载os/io等库lua_pcall保护模式调用错误不会崩溃进程但Lua脚本中的死循环仍会卡住协程8.3 动态库安全C动态库.so有和C主程序相同的权限可以访问进程内存可以调用系统API一个有bug的动态库可以导致进程崩溃实际建议动态库代码必须经过完整测试不能随意替换。九、总结你的理解修正原理解正确理解C中有一段Lua逻辑在跑C嵌入了Lua VM按需调用Lua函数Lua热更需要C配合C代码编译时就预留了if(needHotfix)分支运行时通过Redis切换Lua直接操作游戏对象Lua通过lightuserdata拿到C指针转发给C动态库操作热更修改C代码热更替换消息处理入口从C函数切换到Lua函数热更新的本质编译时C代码中预埋了 if(needHotfix) { callLua(); } else { callCpp(); } 运行时通过Redis动态修改 needHotfix 标志 通过Redis PubSub通知所有服务器实例 替换Lua脚本文件和C动态库 效果 特定消息的处理逻辑在运行时被替换无需重启进程三层热更能力层次修改内容是否需要编译生效方式Lua脚本fix_1000.lua不需要重新加载Lua VMC动态库libfixecho.so需要编译.soLua require时加载新.soC主程序Lobby服二进制需要重新编译重启进程框架的热更新主要覆盖前两层第三层主程序无法热更只能重启。
返回列表