【从零开始学Skynet】实战篇《球球大作战》(三):封装常用的API

        为什么要封装?封装可以减少一些重复代码,提高我们的工作效率。

1、定义属性

        新建文件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
  • name代表服务的类型id代表服务编号。

        如下图中的gateway1,它的namegatewayid1;对于agentmgr,它的nameagentmgrid0(全局唯一)

  • initexit是回调方法,在服务初始化和退出时会被调用(本篇暂不实现exit的功能)。
  • resp表会存放着消息处理方法。

【从零开始学Skynet】实战篇《球球大作战》(三):封装常用的API_第1张图片

 2、启动逻辑

        给service模块添加如下所示的start方法,用于开启服务。当外部调用start方法时,它先给nameid赋值,再调用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

【从零开始学Skynet】实战篇《球球大作战》(三):封装常用的API_第2张图片

3、消息分发 

         消息分发方法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]……,并返回给发送方。

调用流程如下图所示:

【从零开始学Skynet】实战篇《球球大作战》(三):封装常用的API_第3张图片

         在阶段①,login1agentmgr发送reqlogin请求,agentmgr收到后,经由Skynet调用封装层(即service模块,阶段②),再调用服务脚本的s.resp.reqlogin实现分发(阶段③)。图中reqlogin处理完消息后,返回true(阶段④),返回值经由封装层(阶段⑤)最终发回给login1(阶段⑦)。

4、辅助方法

        service模块还会提供一些辅助方法,以减少服务脚本的代码量。封装了callsend方法,用于抹平节点差异(在理解节点间通信代价后使用)。代码如下所示:

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

 5、编写空服务

        (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()退出。

【从零开始学Skynet】实战篇《球球大作战》(三):封装常用的API_第4张图片

完整代码地址:https://gitee.com/frank-yangyu/ball-server

你可能感兴趣的:(从零开始学Skynet,lua,Skynet,服务器开发)