skynet热更新之inject

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

skynet内置服务debug_console

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

lua 复制代码
skynet.newservice("debug_console", "127.0.0.1", "9666")

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

sh 复制代码
> nc 127.0.0.1 9666

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

sh 复制代码
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指令,我们可以将某个代码文件,注入到指定的服务中:

sh 复制代码
inject :0000000a service/hotfix.lua

<CMD OK>

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

inject实例

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

lua 复制代码
--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中,我们根据参数,调用不同的处理函数:

lua 复制代码
--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秒输出:

sh 复制代码
foo
cmd not found   bar

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

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指令:

sh 复制代码
echo 'inject :0000000a services/hotfix.lua' | nc 127.0.0.1 9666

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

sh 复制代码
foo after hotfix
bar after hotfix

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

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

对upValue的处理

如果我们的serviceA是这样的:

lua 复制代码
--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绑定起来。

完整代码如下:

lua 复制代码
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类型的消息到目标服务:

lua 复制代码
--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消息类型的处理:

lua 复制代码
--lualib/skynet.lua
-- Inject internal debug framework
local debug = require "skynet.debug"
debug.init(skynet, {
	dispatch = skynet.dispatch_message,
	suspend = suspend,
	resume = coroutine_resume,
})
lua 复制代码
--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是这样处理的

lua 复制代码
--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中直接使用这两个参数。

相关推荐
软件资深者2 天前
植物大战僵尸1经典版(小游戏)+超强辅助工具 自动收取阳光
windows·游戏程序·windows11
孟无岐4 天前
【Laya】Socket 使用指南
websocket·typescript·游戏引擎·游戏程序·laya
Watermelo6174 天前
随机扣款实现赛博共产主义,《明日方舟:终末地》公测支付事故复盘
数据库·后端·游戏程序·技术美术·用户体验·游戏策划·游戏美术
晚霞的不甘4 天前
Flutter 方块迷阵游戏开发全解析:构建可扩展的关卡式益智游戏
前端·flutter·游戏·游戏引擎·游戏程序·harmonyos
孟无岐7 天前
【Laya】HttpRequest 网络请求
网络·typescript·游戏引擎·游戏程序·laya
孟无岐8 天前
【Laya】LocalStorage 本地存储
typescript·游戏引擎·游戏程序·laya
怣疯knight8 天前
外部类触发角色状态切换
游戏程序
孟无岐10 天前
【Laya】Byte 二进制数据处理
网络·typescript·游戏引擎·游戏程序·laya
孟无岐10 天前
【Laya】ClassUtils 类反射工具
typescript·游戏引擎·游戏程序·laya
孟无岐10 天前
【Laya】Ease 缓动函数
typescript·游戏引擎·游戏程序·laya