为什么要封装?封装可以减少一些重复代码,提高我们的工作效率。
新建文件lualib/service.lua,定义模块的属性, service模块是对Skynet服务的一种封装,代码如下所示:
local skynet = require "skynet"
local cluster = require "skynet.cluster"
local M = {
--类型和id
name = "",
id = 0,
--回调函数
exit = nil,
init = nil,
--分发方法
resp = {},
}
return M
如下图中的gateway1,它的name是gateway,id是1;对于agentmgr,它的name是agentmgr,id是0(全局唯一)。
给service模块添加如下所示的start方法,用于开启服务。当外部调用start方法时,它先给name和id赋值,再调用skynet.start开启服务。服务启动后,Skynet会调用init方法,由它调用skynet.dispatch实现消息的路由,再调用上层的M.init()。
function init()
skynet.dispatch("lua", dispatch)
if M.init then
M.init()
end
end
function M.start(name, id, ...)
M.name = name
M.id = tonumber(id)
skynet.start(init)
end
调用流程如下图所示,从服务脚本调用s.start开始(图中阶段①),一直到服务脚本的init方法被调用(图中阶段⑤)。图中的服务脚本是服务的Lua代码,封装层代表service模块,skynet代表Skynet的原生API。
消息分发方法dispatch如下代码所示,它会查找消息方法表resp[cmd],如果没定义处理方法(if not fun then),则直接返回;如果定义了处理方法,使用xpcall安全地调用处理方法,再根据返回值做出不同处理。如果返回值为空(if not isok then),则会直接返回,否则把返回结果发回给发送方(skynet.retpack)。
function traceback(err)
skynet.error(tostring(err))
skynet.error(debug.traceback())
end
local dispatch = function(session, address, cmd, ...)
local fun = M.resp[cmd]
if not fun then
skynet.ret()
return
end
local ret = table.pack(xpcall(fun, traceback, address, ...))
local isok = ret[1]
if not isok then
skynet.ret()
return
end
skynet.retpack(table.unpack(ret,2))
end
参数 | 说明 |
address | 代表消息发送方 |
cmd | 代表消息名的字符串 |
fun | 消息处理方法 |
xpcall | 安全的调用fun方法: 如果fun方法报错,程序不会中断,而是会把错误信息转交给第2个参数的traceback; 如果程序报错,xpcall会返回false; 如果程序正常执行,xpcall返回的第一个值为true,从第2个值开始才是fun的返回值。xpcall会把第3个及后面的参数传给fun,即fun的第1参数是address,从第2个参数开始是可变参数“...”。 |
traceback | 作为xpcall的第2个参数,功能是打印出错误提示和堆栈 |
ret | xpcall返回值的打包: 如果fun方法报错,那ret[1]将是false,否则为true; 如果为false,调用skynet.ret()直接返回。 |
skynet.retpack | fun方法的真正返回值从ret[2]开始,用table.unpack解出ret[2]、ret[3]……,并返回给发送方。 |
调用流程如下图所示:
在阶段①,login1向agentmgr发送reqlogin请求,agentmgr收到后,经由Skynet调用封装层(即service模块,阶段②),再调用服务脚本的s.resp.reqlogin实现分发(阶段③)。图中reqlogin处理完消息后,返回true(阶段④),返回值经由封装层(阶段⑤)最终发回给login1(阶段⑦)。
service模块还会提供一些辅助方法,以减少服务脚本的代码量。封装了call和send方法,用于抹平节点差异(在理解节点间通信代价后使用)。代码如下所示:
function M.call(node, srv, ...)
local mynode = skynet.getenv("node")
if node == mynode then
return skynet.call(srv, "lua", ...)
else
return cluster.call(node, srv, ...)
end
end
function M.send(node, srv, ...)
local mynode = skynet.getenv("node")
if node == mynode then
return skynet.send(srv, "lua", ...)
else
return cluster.send(node, srv, ...)
end
end
参数node代表接收方所在的节点,srv代表接收方的服务名。程序先用skynet.getenv获取当前节点,如果接收方在同个节点,则调用skynet.call;如果在不同节点,则调用cluster.call。
(1)现在试一试使用刚刚完成的service模块写一个空的gateway服务,并启动它。新建
sevice/gateway/init.lua,编写如下所示的代码:
local skynet = require "skynet"
local s = require "service"
function s.init()
skynet.error("[start]" .. s.name .. " " .. s.id)
end
s.start(...)
s.start(...)中的“...”代表可变参数,在用skynet.newservice启动服务时,可以传递参数给它。service模块将会把第1个参数赋值给s.name,第2个参数赋值给s.id。空服务没有任何功能,仅在启动时打印一条日志。
(2)修改service/main.lua,创建一个gateway服务,代码如下所示:
local skynet = require "skynet"
skynet.start(function()
--初始化
skynet.error("[start main]")
skynet.newservice("gateway", "gateway", 1)
--退出自身
skynet.exit()
end)
skynet.newservice("gateway", "gateway", 1)的第1个参数gateway代表着要启动的服务类型,第2个和第3个参数则会被传进s.start(...)的可变参数。
(3)运行结果如下图所示,节点先启动主服务main,main启动gateway1,最后main服务调用skynet.exit()退出。
完整代码地址:https://gitee.com/frank-yangyu/ball-server