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服务识别。

posted on 2018-04-27 21:40  RainRill  阅读(1916)  评论(0编辑  收藏  举报

导航