snax学习

snax是一个方便 skynet 服务实现的简单框架。(简单是相对于 skynet 的 api 而言)

在空余时间,看了下源码的实现,发现在实现上别具一格,让自己对 skynet的玩法上和 lua 的语法上有了更深的体会。

个人对 snax 服务的理解:

  1. 对 skynet.call、skynet.send 封装,能通过操作一张表的方法就能达到 call、send 的效果。
  2. 对于远程rpc调用,也能达到一样的效果。不需要显示的用 cluster.xxx 调用。
  3. 可以做到简单的热更新,主要用于修复bug。

snax 服务创建后会返回一张表,然后对表的操作。就相对于对snax服务的操作。就以官方 testping.lua 为例,介绍 snax 实现流程。

--testping.lua
local ps = snax.newservice("pingserver", "hello world") -- 相当于创建一个 pingserver 服务,并传参数 "hello world",返回一张表
print(ps.req.ping("foobar")) -- req.xxx() 方法调用可以理解为 skynet.call("pingserver", "lua", "response.ping", "foobar")
print(ps.post.hello())       -- post.xxx() 方法调用可以理解为 skynet.send("pingserver", "lua", "accept.hello")

之所以用 相当于创建一个 pingserver 服务 ,是因为底层实现上并没有直接用 skynet.newservice("pingserver"),而是先创建一个叫 snaxd 的服务,再由 snaxd 服务加载 pingserver 模块。

像代码中写的那样,大概可以推测,ps.req.ping("foobar") ,表 ps 中的 req.xxx 方法是不可能存在的,因为你无法预知 pingserver 里面到底有哪些方法,然后在把这些方法插入到 ps.req 中。那么我们可以从 lua 的基础语法中想到,对于访问一个表中不存在的字段,通常要设置元表,并且元表带有 __index 字段(表示对于访问不存在的字段或数据,我们应该怎样操作),snax.newservice 确实也是这么干的。在返回的 ps 表中,设置了元表, __index 指向的方法中,调用了 skynet.call 或者 skynet.send 方法,把 ping 、foobar 传给 snaxd 服务,再由snaxd 调用 pingserver 模块里面的方法,并传参。就是这样,实现了一次 ps.req.ping("foobar") 的调用。

下面的介绍中,都是围绕 test/ testping.lua 和 test/pingserver.lua 为例。

下面来看看整个流程的代码实现部分

--snax.lua
local snax_interface = require "snax.interface"

function snax.interface(name)
    if typeclass[name] then
        return typeclass[name]
    end

    local si = snax_interface(name, G)  -- (1)
    local ret = {  -- (2)
        name = name,
        accept = {},
        response = {},
        system = {},
    }

    for _,v in ipairs(si) do
        local id, group, name, f = table.unpack(v)
        ret[group][name] = id
    end

    typeclass[name] = ret
    return ret
end

function snax.rawnewservice(name, ...)
    local t = snax.interface(name)
    local handle = skynet.newservice("snaxd", name) --创建一个 snaxd 服务,并把 模块名 name 传递过去
    assert(handle_cache[handle] == nil)
    if t.system.init then
        skynet.call(handle, "snax", t.system.init, ...)
    end
    return handle
end

function snax.newservice(name, ...)
    local handle = snax.rawnewservice(name, ...)
    return snax.bind(handle, name)
end

创建 一个 snax 服务,有几个步骤需要屡清楚

  1. snax_interface() 方法的作用
  2. 注释中 第 (2) 处表 ret 里面字段存的内容是什么
  3. 模块 name 的 init() 调用时机
  4. snax.bind() 方法做了什么

对于 snax_interface 方法作用,可以先看下源代码的实现细节,有个初步的认识

-- lualib/snax/interface.lua
local skynet = require "skynet"

-- 加载 lua 文件
local function dft_loader(path, name, G)
    local errlist = {}

    for pat in string.gmatch(path,"[^;]+") do
        local filename = string.gsub(pat, "?", name)
        local f , err = loadfile(filename, "bt", G) -- 加载文件,并设置 _ENV 环境
        if f then
            return f, pat -- 如果模块存在,返回模块的代码块
        else
            table.insert(errlist, err)
        end
    end

    error(table.concat(errlist, "\n")) -- 出错打印
end

--[[
name: 加载模块的模块名称,如果以testping 为例,那么 name 就是 pingserver
G: 做为新模块的 _ENV
loader: 加载模块的方式,可以为空
]]
return function (name , G, loader)
    loader = loader or dft_loader -- 选择加载模块 name 的方式,可以传参,也可以不传,为空时,用默认 dft_loader 加载文件
    local mainfunc

    local function func_id(id, group)
        local tmp = {}
        local function count( _, name, func)
            ...
            tmp[name] = true
            table.insert(id, { #id + 1, group, name, func} )
        end
        return setmetatable({}, { __newindex = count }) -- (2)
    end

    local temp_global = {}
    local env = setmetatable({} , { __index = temp_global })
    local func = {}

    local system = { "init", "exit", "hotfix", "profile"}

    do
        for k, v in ipairs(system) do
            system[v] = k
            func[k] = { k , "system", v }
        end
    end

    env.accept = func_id(func, "accept")      -- 当给 accept 添加方法时,触发局部 count 方法,见(2)
    env.response = func_id(func, "response")  -- 当给 response 添加方法时,触发局部 count 方法,见(2)

    local function init_system(t, name, f)
        local index = system[name]
        if index then
            if type(f) ~= "function" then
                error (string.format("%s must be a function", name))
            end
            func[index][4] = f -- (3)
        else
            temp_global[name] = f -- (4)
        end
    end

    local pattern

        local path = assert(skynet.getenv "snax" , "please set snax in config file")
        mainfunc, pattern = loader(path, name, G) -- (1)

    setmetatable(G, { __index = env , __newindex = init_system })
    local ok, err = xpcall(mainfunc, debug.traceback)
    setmetatable(G, nil)
    assert(ok,err)

    for k,v in pairs(temp_global) do 
        G[k] = v  -- (5)
    end

    return func, pattern
end

在整个文件最后返回 一个方法(闭包形式),初看起来有点绕,但它主要做了如下几件事:

  1. 加载 name 模块(pingserver)
    • loader 参数:一个加载器,根据 name 加载新模块文件,但没有运行它,只是设置了新的 _ENV 环境 。

      mainfunc, pattern = loader(path, name, G) -- (1)
      
    • 设置全局变量 accept、response 访问方式:给 _ENV(即_G) 添加了元方法

env.accept = func_id(func, "accept")
env.response = func_id(func, "response")
setmetatable(G, { __index = env , __newindex = init_system })

所以在新模块 name(pingserver) 中,访问 accept、response 变量不需要声明,可直接获取到。

  1. 调用 name 模块(pingserver)

     local ok, err = xpcall(mainfunc, debug.traceback)
    
    • 如果 name 模块中包含 "init", "exit", "hotfix", "profile" 令名的方法,会放到 func 表中,因为如果全局方法不存在的话,必然触发 _ENV 的元方法 __newindex = init_system()。在 init_system 方法中,就会保存这些全局方法到 func 表中,形成一个关系映射,见 (3) 处。

新模块的 init、exit、hotfix、profile 全局方法加入到 func 中
func[1] = {1, "system", "init", init() }
func[2] = {2, "system", "exit", exit() }
func[3] = {3, "system", "hotfix", hotfix() }
func[4] = {4, "system", "profile", profile() }
...
新模块的 accept.hello、accept.sleep ...等方法加入到 func 中
func[5] = {5, "accept", "hello", accept.hello() }
func[6] = {6, "accept", "sleep", accept.sleep() }
...
关系映射(key 为数组下标 - value 为方法表集)

  • 如果 name 模块中的全局函变量不是叫 "init", "exit", "hotfix", "profile",就保存到 temp_global 中。见 (4) 处。最后,再把 temp_global 里面的 key-value 保存到 _G 中,也就是 _ENV(见(5) 处),再之后的访问中,就可以直接从 _ENV 中访问到全局方法或者全局变量,而不再需要通过 __index 元方法索引。这样做的好处,个人理解,一个是为了加快访问速度,一个可以很方便的把 accept.xxx()、 response.xxx()、init()、exit()、hotfix()、profile() 等方法都保存到 func 中。最终,这个模块返回的就是这个关系映射 func,以及 pingserver 模块的配置路径(例如,以pingserver 为例的话,配置路径:./test/?.lua)。
    用一句话总结 interface.lua 模块的作用,就是完成 name 模块加载调用,并把模块里面的方法都存储到一个表中,最终返回这个表(func)。

介绍完 snax_interface() 方法的作用后,再看看 snax 服务中 的第 (2) 处 表 ret 存储内容:

--lublib/snax.lua
function snax.interface(name)
    if typeclass[name] then
        return typeclass[name] -- 如果有,直接返回,typeclass 缓存了 snax_interface() 的结果
    end

    local si = snax_interface(name, G)

    local ret = {
        name = name,
        accept = {},
        response = {},
        system = {},
    }

    for _,v in ipairs(si) do
        local id, group, name, f = table.unpack(v)
        ret[group][name] = id -- 注意这里的值是 id, 也就是 func 表的数组下标
    end

    typeclass[name] = ret
    return ret
end

在知道 snax_interface() 方法返回结果后,就不难得出, ret 表最终存储的内容:

ret = {
name = "pingserver",
system = {init=1, exit=2, hotfix=3, profile=4},
reponse = {ping=5, error=6, ...},
accept = {hello=7, sleep=8, ...},
}

最终把 ret 表缓存到 typeclass 表中,方便下次 通过 name 索引时,直接通过 typeclass 返回, 提高访问效率。

如果 name 模块中定义了 init 全局函数,t.system.init 不为空(id=1,func 数组索引为1处),就会在 snaxd 服务的消息派发函数中,调到 name 模块的 init 方法,并传递 ... 可变参数,进行初始化工作。而例子中 pingserver 模块恰好 有这个init() 方法的声明,所以运行例子,会发现在控制台中有打印 init() 中的 print ("ping server start:", ...)

-- pingserver.lua
function init( ... )
    print ("ping server start:", ...)
    ...
end

-- snax.lua
function snax.rawnewservice(name, ...)
    ...
    if t.system.init then
        skynet.call(handle, "snax", t.system.init, ...)
    end
    ...
end

最后,了解下 snax.bind(handle, name) 方法的作用。其中 handle 是 snaxd 服务句柄,或者叫服务地址,name 是模块名称。

--lualib/snax.lua
local function gen_post(type, handle)
      ...
        skynet_send(handle, "snax", id, ...)
      ...
end

local function gen_req(type, handle)
      ...
        return skynet_call(handle, "snax", id, ...)
      ...
end

local function wrapper(handle, name, type)
    return setmetatable ({
        post = gen_post(type, handle),
        req = gen_req(type, handle),
        type = name,
        handle = handle,
        }, meta)
end

function snax.bind(handle, type)
    local ret = handle_cache[handle] -- 缓存 handle 是 snaxd 服务的句柄
    if ret then
        assert(ret.type == type) -- type 是模块 name 名称
        return ret
    end
    local t = snax.interface(type)
    ret = wrapper(handle, type, t)
    handle_cache[handle] = ret
    return ret
end

从代码可以看出,调用 wrapper 方法,创建一个 table,其中 post、req 字段分别绑定了 name 模块中 accept 开头、response 开头的方法,因为他们都设置了元表 __index 访问方式。从中可以看出,当调用 ps.post.xxx(...) 时,即调用 skynet_send(handle, "snax", id, ...),发送给 snaxd 服务,再由 snaxd 服务调用 name 模块的 accept.xxx(...) 方法。(其中,id 为 xxx 在 func 表中的索引)

总结:

  1. snax 服务,先由 interface 加载 name 模块, 再隐式创建 snaxd 服务,由 snaxd 绑定 name 模块,再由 snaxd 服务负责 name 模块方法的调用
  2. 在snax 模块使用上,post(即 aceept ) 对应 skynet.send 方法,没有返回值,req(即 response ) 对应 skynet.call 方法

调用过程:
snax -> snaxd -> name 模块

你可能感兴趣的:(snax学习)