skynet有兩種方法支持熱更新lua代碼:clearcache和inject,在介紹skynet熱更新機制之前,先介紹skynet控制臺,參考官方wiki https://github.com/cloudwu/skynet/wiki/DebugConsole
1. skynet控制臺
想要使用skynet控制臺,需啟動debug_console服務skynet.newservice("debug_console", ip, port),指定一個地址,skynet啟動后,用nc命令就可以進入控制臺,如圖,

debug_console服務啟動后,監聽外部連接(第3行),
第15行,當打開控制臺連接建立后,fork一個協程在console_main_loop里處理這個tcp連接的通信互動
第6-13行,使用特定的print,資料不是輸出到螢屏上,而是通過socket.write發送給控制臺
第24-28行,獲取控制臺發來的資料,然后呼叫docmd
第35-52行,決議出相應指令,執行完后,通過print發送給控制臺
-- service/debug_console.lua
skynet.start(function()
local listen_socket = socket.listen (ip, port)
skynet.error("Start debug console at " .. ip .. ":" .. port)
socket.start(listen_socket , function(id, addr)
local function print(...)
local t = { ... }
for k,v in ipairs(t) do
t[k] = tostring(v)
end
socket.write(id, table.concat(t,"\t"))
socket.write(id, "\n")
end
socket.start(id)
skynet.fork(console_main_loop, id , print)
end)
end)
local function console_main_loop(stdin, print)
print("Welcome to skynet console")
skynet.error(stdin, "connected")
local ok, err = pcall(function()
while true do
local cmdline = socket.readline(stdin, "\n")
...
if cmdline ~= "" then
docmd(cmdline, print, stdin)
end
end
end)
...
end
local function docmd(cmdline, print, fd)
local split = split_cmdline(cmdline)
local command = split[1]
local cmd = COMMAND[command]
local ok, list
if cmd then
ok, list = pcall(cmd, table.unpack(split,2))
else
...
end
if ok then
...
print(list)
print("<CMD OK>")
else
print(list)
print("<CMD Error>")
end
end
比如,在控制臺輸入"list",最侄訓呼叫到COMMAND.list(),獲取當前服務資訊,然后回傳給控制臺,于是就有了上面截圖的資訊,
-- service/debug_console.lua
function COMMAND.list()
return skynet.call(".launcher", "lua", "LIST")
end
2. clearcache更新方法
clearcache用于新建服務的熱更新,比如agent,對已有的服務不能熱更新,使用方法很簡單:在控制臺輸入"clearcache"即可,下面分析其原理:
每個snlua服務會啟動一個單獨的lua VM,對于同一份Lua檔案,N個服務就要加載N次到記憶體,skynet對此做了優化,每個Lua檔案只加載一次到記憶體,保存Lua檔案-記憶體映射表,下一個服務加載的時候copy一份記憶體即可,提高了VM的啟動速度(省掉讀取Lua檔案和決議Lua語法的程序),參考官方wiki https://github.com/cloudwu/skynet/wiki/CodeCache
第2-6行,全域的Lua狀態機,以Lua檔案名為key,記憶體指標為value,保存在狀態機的注冊表里,位于堆疊上有效偽索引LUA_REGISTERYINDEX處,
第8行,修改了官方的luaL_loadfilex介面:
第11-15行,呼叫load從全域狀態機的注冊表里獲取檔案名對應的記憶體塊,呼叫lua_clonefunction拷貝一份后即可回傳
第16-18行,第一次加載檔案到記憶體里
第19-26行,呼叫save保存檔案名-記憶體塊的映射,如果有舊的記憶體塊,回傳舊的,否則回傳剛加載的記憶體塊
// 3rd/lua/lauxlib.c
struct codecache {
struct spinlock lock;
lua_State *L;
};
static struct codecache CC;
LUALIB_API int luaL_loadfilex (lua_State *L, const char *filename,
const char *mode) {
...
const void * proto = load(filename);
if (proto) {
lua_clonefunction(L, proto);
return LUA_OK;
}
lua_State * eL = luaL_newstate();
int err = luaL_loadfilex_(eL, filename, mode);
proto = lua_topointer(eL, -1);
const void * oldv = save(filename, proto);
if (oldv) {
lua_close(eL);
lua_clonefunction(L, oldv);
} else {
lua_clonefunction(L, proto);
/* Never close it. notice: memory leak */
}
return LUA_OK;
}
load介面,從全域狀態機CC的注冊表里獲取指定檔案對應的記憶體塊(可能不存在)
// 3rd/lua/lauxlib.c
static const void *
load(const char *key) {
if (CC.L == NULL)
return NULL;
SPIN_LOCK(&CC)
lua_State *L = CC.L;
lua_pushstring(L, key);
lua_rawget(L, LUA_REGISTRYINDEX);
const void * result = lua_touserdata(L, -1);
lua_pop(L, 1);
SPIN_UNLOCK(&CC)
return result;
}
save介面,先獲取舊的記憶體塊(12-15行),如果有則直接回傳,否則把新記憶體塊加載到注冊表中(17-19行)
static const void *
save(const char *key, const void * proto) {
lua_State *L;
const void * result = NULL;
SPIN_LOCK(&CC)
if (CC.L == NULL) {
init();
L = CC.L;
} else {
L = CC.L;
lua_pushstring(L, key);
lua_pushvalue(L, -1);
lua_rawget(L, LUA_REGISTRYINDEX);
result = lua_touserdata(L, -1); /* stack: key oldvalue */
if (result == NULL) {
lua_pop(L,1);
lua_pushlightuserdata(L, (void *)proto);
lua_rawset(L, LUA_REGISTRYINDEX);
} else {
lua_pop(L,2);
}
}
SPIN_UNLOCK(&CC)
return result;
}
clearcache的原理就是洗掉這個全域的狀態機,這樣新服務就可以用最新的Lua檔案(load介面回傳NULL),且不影響已有服務的運行,此時,新服務運行新的代碼,舊服務運行舊的代碼,
在控制臺輸入"clearcache"后,最終呼叫到c中的clearcache,洗掉舊的全域VM,然后新建一個(19-20行),
-- service/debug_console.lua
function COMMAND.clearcache()
codecache.clear()
end
// 3rd/lua/lauxlib.c
static int
cache_clear(lua_State *L) {
(void)(L);
clearcache();
return 0;
}
static void
clearcache() {
if (CC.L == NULL)
return;
SPIN_LOCK(&CC)
lua_close(CC.L);
CC.L = luaL_newstate();
SPIN_UNLOCK(&CC)
}
3. inject更新方法
inject譯為“注入”,即將新代碼注入到已有的服務里,讓服務執行新的代碼,可以熱更已開啟的服務,使用方法簡單,在控制臺輸入"inject address xxx.lua"即可,難點在于lua代碼的撰寫,建議只做一些簡單的熱更,其實作原理是:給服務發送訊息,讓其執行新代碼,新代碼修改已有的函式原型(包括upvalues),完成對函式的更新,
第10行,給指定服務發送"DEBUG"型別訊息
第20行,最終呼叫inject介面注入代碼修改函式原型(包括閉包),注:只需修改服務的register_protocol介面以及訊息分發介面
-- service/debug.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
-- lualib/skynet/debug.lua
function dbgcmd.RUN(source, filename)
local inject = require "skynet.inject"
local ok, output = inject(skynet, source, filename , export.dispatch, skynet.register_protocol)
collectgarbage "collect"
skynet.ret(skynet.pack(ok, table.concat(output, "\n")))
end
inject的處理程序:
第7-9行,獲取介面的函式原型(包括閉包),保存在u里
第11-21行,遍歷所有的訊息分發函式(每種訊息型別對應一個函式),通過getupvaluetable介面保存函式原型(包括閉包)
第22-23行,執行新的Lua代碼,通過env里的_U,_P獲取原有的函式原型
-- lualib/skynet/inject.lua
return function(skynet, source, filename , ...)
local output = {}
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)
end
end
end
local env = setmetatable( { print = print , _U = u, _P = p}, { __index = _ENV })
local func, err = load(source, filename, "bt", env)
...
return true, output
end
示例:比如啟動了一個test服務
-- test.lua 1 local skynet = require "skynet"
local CMD = {}
local function test(...)
print(...)
skynet.ret(skynet.pack("OK"))
end
function CMD.ping(msg)
test(msg)
end
skynet.dispatch("lua", function(session, source, cmd, ...)
local f = CMD[cmd]
if f then
f(...)
end
end)
skynet.start(function()
end)
在控制臺輸入"inject address inject_test.lua"熱更test服務,
第23行,通過全域環境變數_P獲取lua型別訊息分發函式里的介面CMD
第24行,獲取CMD.ping介面的所有閉包
第25行,得到test的函式原型
第27-30行,更新介面,完成熱更,
-- inject_test.lua
print("hotfix begin")
if not _P then
print("hotfix faild, _P not define")
return
end
local function get_upvalues(f)
local u = {}
if not f then return u end
local i = 1
while true do
local name, value = debug.getupvalue(f, i)
if name == nil then
return u
end
u[name] = value
i = i + 1
end
end
local CMD = _P.lua.CMD
local upvalues = get_upvalues(CMD.ping)
local test = upvalues.test
CMD.ping = function(msg)
local postfix = "aaa"
test(msg .. postfix)
end
print("hotfix end")
本篇文章就寫到這,在2021年1月13/14號我會開一個四小時玩轉skynet訓練營,也就是兩個禮拜之后,現在已經開放報名,對游戲開發感興趣的諸位同好可以訂閱一下,
訓練營內容大概如下:
1. 多核并發編程
2. 訊息佇列,執行緒池
3. actor訊息調度
4. 網路模塊實作
5. 時間輪定時器實作
6. lua/c介面編程
7. skynet編程精要
8. demo演示actor編程思維
期待與諸位同好共襄技術盛舉
憑借報名截圖可以進群973961276領取上一期skynet訓練營的錄播以及這期的預習資料哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/243229.html
標籤:其他
上一篇:扭倒費賭局問題
下一篇:資料結構與演算法 基礎實驗
