skynet 热更新

有了前面的 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:

 

重载一个 skynet 中的 lua 服务

 

如何让 lua 做尽量正确的热更新

 

有机会再来剖析一下。

 

欢迎加入QQ群 858791125 讨论skynet,游戏后台开发,lua脚本语言等问题。

 

 

 

 

 

 

 

 

 

 

参看:

https://www.cnblogs.com/RainRill/p/8940673.html

https://blog.csdn.net/mycwq/article/details/53943890

 

 

 

你可能感兴趣的:(skynet,skynet源码剖析)