首页 > 其他分享 >skynet热更新之inject

skynet热更新之inject

时间:2024-07-27 11:50:19浏览次数:18  
标签:end 更新 lua inject skynet debug local

游戏服务器的热更新是一种常见的需求,skynet可以通过inject的方式,来修改一个服务的消息处理函数,达到热更新的效果。

skynet内置服务debug_console

skynet自带了一个调试控制台服务。inject注入代码需要先启动这个服务。

skynet.newservice("debug_console", "127.0.0.1", "9666")

启动之后,我们可以用telnet或者nc等指令来登录调试控制台。

> nc 127.0.0.1 9666

输入list指令,可以得到当前系统中所有服务的地址:

list
:00000004       snlua cdummy
:00000006       snlua datacenterd
:00000007       snlua service_mgr
:00000008       snlua main
:00000009       snlua debug_console 127.0.0.1 9666
:0000000a       snlua serviceA
<CMD OK>

输入inject指令,我们可以将某个代码文件,注入到指定的服务中:

inject :0000000a service/hotfix.lua

<CMD OK>

更多的debug_console指令可以参考这里

inject实例

我们在系统启动时,打开debug_console,然后启动服务serviceA,接着设置每隔5秒给serviceA发送两个lua消息,一个参数bar,一个参数foo,代码如下:

--main.lua
local skynet = require "skynet"
skynet.start(function()
    skynet.newservice("debug_console", "127.0.0.1", "9666")
    local addr = skynet.newservice("serviceA")
    
    local function tick()
        skynet.send(addr, "lua", "foo")
        skynet.send(addr, "lua", "bar")
        skynet.timeout(500, tick)
    end

    skynet.timeout(500, tick)
end)

在服务serverA中,我们根据参数,调用不同的处理函数:

--serviceA.lua
local skynet = require "skynet"
local handles = {}

handles.foo = function()
    print("foo")
end

skynet.start(function()
    skynet.dispatch("lua", function(session, source, cmd, ...)
        local handle = handles[cmd]
        if handle then
            handle()
        else
            print("cmd not found", cmd)
        end
    end)
end)

现在我们启动skynet,可以看到每隔5秒输出:

foo
cmd not found   bar

现在我们新建一个文件hotfix.lua

--hotfix.lua
local handles = _P.lua.handles
local print = _G.print
handles.foo = function()
    print("foo after hotfix")
end

handles.bar = function()
    print("bar after hotfix")
end

接下来连接到控制台,并输入inject指令:

echo 'inject :0000000a services/hotfix.lua' | nc 127.0.0.1 9666

等到下次输出的时候,我们看到的就是:

foo after hotfix
bar after hotfix

更新完成,修改了foo函数,新增了bar函数。

使用inject调用hotfix.lua时,print函数是被修改过成debug_console的返回输出函数,所以如果要用到print的话,需要使用全局变量_G.print

对upValue的处理

如果我们的serviceA是这样的:

--serviceA.lua
local skynet = require "skynet"
local handles = {}

local N = 1
local T = {
    count = 0,
}

handles.foo = function()
    N = N + 2
    T.count = T.count + 1
    print("foo", N, T.count)
end

handles.bar = function()
    N = N - 1
    print("bar", N)
end

skynet.start(function()
    skynet.dispatch("lua", function(session, source, cmd, ...)
        local handle = handles[cmd]
        if handle then
            handle()
        else
            print("cmd not found", cmd)
        end
    end)
end)

foo函数带有两个upValue: NTbar函数带有一个upValue: N 如果hotfix.lua文件没有做特殊处理,直接覆盖函数的话,那么就会丢失这些upValue。那么,要怎么处理这些upValue呢?这里需要用到luadebug库,主要是两个函数:

  • debug.getupvalue(f, i): 获取函数f中的第iupValue的变量名和值。
  • debug.upvaluejoin(f1, i, f2, j):让函数f1的第iupValue引用f2中的第jupValue

热更新带有upValue的函数,我们的hotfix.lua分三步走:

  1. 定义一个函数get_up,来获取原有的函数的upValue列表。
  2. 定义新的处理函数。
  3. 定义一个函数uv_join,将新函数的upValue和旧函数的upValue绑定起来。

完整代码如下:

local handles = _P.lua.handles
local print = _G.print

local function get_up(f)
    local u = {}
    if not f then
        return u
    end
    local i = 1
    while true do
        local name = debug.getupvalue(f, i)
        if name == nil then
            return u
        end
        u[name] = i
        i = i + 1
    end
    return u
end

local function uv_join(f, old_f, old_uv)
    local i = 1
    while true do
        local name = debug.getupvalue(f, i)
        if not name then
            break
        end

        if old_uv[name] then
            debug.upvaluejoin(f, i, old_f, old_uv[name])
        end
        i = i + 1
    end
end

local foo = handles.foo
local up = get_up(foo)

local N, T      --定义两个upValue,否则函数里会变成全局变量
handles.foo = function()
    N = N + 200
    T.count = T.count + 100
    print("foo", N, T.count)
end
uv_join(handles.foo, foo, up)

这里的get_up函数只取了传入函数的upValue,如果要嵌套处理函数中的函数,可以参考lualib/skynet/inject.lua中的getupvaluetable函数。

inject实现原理

debug_console服务的代码位于service/debug_console.lua文件中,其对inject指令的处理,其实就是发送一条debug类型的消息到目标服务:

--debug_console.lua
function COMMAND.inject(address, filename, ...)
	address = adjust_address(address)
	local f = io.open(filename, "rb")
	if not f then
		return "Can't open " .. filename
	end
	local source = f:read "*a"
	f:close()
	local ok, output = skynet.call(address, "debug", "RUN", source, filename, ...)
	if ok == false then
		error(output)
	end
	return output
end

在我们的服务中,当我们require 'skynet'的时候,会自动注册debug消息类型的处理:

--lualib/skynet.lua
-- Inject internal debug framework
local debug = require "skynet.debug"
debug.init(skynet, {
	dispatch = skynet.dispatch_message,
	suspend = suspend,
	resume = coroutine_resume,
})
--lualib/skynet/debug.lua
    skynet.register_protocol {
		name = "debug",
		id = assert(skynet.PTYPE_DEBUG),
		pack = assert(skynet.pack),
		unpack = assert(skynet.unpack),
		dispatch = _debug_dispatch,
	}

其中,参数RUN是这样处理的

--lualib/skynet/debug.lua
function dbgcmd.RUN(source, filename, ...)
    local inject = require "skynet.inject"
    local args = table.pack(...)
    local ok, output = inject(skynet, source, filename, args, export.dispatch, skynet.register_protocol)
    collectgarbage "collect"
    skynet.ret(skynet.pack(ok, table.concat(output, "\n")))
end

追溯代码,来到最终的inject函数:

  1. 修改print函数,可以返回输出内容给debug_console服务。
  2. 在上一层调用的时候,传进来的...实际上两个函数skynet.dispatch_messageskynet.register_protocol,这里将这两个函数,以及函数中包含的子函数,所用到的upVluae都收集起来,存入表u中。
  3. protoskynet.register_protocol中用到的一个upValue,存放着当前服务所注册的消息类型。遍历proto,将每个消息的处理函数用到的upValue收集起来,存放到表p中。
  4. 设置环境,调用传入的热更新文件。现在我们知道,在上面的例子中的hotfix.lua,用到的_P,就是存放各种消息类型的处理函数的upValue表。

在控制台调用inject指令时,还可以传入额外的参数,例如:inject :0000000a services/hotfix.lua xxx yyy,最终这两个参数,就是这里的inject函数中的第四个参数args,可以在hotfix.lua中直接使用这两参数。

标签:end,更新,lua,inject,skynet,debug,local
From: https://www.cnblogs.com/lcc9527/p/18326776

相关文章

  • 谈一谈一条SQL的查询、更新语句究竟是如何执行的?
    文章目录理解执行流程衍生知识redologbinlog本篇文章是基于《MySQL45讲》来写的个人理解与感悟。理解先看下图:上一篇文章我们讨论了一条SQL查询语句的执行流程,并介绍了执行过程中涉及的处理模块。回顾一下:大体来说,MySQL可以分为Server层和存储引擎层两部分......
  • jeecg boot 企业级应用管理后台 (五)菜单模块介绍 从0开始 (持续更新)
    Jeecg的菜单模块是其重要组成部分,管理和展示应用程序中的导航菜单。以下是关于Jeecg菜单模块的详细介绍,涵盖了数据库、前端和后端的各个部分。数据库部分Jeecg的菜单模块在数据库中通常涉及以下几个表:菜单表(sys_menu):存储菜单的基本信息,如菜单名称、路径、图标、父级菜......
  • jeecg boot 企业级应用管理后台 (六)权限模块介绍 从0开始 (持续更新)
    数据库部分Jeecg的权限模块在数据库中通常涉及以下几个表:权限表(sys_permission):存储权限的基本信息,如权限名称、权限标识、描述等。CREATETABLE`sys_permission`(`id`BIGINTNOTNULLAUTO_INCREMENT,`name`VARCHAR(50)NOTNULLCOMMENT'权限名称',`perms`......
  • python中的无损字典更新
    是否有一个库函数可以实现像这样的非破坏性字典更新defupdate(d,key,val):d=d.copy()d[key]=valreturnd在Python中,没有直接用于非破坏性字典更新的内置库函数。但是,你可以使用多种方法来实现相同的结果。1.使用dict.copy()和更新......
  • TInterfaceResolver&TInjectableObject
    TInterfaceResolver&TInjectableObject{************TInterfaceResolverTInjectableObject用于控制反转(IoC)/依赖注入(DependencyInjection)}type///在依赖注入(又名IoC)问题发生时抛出的异常EInterfaceResolver=class(ESynException);{$M+}///抽象工厂类,允许......
  • Unity ECS System在什么时候更新?如何自定义这个更新的时机?
    在什么时候更新?在其他用户代码都执行完之后。去Netcode的ClientServerBootstrap里可以找到CreateLocalWorld函数,里面有类似这样的代码:publicstaticWorldCreateLocalWorld(stringdefaultWorldName="DefaultWorld"){varworld=newWorld(defaultWorldName,WorldF......
  • Chrome 浏览器更新后,我的 python tagui 代码工作不稳定。问题的解决方法是什么
    我编写了一个pythontagui脚本来在chrome中操作Intranet网页。它运行良好。但是在chrome浏览器更新后,脚本每次都会在某个时刻随机停止,并且也不会抛出错误。请帮忙。我尝试重新启动电脑,但这并没有解决问题。Chrome浏览器更新后,TagUI脚本工作不稳定的问题可能由以......
  • 在K8S中,滚动更新的过程是什么?
    在Kubernetes(K8S)中,滚动更新是一种更新应用程序的方式,它允许逐步替换旧版本的Pods以使用新版本的Pods,从而减少停机时间,并确保服务的连续性。以下是滚动更新的详细过程:定义Deployment资源:首先,需要定义一个Deployment资源,它描述了Pods的期望状态和更新策略。Deployment负责管理P......
  • 【React】useState:状态更新规则详解
    文章目录一、基本用法二、直接修改状态vs使用`setState`更新状态三、对象状态的更新四、深层次对象的更新五、函数式更新六、优化性能的建议在React中,useState是一个非常重要的Hook,用于在函数组件中添加状态管理功能。正确理解和使用useState更新状态的规......
  • 没发论文的注意啦!重磅更新!GWO-BP-AdaBoost预测!灰狼优化、人工神经网络与AdaBoost集成
      ......