skynet源码分析之snax
snax是一个方便实现skynet服务的简单框架,对服务的接口(比如skynet.call, skynet.send等)做了进一步的封装,编写snax服务比较容易,详情参考官方wiki https://github.com/cloudwu/skynet/wiki/Snax
下面是一个简单的snax服务代码,并不是独立的Lua程序。包含了一组Lua函数,会被snax框架加载分析。一般至少包含4组函数:init,服务的初始接口;exit,服务的退出接口;response前缀,响应其他服务请求并给出回应的方法;accept前缀,响应其他服务请求不需要回应的方法。
-- print("xxx")
1 function response.f1(msg) 2 return a .. msg 3 end 4 5 function response.f2(msg) 6 return a .. msg 7 end 8 9 function accept.g1(...) 10 print(...) 11 end 12 13 function accept.g2(...) 14 print(...) 15 end 16 17 function init() 18 print("test start!!!") 19 end 20 21 function exit() 22 print("test exit!!!") 23 end
1. snax框架如何分析Lua代码
接下来重点分析snax框架如何加载分析Lua代码:
第10行,返回一个闭包,需三个参数:name,snax服务的名字;G,全局环境,默认是空表{};loader,加载Lua文件的接口,不提供则用默认的。
第4-18行,默认的加载器,通过Lua文件名匹配到路径,然后调用loadfile加载Lua文件。注:这时不能用print,math系统api,因为还未设置package.path路径。
第24-40行,注册response组和accept组的接口信息,对参数做相关检查,比如不能出现相同的接口名字
第55-62行,注册system组的接口信息
第64-65行,注册response组合accept组接口信息
第78-81行,用指定loader加载器加载Lua代码
第83-85行,分析(运行)Lua代码,设置了全局环境G的__index=env,__newindex=init_system。以上面代码为例,分析流程:
response.f1 -> env.reponse -> func_id -> count,把接口信息保存在func里(第37行)
response.f2, accept.g1, accept.g2流程同上
init -> init_system,把函数原型保存在接口信息第4个索引位置(第72行),前3个位置是{1, "system", "init"}
exit -> init_system,把函数原型保存在接口信息第4个索引位置(第72行),前3个位置是{1, "system", "exit"}
1 -- lualib/snax/interface.lua 2 local skynet = require "skynet" 3 4 local function dft_loader(path, name, G) 5 local errlist = {} 6 7 for pat in string.gmatch(path,"[^;]+") do 8 local filename = string.gsub(pat, "?", name) 9 local f , err = loadfile(filename, "bt", G) 10 if f then 11 return f, pat 12 else 13 table.insert(errlist, err) 14 end 15 end 16 17 error(table.concat(errlist, "\n")) 18 end 19 20 return function (name , G, loader) 21 loader = loader or dft_loader 22 local mainfunc 23 24 local function func_id(id, group) 25 local tmp = {} 26 local function count( _, name, func) 27 if type(name) ~= "string" then 28 error (string.format("%s method only support string", group)) 29 end 30 if type(func) ~= "function" then 31 error (string.format("%s.%s must be function"), group, name) 32 end 33 if tmp[name] then 34 error (string.format("%s.%s duplicate definition", group, name)) 35 end 36 tmp[name] = true 37 table.insert(id, { #id + 1, group, name, func} ) 38 end 39 return setmetatable({}, { __newindex = count }) 40 end 41 42 do 43 assert(getmetatable(G) == nil) 44 assert(G.init == nil) 45 assert(G.exit == nil) 46 47 assert(G.accept == nil) 48 assert(G.response == nil) 49 end 50 51 local temp_global = {} 52 local env = setmetatable({} , { __index = temp_global }) 53 local func = {} 54 55 local system = { "init", "exit", "hotfix", "profile"} 56 57 do 58 for k, v in ipairs(system) do 59 system[v] = k 60 func[k] = { k , "system", v } 61 end 62 end 63 64 env.accept = func_id(func, "accept") 65 env.response = func_id(func, "response") 66 local function init_system(t, name, f) 67 local index = system[name] 68 if index then 69 if type(f) ~= "function" then 70 error (string.format("%s must be a function", name)) 71 end 72 func[index][4] = f 73 else 74 temp_global[name] = f 75 end 76 end 77 78 local pattern 79 80 local path = assert(skynet.getenv "snax" , "please set snax in config file") 81 mainfunc, pattern = loader(path, name, G) 82 83 setmetatable(G, { __index = env , __newindex = init_system }) 84 local ok, err = xpcall(mainfunc, debug.traceback) 85 setmetatable(G, nil) 86 assert(ok,err) 87 88 for k,v in pairs(temp_global) do 89 G[k] = v 90 end 91 92 return func, pattern 93 end
第92行,返回分析后的接口信息func。即:
{{1, "system", "init", f}, {2, "system", "exit", f}, {3, "system", "hotfix", f}, {4, "system", "profile", f}, {5, "response", "f1", f}, {6, "response", "f2", f}, {7, "accept", "g1", f}, {8, "accept", "g2", f},}
snax.interface api是将snax_interface返回的一维表转化成key-value形式。即:
{
name = "test",
system = {init=1, exit=2, hotfix=3, profile=4},
reponse = {f1=5, f2=6},
accept = {g1=7, g2=8},
}
1 -- lualib/skynet/snax.lua 2 function snax.interface(name) 3 if typeclass[name] then 4 return typeclass[name] 5 end 6 7 local si = snax_interface(name, G) 8 9 local ret = { 10 name = name, 11 accept = {}, 12 response = {}, 13 system = {}, 14 } 15 16 for _,v in ipairs(si) do 17 local id, group, name, f = table.unpack(v) 18 ret[group][name] = id 19 end 20 21 typeclass[name] = ret 22 return ret 23 end
2. snaxd服务
介绍完snax框架如何分析Lua代码,接下来介绍snax服务的工作流程:
第2行,通过snax.newservice启动一个snaxd服务,服务名为name
第8行,分析加载服务名对应的Lua文件
第9行,启动名称为"snaxd"的snlua服务,参数是name
第12行,给该服务发送"snax"类型消息,第一个参数是t.system.init(即id是1)
1 -- lualib/skynet/snax.lua 2 function snax.newservice(name, ...) 3 local handle = snax.rawnewservice(name, ...) 4 return snax.bind(handle, name) 5 end 6 7 function snax.rawnewservice(name, ...) 8 local t = snax.interface(name) 9 local handle = skynet.newservice("snaxd", name) 10 assert(handle_cache[handle] == nil) 11 if t.system.init then 12 skynet.call(handle, "snax", t.system.init, ...) 13 end 14 return handle 15 end
启动snaxd服务过程,
第2-10行,分析snax服务的Lua代码,设置package.path路径以及SERVICE_NAME,SERVICE_PATH等
第47行,设置"snax"类型的消息分发函数dispatcher。调用snax.newservice会收到"snax"消息,第一个参数id是1(t.system.init),在system组里:
第15行,通过id获取接口信息,此时method={1, "system", "init", f},然后执行24行分支
第24-32行,执行snax服务的Lua文件的init函数,即snax服务的初始化逻辑放到init函数里
第34-40行,同理,snax服务退出时,通常把退出逻辑放到exit函数里。除了init、exit,system组还包含hotfix,profile,都有相应的处理。
1 -- service/snaxd.lua 2 local snax_name = tostring(...) 3 local loaderpath = skynet.getenv"snax_loader" 4 local loader = loaderpath and assert(dofile(loaderpath)) 5 local func, pattern = snax_interface(snax_name, _ENV, loader) 6 local snax_path = pattern:sub(1,pattern:find("?", 1, true)-1) .. snax_name .. "/" 7 package.path = snax_path .. "?.lua;" .. package.path 8 9 SERVICE_NAME = snax_name 10 SERVICE_PATH = snax_path 11 12 skynet.start(function() 13 local init = false 14 local function dispatcher( session , source , id, ...) 15 local method = func[id] 16 17 if method[2] == "system" then 18 local command = method[3] 19 if command == "hotfix" then 20 local hotfix = require "snax.hotfix" 21 skynet.ret(skynet.pack(hotfix(func, ...))) 22 elseif command == "profile" then 23 skynet.ret(skynet.pack(profile_table)) 24 elseif command == "init" then 25 assert(not init, "Already init") 26 local initfunc = method[4] or function() end 27 initfunc(...) 28 skynet.ret() 29 skynet.info_func(function() 30 return profile_table 31 end) 32 init = true 33 else 34 assert(init, "Never init") 35 assert(command == "exit") 36 local exitfunc = method[4] or function() end 37 exitfunc(...) 38 skynet.ret() 39 init = false 40 skynet.exit() 41 end 42 else 43 assert(init, "Init first") 44 timing(method, ...) 45 end 46 end 47 skynet.dispatch("snax", dispatcher) 48 49 -- set lua dispatcher 50 function snax.enablecluster() 51 skynet.dispatch("lua", dispatcher) 52 end 53 end)
3. 给snaxd服务发送消息
snax.newservice启动服务后,通过snax.bind绑定后返回一个对象,对象里包含接口信息,供使用者调用:
第8-12行,如果已经绑定过,从缓存里获取即可
第13-14行,获取Lua代码接口信息,然后通过wrapper封装
第19-26行,返回一个table,包含4个域:handle,服务地址;type,服务名称;post,不需要返回的请求;req,需要返回的请求。
当我们用经典的test.req.f1()给test服务发送消息,流程是:调用req域 -> gen_req接口(第28行) -> 获取接口编号id(第31行) -> 给handle服务(snaxd)发送消息(第36行)
1 -- lualib/skynet/snax.lua 2 function snax.newservice(name, ...) 3 local handle = snax.rawnewservice(name, ...) 4 return snax.bind(handle, name) 5 end 6 7 function snax.bind(handle, type) 8 local ret = handle_cache[handle] 9 if ret then 10 assert(ret.type == type) 11 return ret 12 end 13 local t = snax.interface(type) 14 ret = wrapper(handle, type, t) 15 handle_cache[handle] = ret 16 return ret 17 end 18 19 local function wrapper(handle, name, type) 20 return setmetatable ({ 21 post = gen_post(type, handle), 22 req = gen_req(type, handle), 23 type = name, 24 handle = handle, 25 }, meta) 26 end 27 28 local function gen_req(type, handle) 29 return setmetatable({} , { 30 __index = function( t, k ) 31 local id = type.response[k] 32 if not id then 33 error(string.format("request %s:%s no exist", type.name, k)) 34 end 35 return function(...) 36 return skynet_call(handle, "snax", id, ...) 37 end 38 end }) 39 end
snaxd服务收到消息后,因为是response/accept组的信息,最终调用timing(method, ...)。如果是accept类型,直接执行对应的函数即可(第4行);如果是response类型,执行对应的函数并返回(第17行)。至此,完成跟snax服务一次交互。
1 -- service/snaxd.lua 2 local function timing( method, ... ) 3 local err, msg 4 profile.start() 5 if method[2] == "accept" then 6 -- no return 7 err,msg = xpcall(method[4], traceback, ...) 8 else 9 err,msg = xpcall(return_f, traceback, method[4], ...) 10 end 11 local ti = profile.stop() 12 update_stat(method[3], ti) 13 assert(err,msg) 14 end 15 16 local function return_f(f, ...) 17 return skynet.ret(skynet.pack(f(...))) 18 end
4. snax服务的热更新
snax服务支持热更新(只能热更snax类型的服务)。snax框架约定了Lua文件的格式,所以可获取Lua代码里所有闭包信息,通过修改这些闭包状态达到热更的目的。调用snax.hotfix进行热更新(可在debug_console加一个hotfix命令调用snax.hotfix接口),最终调用到inject接口(第9行)。
1 -- lualib/skynet/snax.lua 2 function snax.hotfix(obj, source, ...) 3 local t = snax.interface(obj.type) 4 return test_result(skynet_call(obj.handle, "snax", t.system.hotfix, source, ...)) 5 end 6 7 -- lualib/snax/hotfix.lua 8 return function (funcs, source, ...) 9 return pcall(inject, funcs, source, ...) 10 end
inject接口的参数:funcs原Lua文件的接口信息,source要热更的Lua代码块,以及可变参数。工作流程是:
第2行,通过snax_interface接口分析加载要热更的Lua代码,保存在patch变量中。
第3行,通过funcs获取原有Lua代码所有闭包。
第5-10行,依次处理patch里的每个闭包,调用_patch:
第25-41行,获取闭包每个上值的名字和值,如果热更的Lua代码里没提供值,则表示这个上值不需要热更,复用原来的值即可,即调用debug.upvaluejoin将现有闭包引用原有的上值
第21行,更新完一个闭包的所有上值后,覆盖原来的接口信息。
第12-15行,运行热更的Lua代码里的hotfix接口,在这个接口里可以查看和修改服务的状态(内部local变量的值)。示例参看官方wiki。
1 local function inject(funcs, source, ...) 2 local patch = si("patch", dummy_env, loader(source)) 3 local global = collect_all_uv(funcs) 4 5 for _, v in pairs(patch) do 6 local _, group, name, f = table.unpack(v) 7 if f then 8 patch_func(funcs, global, group, name, f) 9 end 10 end 11 12 local hf = find_func(patch, "system", "hotfix") 13 if hf and hf[4] then 14 return hf[4](...) 15 end 16 end 17 18 local function patch_func(funcs, global, group, name, f) 19 local desc = assert(find_func(funcs, group, name) , string.format("Patch mismatch %s.%s", group, name)) 20 _patch(global, f) 21 desc[4] = f 22 end 23 24 local function _patch(global, f) 25 local i = 1 26 while true do 27 local name, value = debug.getupvalue(f, i) 28 if name == nil then 29 break 30 elseif value == nil or value == dummy_env then 31 local old_uv = global[name] 32 if old_uv then 33 debug.upvaluejoin(f, i, old_uv.func, old_uv.index) 34 end 35 else 36 if type(value) == "function" then 37 _patch(global, value) 38 end 39 end 40 i = i + 1 41 end 42 end
总结:snax是一个简单实用的框架,对比用原生的skynet接口,snax编写出的Lua代码更加直观易懂,且热更新也比较方便。但有几个易错点:
1. snax.queryservice会发送rpc请求,是个阻塞调用。
2. 编写的Lua服务在loadfile过程中不能调用print,math等系统接口,因为还未设置package.path路径。当然,也可以自己设置加载器支持指定的系统接口。
3. snax热更不能增加新的远程response/accept方法。已经新增加的方法不能被snaxd服务识别。