snax是一个方便 skynet 服务实现的简单框架。(简单是相对于 skynet 的 api 而言)
在空余时间,看了下源码的实现,发现在实现上别具一格,让自己对 skynet的玩法上和 lua 的语法上有了更深的体会。
个人对 snax 服务的理解:
- 对 skynet.call、skynet.send 封装,能通过操作一张表的方法就能达到 call、send 的效果。
- 对于远程rpc调用,也能达到一样的效果。不需要显示的用 cluster.xxx 调用。
- 可以做到简单的热更新,主要用于修复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 服务,有几个步骤需要屡清楚
- snax_interface() 方法的作用
- 注释中 第 (2) 处表 ret 里面字段存的内容是什么
- 模块 name 的 init() 调用时机
- 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
在整个文件最后返回 一个方法(闭包形式),初看起来有点绕,但它主要做了如下几件事:
- 加载 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 变量不需要声明,可直接获取到。
-
调用 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 表中的索引)
总结:
- snax 服务,先由 interface 加载 name 模块, 再隐式创建 snaxd 服务,由 snaxd 绑定 name 模块,再由 snaxd 服务负责 name 模块方法的调用
- 在snax 模块使用上,post(即 aceept ) 对应 skynet.send 方法,没有返回值,req(即 response ) 对应 skynet.call 方法
调用过程:
snax -> snaxd -> name 模块