有了前面的 lua 热更新原理(一)作为铺垫,相信理解skynet热更新会容易点。
但是有个问题是,skynet不能像前面讲的那样,重新require文件来达到热更的目的,为什么?
出于效率的考虑,云风大大修改了lua的官方实现,require文件时会缓存之前的代码,再次require该文件,检测到有缓存是不会加载新的文件的。参见:CodeCache
不过那个也可以通过选项来控制,调用skynet.codecache.mode('OFF')就可以破除这个限制。除了通过这个开关,也可以调用skynet.codecache.clear(),这个在debug模式下有这个实现,可以参考skynet debug_console源码分析。
但codecache有个不可忽视的问题,每次codecache后,不管代码有没有用到,skynet不会清理旧的内存。这会导致了多次codecache后,skynet内存使用会越来越大
这是为什么?因为codecache后,只有新起的服务会用到新代码,旧的服务还引用着旧代码。而skynet没有做引用GC的复杂逻辑,在旧服务销毁时,没有清理用不到的旧代码。
这种方式可以用于简单的场景,例如在棋牌游戏中测试时每一局的发牌数据,就可以用这种方式来更新,而不用重启服务器。
如果要替换掉已运行lua文件的代码,则要用应用更高级的lua的更新机制,参见lua 热更新原理(二)
skynet中热更新具体是怎么操作的呢?
再介绍一个函数:
load (chunk [, chunkname [, mode [, env]]])
他的作用是加载chunk代码块,并且以env作为他的上值。我们可以让旧的值保存在env,然后更换新的chunk块,这样就可以达到热更的目的。如果env为nil,默认以全局环境_ENV作为其上值,这样chunk块代码共享全局变量。如果以元表{__index = _ENV}作为env,chunk模块仍然可以共享全局变量,但是加载后的全局环境却没有chunk块中的全局变量。
在skynet控制台中有个命令就是用来加载新的模块的:
function dbgcmd.RUN(source, filename)
local inject = require "skynet.inject"
local output = inject(skynet, source, filename , export.dispatch, skynet.register_protocol)
collectgarbage "collect"
skynet.ret(skynet.pack(table.concat(output, "\n")))
end
其中inject函数就是保存传入函数的上值:
local function getupvaluetable(u, func, unique)
local i = 1
while true do
local name, value = debug.getupvalue(func, i)
if name == nil then
return
end
local t = type(value)
if t == "table" then
u[name] = value
elseif t == "function" then
if not unique[value] then
unique[value] = true
getupvaluetable(u, value, unique)
end
end
i=i+1
end
end
return function(skynet, source, filename , ...)
if filename then
filename = "@" .. filename
else
filename = "=(load)"
end
local output = {}
local function print(...)
local value = { ... }
for k,v in ipairs(value) do
value[k] = tostring(v)
end
table.insert(output, table.concat(value, "\t"))
end
local u = {}
local unique = {}
local funcs = { ... }
for k, func in ipairs(funcs) do
getupvaluetable(u, func, unique)
end
local p = {}
local proto = u.proto
if proto then
for k,v in pairs(proto) do
local name, dispatch = v.name, v.dispatch
if name and dispatch and not p[name] then
local pp = {}
p[name] = pp
getupvaluetable(pp, dispatch, unique) --1)
end
end
end
local env = setmetatable( { print = print , _U = u, _P = p}, { __index = _ENV })
local func, err = load(source, filename, "bt", env)
if not func then
return { err }
end
local ok, err = skynet.pcall(func)
if not ok then
table.insert(output, err)
end
return output
end
在skynet.lua最后一段很不起眼的代码中:
local debug = require "skynet.debug"
debug(skynet, {
dispatch = skynet.dispatch_message,
clear = clear_pool,
suspend = suspend,
})
结合上面的代码,我们可以看到递归获取了skynet.dispatch_message,skynet.register_protocol函数的上值。正是在skynet.register_protocol函数中我们定义了自己的回调函数。上面的1)中获取了回调函数的上值。在的回调函数中,我们一般会如此写代码:
skynet.start(function()
skynet.dispatch("lua", function (_, address, cmd, ...)
local f = CMD[cmd] --2)
if f then
skynet.ret(skynet.pack(f(address, ...)))
end
end)
end)
关键的执行代码就在2)处,而2)是回调函数的上值,所以我们可以轻松的替换掉他,从而实现热更的目的。
其实以上的热更思路也只是替换了关键的函数,能不能做到不停机重启一个服务呢?这个看看云风大大的blog:
有机会再来剖析一下。
欢迎加入QQ群 858791125 讨论skynet,游戏后台开发,lua脚本语言等问题。
参看:
https://www.cnblogs.com/RainRill/p/8940673.html
https://blog.csdn.net/mycwq/article/details/53943890