Skip to content
bruce edited this page Mar 18, 2023 · 29 revisions

简介

moon是一个基于Actor模型的轻量级游戏服务器框架,使用少量的核心代码实现了Actor的调度和Lua层API的封装。moon不是一个开箱即用的框架,本身没有包含游戏业务逻辑,主要目标是提供游戏服务器开发的脚手架。这里有一个DEMO可以作为参考,你也可以基于它构建适合自己的服务器框架。

源码目录

  • common 框架公共源码目录
  • lualib-src lua C Module 源码目录
  • moon-src 框架源码目录
  • third 第三方库目录
  • example 示例目录
  • lualib 框架的lua层封装目录
  • service 框架提供的常用服务

框架设计

随着硬件提升,moon设计时希望能充分利用多核的优势,采用了单进程多线程模式。为了简化设计,使用Asio库作为网络和线程通信的基础组件,Asio是一个成熟的跨平台异步IO库,使我们不必关心平台相关的细节,把复杂度限制在可控的范围内。moon采用了One Thread One IoContext的线程调度模型,Lua服务作为Actor的载体,每个线程可以有一个或者多个Lua Service,这样的好处是可以实现独占线程服务共享线程服务

  • 独占线程服务可以表示运行期常驻的服务,如网关,DB Proxy等,这种服务很容易成为整个系统的的热点,希望它们能独占线程,提高响应能力,避免受到其它服务调度的影响

  • 共享线程服务可以表示运行期间动态创建和销毁的服务,如一个玩家,一个组队副本场景等,这种服务功能上通常彼此之间是独立的,天然可拆分,可以多线程负载提高处理能力

moon提供的核心功能就是创建服务,并为它提供一个进程内不重复的ID, 服务间可以互相发送消息, 每个服务可以注册消息回调,来接收发送给它的消息, 每个服务都是被消息驱动的, 定时器也是一种消息。推荐所有服务尽量在同一个进程中, 按业务创建不同的服务,它们协同工作构成游戏服务器的整体,服务间通信不必时刻关心对方是否还活着,通讯数据能否正确到达等问题。进程内的所有服务是同生共死的,对于游戏业务某个环节出了错都可能都是致命的,没必要把问题隐藏起来, 系统处于不完整状态可能会造成更大的损害。框架采用Lua编写逻辑代码C++编写少量核心库的开发方式,Lua作为脚本语言弥补了C++开发效率低,对于健壮性,采用Lua沙盒基本上能隔绝逻辑层的bug。

moon在Lua层API的封装参考了skynet, 所以和skynet有许多相似的地方, 它们最大区别是的调度方式,skynet使用了调度队列服务消息队列实现了近似均衡的调度,同一个服务可能会切换不同的线程间调度。moon只拥有线程消息队列,服务可以独占或者共享线程,独占线程的服务可以有更高的响应能力,并且不受其他服务的影响。对于游戏业务,往往会出现难以拆分的热点服务, 如大地图战斗场景,这种服务的处理能力往往就是游戏服务器的上限,独占线程能最大发挥这种服务的性能。而服务切换不同的线程间调度,往往会降低响应能力,影响处理性能。

现在游戏往往需要跨服交互,对于单个游戏服务器进程可以称之为node, 多个node可以组成一个集群,cluster服务提供了简单的进程间通信功能,基本能满足需求。

消息设计

服务内部采用消息通信,通常是传递一个message指针,这样比进程间通信效率高得多,底层消息定义:

class message{
    private:
        uint8_t type_ = 0;//消息类型
        uint32_t sender_ = 0;//发送者服务id
        uint32_t receiver_ = 0;//接收者服务id
        int32_t sessionid_ = 0;//session
        std::unique_ptr<std::string> header_;//消息头
        std::shared_ptr<moon::buffer> data_;//消息数据
}

消息类型用于区分不同来源的消息,分别定制不同的编解码规则。lua type的消息,采用了一种lua对象序列化的方式,常用于服务器内部通信。

对于网络消息sender表示socket fd, 定时器消息sender表示timerid, 广播消息sender=0 其它情况就是发送者服务id。

消息的数据分为 消息头数据部分消息头是可选部分,需要携带额外信息时可以用它来保存,例如场景服务内某个玩家释放了技能,通过网关服务进行广播,为了广播优化共享同一个消息数据,把接收者uid放到消息头部分,避免了多次序列化的开销。

sessionid 用于请求回应模式,方便像写同步代码一样写异步。有时我们发送一条消息,并希望得到消息的处理结果:发送消息时附带一个sessionid,并绑定一个lua协程,对方收到后把sessionid发送回来,触发协程resume。如下面代码,假设other_service_id所代表的服务提供了func_add函数:

moon.async(function()
    local res,err = moon.co_call("lua",other_service_id,"func_add",1,2)
    if not res then
        print(err)
    else
        print(res)--output 3
    end
end)

消息注册

  1. 注册Lua消息类型的处理函数
local command = {}

command.HELLO = function()
    return "world"
end

moon.dispatch("lua", function(sender, session, cmd, ...)
    local f = command[cmd]
    if f then
        f(...)
    else
        error(string.format("Unknown command %s", tostring(cmd)))
    end
end)

---其他服务
moon.send("lua", id, "HELLO", ...)
  1. 注册自定义的消息类型
local PTYPE_CLIENT = 100 --建议大于100(1-255), 避免与内置定义冲突

moon.register_protocol({
	name = "client",
	PTYPE = PTYPE_CLIENT,
	pack = function(...) --可选,发送消息时会调用(moon.send, moon.co_call)
		-- body
	end,
	unpack = function(sz, len) --可选,收到消息时会调用
		-- body
	end,
	dispatch = function(sender, session, ...)

	end
})

---如果需要自定义解析,需要设置israw = true, 常用于性能相关场合
moon.register_protocol({
	name = "client",
	PTYPE = PTYPE_CLIENT,
    israw = true,
	dispatch = function(msg)
        ---获取message相关信息
        ---'S' message:sender()
        ---
        ---'R' message:receiver()
        ---
        ---'E' message:sessionid()
        ---
        ---'H' message:header()
        ---
        ---'Z' message:bytes()
        ---
        ---'N' message:size()
        ---
        ---'B' message:buffer()
        ---
        ---'C' C-Pointer + size

        ---根据需求获取
        print(moon.decode(msg,"C"))
	end
})

消息的发送和接收

  1. 回调方式

moon中服务间通信需要发送消息,这是个异步过程,通常是注册回调函数来处理逻辑,如:

--发送方
--先注册处理结果回调
local command = {}

command.ADDRESULT = function(sender, result)
    print(result)
end

--此处省略回调注册逻辑

--发送消息
moon.send('lua', serviceid,"ADD",1,2)
--接收方
local command = {}

command.ADD = function(sender, a,b)
    --把结果返回给发送者
    moon.send('lua', sender,'ADDRESULT', a+b)
end
  1. 使用Lua协程取代回调函数

moon.co_call 和 moon.response 组合

  • 发送者:生成一个唯一ID,创建一个协程,并保存ID-协程的映射。 发送消息,把协程ID同时发送给接收者,然后挂起协程。
  • 接收者:收到消息处理完逻辑,把 处理结果 和收到的协程ID 返回给发送者。
  • 发送者:注册一个总的回调函数, 收到结果数据,根据协程ID唤醒协程。
--发送方
--moon.async 实际上是创建一个协程
moon.async(function()
    --像另一个服务发送请求,并使用协程等待调用结果,不需要再注册回调函数
    local result = moon.co_call('lua', serviceid,"ADD",1,2)
    print(result)
end)
--接收方
local command = {}
--sessionid 用于保存调用者的协程ID
command.ADD = function(sender,sessionid,a,b)
    --把结果返回给发送者
    moon.response('lua',sender,sessionid,a+b)
end

服务

moon中的服务是线程调度的最小单元。Lua Service用一个LuaVM表示,它们可以独占或者共享线程。不同Lua Service之间不能直接访问,它们只能通过消息通信来交互。服务主要分为唯一服务普通服务。moon中创建服务的API是moon.new_service(stype, config)

唯一服务

游戏服务器经常会有一些运行期常驻的模块,如 Gate, Center, WorldMap, DB proxy。这些模块拥有共同的特点:

  • 长时间运行,直到进程关闭
  • 经常被其它服务访问
  • 是当前进程节点的重要组件,缺少它们就不能正常运行

这些模块就可以用唯一服务来编写,moon中的唯一服务有如下特点

  • 唯一服务通常非常重要,如果启动失败,服务器就不应该继续运行。
  • 名字唯一, 可以使用moon.queryservice(name)查询到服务ID, 方便被其他服务访问。
  • 必须自己控制唯一服务的退出,方便控制进程关闭流程。如数据库连接服务应该最后退出。
  • 可以独占线程提高处理和响应能力。
  1. 创建唯一服务
moon.async(function()
    local id = moon.new_service("lua", {
        name = "worldmap",
        file = "worldmap.lua",
        unique = true --唯一服务标识
    })
    assert(id>0,"create service failed")
end)
moon.async(function()
    local id = moon.new_service("lua", {
        name = "worldmap",
        file = "worldmap.lua",
        unique = true,
        threadid = 1, --独占id为1的线程
    })
    assert(id>0,"create service failed")
end)
  1. 唯一服务退出
  • 优雅退出

注册一个消息处理,进程关闭时,bootstrap 服务向其它唯一服务,按逻辑顺序,发送消息。

function CMD.Shutdown()
    moon.quit()
end
  • 自动退出
---进程正常关闭时会调用此函数
moon.shutdown(function()
    --- do something
    moon.quit()
end)
  • 强制退出
--other service
moon.kill(id)
  1. 查询唯一服务
local id = moon.queryservice("worldmap")
assert(id>0, "queryservice worldmap failed")
moon.send("lua", id, arg1, arg2, arg3, ...)

普通服务

有些游戏模块是需要在运行时动态创建和删除的,他们多个实例之间通常没有关联,为了利用多核能力提高服务器负载,可以用普通服务表示这些模块。如副本场景,Moba游戏的房间。普通服务不能使用moon.queryservice查询到服务ID。它的创建通常是与逻辑相关的,需要你自己保存。

local roomid = 0
local rooms = {}

moon.async(function()
    for i=1,100 do
        local addr = moon.new_service(service_type,{
            name = "room",
            file = "room.lua",
        })

        roomid = roomid + 1
        rooms[roomid] = addr ---自己保存映射关系
    end
end)

注意普通服务,在收到到进程关闭信号时会直接退出,这样做的好处是,在编写某些命令行脚本时能简化一些代码。如果需要控制普通服务的退出,可以覆盖掉默认退出逻辑,来手动管理

moon.shutdown(function ()
    --do nothing
end)

--然后在管理服务中给它发送消息,优雅的关闭

创建服务时可以传递传递额外的参数

moon.async(function()
    local id = moon.new_service("lua", {
        name = "worldmap",
        file = "worldmap.lua",
        unique = true,
        threadid = 1, --独占id为1的线程
        test_arg1 = 1001,
        test_arg2 = "hello",
        test_arg3 = {a=1,b=2,c=3}
    })
    assert(id>0,"create service failed")
end)

worldmap.lua

local moon = require("moon")
local conf = ...
print_r(conf) --打印上面传递的服务配置
Clone this wiki locally