Skip to content
Bruce edited this page Feb 29, 2024 · 29 revisions

1. moon介绍

moon是一个使用Actor模型实现的轻量级在线游戏服务器框架,遵循Keep it simple, stupid原则, 使用少量的核心代码实现了Actor的调度和LuaAPI的封装。一个线程可以拥有1-N个Lua Service,它们使用消息队列进行通信。对于游戏服务器开发,moon提供了许多有用的功能:

  • 核心代码量小,易于学习

  • 跨平台(Windows,Linux,MacOS)

  • 多种网络通信协议支持

    • TCP
    • UDP/KCP
    • Websockets
    • HTTP
  • 基于Lua协程的异步

    • 协程-socket
    • 定时器
    • 进程内服务之间通信
    • 进程间(集群)通信
    • Redis/Mysql/Pgsql/Mongodb 异步客户端驱动程序
    • 高性能和优化的Lua Json
    • Lua protobuf 库
    • Lua 文件系统
    • Lua RecastNavigation 寻路库
    • Lua zset 用于排行榜的库

image

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

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

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

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

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

游戏服务器面临的主要问题包括进程间通信和逻辑模块之间的通信。在进程层面,需要处理与客户端、服务器集群、数据库和平台之间的远程过程调用。在逻辑层面,需要划分模块并确保它们之间的通信便利且高效,以利用多核CPU。moon通过抽象出三大基础设施解决了这个问题:

  • Node(节点) 表示一个moon进程, 它们通过cluster.send cluster.call进行通信
  • Service(服务) Actor的载体,是Node的子集, 它们通过moon.send moon.call进行通信
  • Module(模块) 表示服务中某个具体的逻辑模块,通常用一个Lua脚本文件表示

这三大设施也是灵活搭建不同类型游戏的基础。这里有一个DEMO可以作为参考,你也可以基于它构建适合自己游戏类型的服务器。

2. 开发环境搭建

2.1 安装vscode和lua-language-server插件

moon框架层API基本都有代码注解,推荐使用vscode获得代码提示能力

2.2 获取moon源码

    git clone --recursive https://github.com/sniper00/moon.git

moon源码目录

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

2.3 在Ubuntu上搭建moon

  1. 安装支持C++17的GCC编译器, GCC 9.3+版本即可
  2. 编译
    make config=release -j4
    ./moon test/main_test.lua

2.4 在Centos上搭建moon

  1. 安装GCC9.3
    yum install epel-release
    yum install centos-release-scl-rh
    yum install devtoolset-9-gcc devtoolset-9-gcc-c++
    # 若要在启动的时候就生效,可以放置到~/.bashrc之中即可
    source /opt/rh/devtoolset-9/enable
  1. 编译
    make config=release -j4
    ./moon test/main_test.lua

2.5 在MacOS上搭建moon

  1. 更新最新系统
  2. 编译
    brew install premake
    premake5 gmake --cc=clang
    make config=release -j4
    ./moon test/main_test.lua

2.6 在Windows上搭建moon

  1. 安装VS2022 Community, 并更新到最新,确保安装了windows c++开发组件
  2. 双击build.bat编译

3. Node(节点)

每个moon进程都是一个Node, 每个Node通过组合不同功能的服务来组成特定类型的进程(游戏逻辑服, 跨服战场),不同的Node组成游戏服务器集群。使用如下命令就创建一个Node:

    moon script.lua [args...]

script.lua被称为启动脚本,同时它也是初始化第一个服务的脚本。第一个服务被称为bootstrap服务,它有一些特殊性,通常用来创建和管理其它唯一服务, 和控制节点进程的退出流程,将会在服务部分详细介绍。注意: moon启动后会把工作目录切换到script.lua所在的路径

3.1 Node进程创建

启动脚本可以分为默认启动带选项启动, 为了先不引入过度复杂度, 这里暂时介绍默认启动的方式,带选项启动会在节点间通信部分详细介绍。默认启动方式创建一个节点, 把下面代码保存成文件node.lua, 放在moon可执行文件同级目录, 使用moon node.lua a b c运行.

    local moon = require("moon")
    local arg = moon.args()
    moon.async(function ()
        moon.sleep(100)
        print("hello world: ", table.concat(arg, "  "))
        moon.quit()
    end)

输出

2023-07-23 17:40:04.922 | :01000001 | INFO  | [WORKER 1] new service [bootstrap]
2023-07-23 17:40:04.922 | 42432     | INFO  | WORKER-20 START
2023-07-23 17:40:05.025 | :01000001 | INFO  | hello world:      a  b  c (node.lua:5)
2023-07-23 17:40:05.025 | :01000001 | INFO  | [WORKER 1] destroy service [bootstrap]

3.2 Node进程退出

主要API

    --- 注册进程退出信号处理函数, 需要在处理函数中主动调用`moon.quit`, 否则服务不会退出。
    --- 可以开启新的协程执行异步逻辑: 如服务器安全关闭流程, 等待服务按指定顺序关闭, 保存数据等。
    --- **对于唯一服务一般需要注册此函数来处理退出流程,或者使用`moon.kill`强制关闭**
    ---@param callback fun()
    function moon.shutdown(callback)
        cb_shutdown = callback
    end
    ---触发进程退出流程
    moon.exit(exit_code)
  • 如果exit_code<0, Node进程会直接退出,不会触发moon.shutdown回调。
    local moon = require("moon")
    print("hello world")

    moon.exit(-1)

    moon.shutdown(function ()
        print("shutdown") ---不会打印这一行
        moon.quit() --退出
    end)
  • 如果exit_code>=0, 会给Node进程内所有服务发送 shutdown 消息, 等没有服务存活时, 进程才会退出。可以注册moon.shutdown回调自定义退出逻辑。唯一服务 moon.shutdown回调的默认行为是空,所以需要主动调用moon.quit()来退出, 通常是在bootstrap服务中控制唯一服务的退出顺序, 对于大部分游戏服务器,唯一服务的退出顺序是很重要的。 对于普通服务 moon.shutdown回调的默认行为是调用moon.quit(), 如果要自己管理普通服务的退出,可以覆盖掉moon.shutdown的默认行为。
    local moon = require("moon")
    print("hello world")

    moon.exit(0)

    moon.shutdown(function ()
        print("shutdown") ---会打印这一行
        moon.quit() --退出
    end)
  • bootstrap服务初始化失败时会自动调用moon.exit(-1)
  • main函数会返回对应的exit_code退出码, 方便进程管理工具判断是否启动成功

4. 服务

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

---@class service_conf
---@field name string 表示服务的名称.
---@field file string 表示服务的启动脚本文件路径. 注意此路径是相对于工作目录的, 不会受到luapath影响。
---@field unique? boolean 表示服务是否为唯一服务, 是一个可选的布尔型变量, 默认值为 false。如果设置为 true, 则可以使用 `moon.queryservice(name)` 函数查询服务的 ID
---@field threadid? integer 表示服务运行的工作线程ID [1-N], 是一个可选的整数型变量,默认值为 0。如果设置为非零值, 则会在指定的工作线程中创建服务, 并且把改线程设置为非共享。否则服务会被添加到当前具有最少服务数的工作线程中。

--- 创建一个服务
---@async
---@param config service_conf @创建服务的配置, 除了基本配置, 也可以用来传递额外的参数到新创建的服务中。
---@return integer @ 返回创建的服务ID, 如果ID0则表示服务创建失败。
moon.new_service(config)

获取当前服务的ID

    moon.id  

获取当前服务的name

    moon.name

根据名字查询唯一服务的ID

    local id = moon.queryservice("unique_service_name")

服务主动退出

    moon.quit()

强制某个服务退出

    moon.kill(id)

4.1 bootstrap服务

Actor模型中一切皆服务,bootstrap服务是Node启动时自动创建第一个服务,它有一些特点:

  • bootstrap服务会在threadid=1的线程中创建(可以用来监控当前Node的状态,此时指定其它独占线的程服务时,尽量不要指定1号线程)
  • bootstrap服务不是唯一服务, 方便编写工具脚本时自动退出进程
  • 通常用来创建和管理其它唯一服务, 和控制Node进程的退出流程。
  • 如果bootstrap服务退出了(初始化失败或者调用moon.quit()主动退出),则Node进程也会退出。
  • 作为当前Node最后退出的服务, 可以初始化一些其它服务只读的状态,如加载 lua-protobuf 协议文件

4.2 创建一个简单服务

test.lua

local moon = require("moon")
print("hello world")

moon.quit() --退出

运行./moon test.lua

2023-06-08 16:46:22.970 | 11336     | INFO  | INIT with 4 workers.
2023-06-08 16:46:22.971 | 42660     | INFO  | WORKER-1 START
2023-06-08 16:46:22.972 | 39104     | INFO  | WORKER-2 START
2023-06-08 16:46:22.972 | 27764     | INFO  | WORKER-3 START
2023-06-08 16:46:22.972 | 11500     | INFO  | WORKER-4 START
2023-06-08 16:46:22.979 | :01000001 | INFO  | [WORKER 1] new service [bootstrap]
2023-06-08 16:46:22.984 | :01000001 | INFO  | hello world       (test.lua:2)
2023-06-08 16:46:22.985 | :01000001 | INFO  | [WORKER 1] destroy service [bootstrap]
2023-06-08 16:46:22.996 | 11500     | INFO  | WORKER-4 STOP
2023-06-08 16:46:22.996 | 27764     | INFO  | WORKER-3 STOP
2023-06-08 16:46:22.996 | 39104     | INFO  | WORKER-2 STOP
2023-06-08 16:46:22.996 | 42660     | INFO  | WORKER-1 STOP
2023-06-08 16:46:22.996 | 11336     | INFO  | STOP

4.3 唯一服务

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

  • 长时间运行,直到进程关闭
  • 经常被其它服务访问
  • 是当前进程节点的重要组件,缺少它们就不能正常运行
  • 它们之间有一定的依赖规则,需要制定启动和关闭顺序

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

  • 唯一服务通常非常重要,如果启动失败,服务器就不应该继续运行。
  • 名字唯一, 可以使用moon.queryservice(name)查询到服务ID, 方便被其他服务访问。
  • 使用者需要控制唯一服务的退出,方便在进程正常关闭时,制定多个唯一服务的退出顺序。如数据库连接服务应该最后退出。
  • 可以独占线程提高处理和响应能力。

4.3.1 创建唯一服务

moon.async(function()
    local id = moon.new_service({
        name = "worldmap", -- 注意名字不能和其它唯一服务重复
        file = "worldmap.lua",
        unique = true --唯一服务标识
        --threadid = 2, --可选: 独占id为2的线程
    })
    assert(id>0,"create service failed")
end)

4.3.2 唯一服务退出

  • 优雅退出

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

function CMD.Shutdown()
    moon.quit()
end
  • 自动退出
---进程正常关闭时会调用此函数
moon.shutdown(function()
    --- do something
    moon.quit()
end)
  • 强制退出
--other service
moon.kill(id)

4.3.3 查询唯一服务

local id = moon.queryservice("worldmap")
assert(id>0, "queryservice worldmap failed")
moon.send("lua", id, arg1, arg2, arg3, ...)

4.4 普通服务

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

local roomid = 0
local rooms = {} ---保存房间ID和服务ID的映射

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

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

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

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

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

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

moon.async(function()
    local id = moon.new_service({
        name = "worldmap",
        file = "worldmap.lua",
        unique = true,
        threadid = 2, --独占id为2的线程
        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) --打印上面传递的服务配置

5. 消息设计

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

class message
{
    int8_t type;
    uint32_t sender;
    uint32_t receiver;
    int32_t sessionid;
    std::shared_ptr<buffer> data;
}
  • type 消息类型用于区分不同来源的消息,定制不同的处理逻辑, 内置的消息类型有:
    moon.PTYPE_SYSTEM = 1
    moon.PTYPE_TEXT = 2
    moon.PTYPE_LUA = 3
    moon.PTYPE_ERROR = 4
    moon.PTYPE_DEBUG = 5
    moon.PTYPE_SHUTDOWN = 6
    moon.PTYPE_TIMER = 7
    moon.PTYPE_SOCKET_TCP = 8
    moon.PTYPE_SOCKET_UDP = 9
    moon.PTYPE_SOCKET_WS = 10
    moon.PTYPE_SOCKET_MOON = 11
    moon.PTYPE_INTEGER = 12
    • PTYPE_LUA类型的消息,采用了一种lua对象序列化的方式,常用于服务间通信。
  • sender 消息的发送者

    • 对于PTYPE_LUA类型的消息sender表示发送者的服务ID
    • 对于PTYPE_SOCKET_*类型的消息sender表示socket fd,
    • 对于PTYPE_TIMER类型的消息sender表示timerid,
    • 其它类型的消息, 是为底层框架服务的, 对于使用者基本不需要关系
  • receiver消息的接受者,主要用于服务间通信,表示接收者服务id

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

moon.async(function()
    local res,err = moon.call("lua",other_service_id,"func_add",1,2)
    if not res then
        print(err)
    else
        print(res)--output 3
    end
end)
  • data 根据不同type消息类型编码后的数据。使用共享智能指针是为了发送数据给多个目标时减少拷贝。

5.1 消息类型注册

注意:内置的消息类型都已经自动注册过, 正常情况下无需再次注册

注册消息类型的API:

    moon.register_protocol(t)
  • 示例: 注册PTYPE_LUA类型的消息
moon.register_protocol {
    name = "lua", --关联字符串名字,方便记忆
    PTYPE = moon.PTYPE_LUA,
    pack = moon.pack, --消息编码函数
    unpack = moon.unpack, --消息解码函数
    dispatch = function() --消息处理函数, 逻辑层一般需要调用 moon.dispatch 自定义消息处理函数
        error("PTYPE_LUA dispatch not implemented")
    end
}
  • 注册自定义的消息类型
local PTYPE_CLIENT = 100 --建议大于100(1-255), 避免与内置定义冲突

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

	end
})

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

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

---或者使用moon.raw_dispatch重写已经注册的解析协议
moon.raw_dispatch("lua", function (m)
    ---根据需求获取
    print(moon.decode(msg,"C"))
end)

5.2 定义消息处理函数

---设置指定协议类型的消息处理函数
---@param PTYPE string
---@param fn fun(sender:integer, session:integer, ...)
moon.dispatch(PTYPE, fn)
  • 定义PTYPE_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)

---其他服务
print(moon.call("lua", id, "HELLO"))

5.3 用于发送消息的API

5.3.1 moon.send向服务发送消息

    ---
    ---向指定服务发送消息,消息内容会根据`PTYPE`类型调用对应的`pack`函数。
    ---@param PTYPE string @protocol type. e. "lua"
    ---@param receiver integer @receiver's service id
    moon.send(PTYPE, receiver, ...)

5.3.2 moon.raw_send向服务发送原始消息

    ---向指定服务发送消息, 不会调用对应的`pack`函数。
    ---@param PTYPE string @协议类型
    ---@param receiver integer @接收者服务id
    ---@param data? string|buffer_ptr @消息内容
    ---@param sessionid? integer
    moon.raw_send(PTYPE, receiver, data, sessionid)

5.3.3 moon.call向某个服务发起调用并获得返回值

    --- 向目标服务发送消息, 然后等待返回值, 接收方必须调用`moon.response`返回结果
    ---  - 如果请求成功, 返回值为`moon.response(id, response, params...)`中`params`部分。
    ---  - 如果请求失败, 返回false和错误消息字符串
    ---@async
    ---@param PTYPE string @protocol type
    ---@param receiver integer @receiver service's id
    ---@return ...
    ---@nodiscard
    moon.call(PTYPE, receiver, ...)

5.3.4 moon.response响应moon.call调用

    --- 用来响应moon.call的请求
    ---@param PTYPE string @protocol type
    ---@param receiver integer @receiver service's id
    ---@param sessionid integer
    moon.response(PTYPE, receiver, sessionid, ...)

5.4 消息的发送和接收

5.4.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

5.4.2 使用Lua协程取代回调函数

moon.callmoon.response 组合

  • 发送者:生成一个唯一ID,创建一个协程,并保存ID-协程的映射。 发送消息,把协程ID同时发送给接收者,然后挂起协程。
  • 接收者:收到消息处理完逻辑,把 处理结果 和收到的协程ID 返回给发送者。
  • 发送者:注册一个总的回调函数, 收到结果数据,根据协程ID唤醒协程。
--发送方
--moon.async 实际上是创建一个协程
moon.async(function()
    --像另一个服务发送请求,并使用协程等待调用结果,不需要再注册回调函数
    local result = moon.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

6 服务调度

6.1 Lua协程

---创建一个新的协程并立即开始执行, 带有`async`标记的函数都需要在`moon.async`中调用。如果 fn 函数没有调用 coroutine.yield, 则会同步执行。
--- ```lua
--- local function foo(a, b)
---     print("start foo", a, b)
---     moon.sleep(1000)
---     print("end foo", a, b)
--- end
--- local function bar(a, b)
---     print("start bar", a, b)
---     moon.sleep(500)
---     print("end bar", a, b)
--- end
--- moon.async(foo, 1, 2)
--- moon.async(bar, 3, 4)
--- ```
---
---@param fn fun(...) @需要异步执行的函数
---@param ... any @可选参数,传递给 fn 函数
---@return thread @新创建的协程
moon.async(fn, ...)

6.2 定时器

6.2.1 创建协程风格的定时器moon.sleep

    ---阻塞当前协程至少`mills`毫秒
    ---@async
    ---@param mills integer@ 毫秒
    ---@return boolean @ `moon.wakeup`唤醒的定时器返回`false`, 正常触发的定时器返回`true`
    moon.sleep(mills)

示例

    moon.async(function()
        print("coroutine timer start")
        moon.sleep(1000)
        print("coroutine timer tick 1 seconds")
        moon.sleep(1000)
        print("coroutine timer tick 1 seconds")
        moon.sleep(1000)
        print("coroutine timer tick 1 seconds")
        moon.sleep(1000)
        print("coroutine timer tick 1 seconds")
        moon.sleep(1000)
        print("coroutine timer tick 1 seconds")
        print("coroutine timer end")
    end)
    2023-06-16 20:37:24.824 | :01000001 | INFO  | coroutine timer start     (example_timer.lua:9)
    2023-06-16 20:37:25.830 | :01000001 | INFO  | coroutine timer tick 1 seconds    (example_timer.lua:11)
    2023-06-16 20:37:26.841 | :01000001 | INFO  | coroutine timer tick 1 seconds    (example_timer.lua:13)
    2023-06-16 20:37:27.850 | :01000001 | INFO  | coroutine timer tick 1 seconds    (example_timer.lua:15)
    2023-06-16 20:37:28.857 | :01000001 | INFO  | coroutine timer tick 1 seconds    (example_timer.lua:17)
    2023-06-16 20:37:29.869 | :01000001 | INFO  | coroutine timer tick 1 seconds    (example_timer.lua:19)
    2023-06-16 20:37:29.869 | :01000001 | INFO  | coroutine timer end       (example_timer.lua:20)

6.2.2 使用moon.sleep(0)让出协程执行权

moon.async创建的lua协程占用执行权后,其他的协程需要等待,可以使用moon.sleep(0)让出当前协程执行权

    ---模拟繁重的任务
    local function heavy_task(name)
        local i = 0
        print(name, "begin task")
        while (i < 200000000) do
            i = i + 1
            if i % 50000000 == 0 then
                moon.sleep(0)
                print(name, "task yield")
            end
        end
        print(name, "end task", i)
    end

    moon.async(heavy_task, "task1")
    moon.async(heavy_task, "task2")
    2023-06-16 20:30:02.440 | :01000001 | INFO  | task1     begin task      (example_timer.lua:35)
    2023-06-16 20:30:04.679 | :01000001 | INFO  | task2     begin task      (example_timer.lua:35)
    2023-06-16 20:30:06.910 | :01000001 | INFO  | task1     task yield      (example_timer.lua:40)
    2023-06-16 20:30:09.129 | :01000001 | INFO  | task2     task yield      (example_timer.lua:40)
    2023-06-16 20:30:11.375 | :01000001 | INFO  | task1     task yield      (example_timer.lua:40)
    2023-06-16 20:30:13.593 | :01000001 | INFO  | task2     task yield      (example_timer.lua:40)
    2023-06-16 20:30:15.822 | :01000001 | INFO  | task1     task yield      (example_timer.lua:40)
    2023-06-16 20:30:18.086 | :01000001 | INFO  | task2     task yield      (example_timer.lua:40)
    2023-06-16 20:30:20.349 | :01000001 | INFO  | task1     task yield      (example_timer.lua:40)
    2023-06-16 20:30:20.349 | :01000001 | INFO  | task1     end task        200000000       (example_timer.lua:43)
    2023-06-16 20:30:20.349 | :01000001 | INFO  | task2     task yield      (example_timer.lua:40)
    2023-06-16 20:30:20.349 | :01000001 | INFO  | task2     end task        200000000       (example_timer.lua:43)

6.2.3 使用moon.wakeup唤醒moon.sleep阻塞的协程

    local co = moon.async(function()
        print("wakeup", moon.sleep(10000))
    end)

    moon.async(function()
        print("normal", moon.sleep(1000))
        moon.wakeup(co)
    end)
    2023-06-16 20:36:44.494 | :01000001 | INFO  | normal    true    (example_timer.lua:28)
    2023-06-16 20:36:44.494 | :01000001 | INFO  | wakeup    false   (example_timer.lua:24)

6.2.4 创建回调方式的定时moon.timeout

    ---创建一个定时器,等待的mills毫秒后触发回调函数。如果`mills<=0`则这个函数的行为退化成向消息队列post一条消息,对于需要延迟(delay)执行的操作非常有用。
    ---@param mills integer @等待的毫秒数
    ---@param fn fun() @调用的函数
    ---@return integer @ 返回timerid,可以使用`moon.remove_timer`删除定时器
    moon.timeout(mills, fn)

示例

    local timerid = moon.timeout(1000, function()
        print("hello world")
    end)

6.2.5 删除定时器moon.remove_timer

    local timerid = moon.timeout(1000, function()
        error("must not print")
    end)
    moon.remove_timer(timerid)

6.3 获取时间

6.3.1 获取当前毫秒时间戳moon.now(),可能会有误差

    moon.async(function()
        print("start", moon.now())
        moon.sleep(1000)
        print("1 seconds later", moon.now())
        moon.sleep(2000)
        print("2 seconds later", moon.now())
        print("end", moon.now())
    end)
    2023-06-16 20:48:28.664 | :01000001 | INFO  | start     1686919708655   (example_timer.lua:51)
    2023-06-16 20:48:29.666 | :01000001 | INFO  | 1 seconds later   1686919709666   (example_timer.lua:53)
    2023-06-16 20:48:31.672 | :01000001 | INFO  | 2 seconds later   1686919711672   (example_timer.lua:55)
    2023-06-16 20:48:31.672 | :01000001 | INFO  | end       1686919711672   (example_timer.lua:56)

6.3.2 获取当前秒时间戳moon.time()

    moon.async(function()
        print("start", moon.time())
        moon.sleep(1000)
        print("1 seconds later", moon.time())
        moon.sleep(2000)
        print("2 seconds later", moon.time())
        print("end", moon.time())
    end)
    2023-06-16 20:49:16.434 | :01000001 | INFO  | start     1686919756      (example_timer.lua:51)
    2023-06-16 20:49:17.432 | :01000001 | INFO  | 1 seconds later   1686919757      (example_timer.lua:53)
    2023-06-16 20:49:19.441 | :01000001 | INFO  | 2 seconds later   1686919759      (example_timer.lua:55)
    2023-06-16 20:49:19.441 | :01000001 | INFO  | end       1686919759      (example_timer.lua:56)

6.3.3 使用moon.clock()获取函数执行时间

    moon.async(function()
        local t1 = moon.clock()
        print("start", t1)
        moon.sleep(1235)
        local t2 = moon.clock()
        print("end", t2)
        print("cost", t2 - t1)
    end)
    2023-06-16 20:52:13.310 | :01000001 | INFO  | start     0.039369        (example_timer.lua:61)
    2023-06-16 20:52:14.543 | :01000001 | INFO  | end       1.2728892       (example_timer.lua:64)
    2023-06-16 20:52:14.543 | :01000001 | INFO  | cost      1.2335202       (example_timer.lua:65)

7. 节点间通信

7.1 设置启动选项

使用默认启动选项创建的进程,适合用来编写一些工具脚本,对于游戏服务器进程,通常需要设置一些选项的:

  • thread: 工作线程数 默认是cpu核心数
  • enable_stdout: 是否打印标准输出 默认是true
  • logfile: 日志文件路径 默认不输出日志文件
  • loglevel: 日志等级, 默认 DEBUG. 可选 DEBUG,INFO,WARN,ERROR
  • path: lua模块搜索路径,默认会包含lualibservice路径

想要设置启动选项,必须在启动脚本第一行开始编写如下代码:

---__init__--- 这一行是固定格式, 用于标记启动脚本是否有设置启动选项
if _G["__init__"] then
    local arg = ... --- command line args
    return {
        thread = 16,
        enable_stdout = true,
        logfile = string.format("log/moon-%s-%s.log", arg[1], os.date("%Y-%m-%d-%H-%M-%S")),
        loglevel = 'DEBUG',
        path = table.concat({ --Define lua module search dir, all services use same lua search path
            "./?.lua",
            "./?/init.lua",
            "../lualib/?.lua",
            "../service/?.lua",
            -- Append your lua module search path
        }, ";")
    }
end

这样做的好处是, 启动选项的配置文件和启动脚本在一起,方便维护。moon进程启动时会先检测代码的第一行是否包含---__init__---,如果包含就设置全局表的__init__,这样运行脚本时就拿到了启动选项。然后再次运行启动脚本,使用它创建第一个服务。

7.2 一个创建Node进程模板

---__init__---  初始化进程选项标识
if _G["__init__"] then
    local arg = ... ---这里可以获取命令行参数, string[] 类型
    return {
        thread = 8, ---启动8条线程
        enable_stdout = true,
        logfile = string.format("log/game-%s.log", os.date("%Y-%m-%d-%H-%M-%S")),
        loglevel = "DEBUG", ---默认日志等级
        path = table.concat({ --- 注意: 工作目录会切换到当前脚本所在的路径
            "./?.lua",
            "./?/init.lua",
            "../lualib/?.lua",   -- moon lualib 搜索路径
            "../service/?.lua",  -- moon 自带的服务搜索路径,需要用到redisd服务
            -- Append your lua module search path
        }, ";")
    }
end

--------开始编写第一个服务的逻辑代码----

local moon = require("moon")

local socket = require "moon.socket"

--初始化服务配置
local db_conf= {host = "127.0.0.1", port = 6379, timeout = 1000}

local gate_host = "0.0.0.0"
local gate_port = 8889
local client_timeout = 300

local services = {
    {
        unique = true,
        name = "db",
        file = "../service/redisd.lua",
        threadid = 2, ---独占线程
        poolsize = 5, ---连接池
        opts = db_conf
    },
    {
        unique = true,
        name = "center",
        file = "game/service_center.lua",
        threadid = 3,
    },
}

moon.async(function ()
    for _, one in ipairs(services) do
        local id = moon.new_service( one)
        if 0 == id then
            moon.exit(-1) ---如果唯一服务创建失败,立刻退出进程
            return
        end
    end

    local listenfd = socket.listen(gate_host, gate_port, moon.PTYPE_SOCKET_TCP)
    if 0 == listenfd then
        moon.exit(-1) ---监听端口失败,立刻退出进程
        return
    end

    print("server start", gate_host, gate_port)

    while true do
        local id = moon.new_service( {
            name = "user",
            file = "game/service_user.lua"
        })

        local fd, err = socket.accept(listenfd, id)
        if not fd then
            print("accept",err)
            moon.kill(id)
        else
            moon.send("lua", id,"start", fd, client_timeout)
        end
    end

end)

-- 注册进程退出回调
moon.shutdown(function ()
    moon.async(function ()
        -- 控制其它唯一服务的退出逻辑
        assert(moon.call("lua", moon.queryservice("center"), "shutdown"))
        moon.raw_send("system", moon.queryservice("db"), "wait_save")

        ---wait all service quit
        while true do
            local size = moon.server_stats("service.count")
            if size == 1 then
                break
            end
            moon.sleep(200)
            print("bootstrap wait all service quit, now count:", size)
        end

        moon.quit()
    end)
end)

7.3 节点(进程)间通信

对于搭建分布式游戏服务器,就需要节点间通信。moon对节点间通信做了简单的封装,能满足大部分需求。节点间通信主要用到 cluster 的两个API:

    ---向指定节点的唯一服务发送消息, 无返回值, 如果网络或其它原因造成消息不可达,消息会被丢弃
    cluster.send(receiver_node, receiver_sname, ...)
    ---向指定节点的唯一服务发起RPC调用, 不管成功和失败一定会得到返回值,可以检测返回值判断执行是否成功
    cluster.call(receiver_node, receiver_sname, ...)

并且需要以下条件:

  • Http服务提供通过 NODE ID 获得 节点的 host port
  • 每个节点注册 moon.env("NODE", node_id) 环境变量
  • 每个节点需要创建一个cluster 唯一服务,名且服务名字需要是cluster
  • 需要被访问的节点 需要在创建cluster服务之后,调用它的Listen函数

7.3.1 创建节点配置文件

node.json

[
    {
        "node": 1,
        "host":"127.0.0.1",
        "port":42345
    },
    {
        "node": 2,
        "host":"127.0.0.1",
        "port":42346
    }
]

7.3.2 创建配置中心节点

cluster_etc.lua, 提供配置中心http服务,cluster服务通过它获取其它节点的端口地址。这里用moon开启了一个简单的http-server实现,也可以使用其它方式。

代码链接

7.3.3 创建Node1-消息发送者

node1.lua 调用 node2 bootstrap 函数提供的函数

代码链接

7.3.4 创建Node2-消息接收者

node2.lua

代码链接

7.3.5 运行

按照如下顺序,开启三个终端运行

./moon cluster_etc.lua node.json
./moon node2.lua 2
./moon node1.lua 1

工具

Lua table schema validate

Lua作为脚本语言,数据类型是动态的,并且只有一种数据结构table,可以表示HashTable数组,灵活性比较高。对于业务逻辑,在结构嵌套比较复杂的时候,编写代码时很容易写错,并且不易察觉 ,特别是需要落地的数据,通常需要比较严格地定义数据结构。moon提供了数据合法性检测库lua_schema,通过proto描述文件生成数据校验结构的方式,来校验逻辑中的指定的table结构,用于在开发期间,验证数据,提前发现错误。

syntax = "proto3";

//特殊类型array_XXXX,用于代码提示,描述pb无法表达的结构 {[1]={1,2,3,4,5},[2]={1,2,3,4,5}}
message array_int64
{
	repeated int64 data = 1;
}

message array_int32
{
	repeated int32 data = 1;
}

message ItemData
{
	int32 id = 1;//道具id
	int64 count = 2;//道具数量
	Reward reward = 3;//test nested
}

message UserData
{
	int64           uid              	 	= 1;	//玩家uid
    string          name            	 	= 3;	//玩家名字
    int32          level            	 	= 4;	//玩家等级
    repeated ItemData itemlist	    = 5;   //玩家道具列表
}

message Reward
{
	int32 type = 1;//
	int32 rewardtimes = 2;//
	int32 buyrewardtimes = 3;//
}
local moon = require("moon")
local schema = require("schema")
local json = require("json")

---加载根据proto生成的结构定义
local proto_define = [[
    {
        "array_int64": {
            "data": {
                "container": "array",
                "value_type": "int64",
                "value_index": "1"
            }
        },
        "UserData": {
            "uid": {
                "value_type": "int64",
                "value_index": "2",
                "comment": "玩家uid"
            },
            "name": {
                "value_type": "string",
                "value_index": "3",
                "comment": "玩家名字"
            },
            "level": {
                "value_type": "int32",
                "value_index": "4",
                "comment": "玩家等级"
            },
            "itemlist": {
                "container": "object",
                "key_type": "int32",
                "value_type": "ItemData",
                "value_index": "11",
                "comment": "道具列表"
            },
            "taskrewardgetlist": {
                "container": "object",
                "key_type": "int32",
                "value_type": "array_int64",
                "value_index": "78",
                "comment": "已领取任务奖励id缓存"
            }
        },
        "ItemData": {
            "id": {
                "value_type": "int32",
                "value_index": "1",
                "comment": "道具id"
            },
            "count": {
                "value_type": "int64",
                "value_index": "2",
                "comment": "道具数量"
            },
            "reward": {
                "value_type": "Reward",
                "value_index": "3",
                "comment": "test"
            }
        },
        "Reward": {
            "type": {
                "value_type": "int32",
                "value_index": "1",
                "comment": "怪物类型"
            },
            "rewardtimes": {
                "value_type": "int32",
                "value_index": "2",
                "comment": "已奖励次数"
            },
            "buyrewardtimes": {
                "value_type": "int32",
                "value_index": "3",
                "comment": "钻石购买已奖励次数"
            }
        }
    }
]]

-- load once then shared by other services
schema.load(json.decode(proto_define))

print(pcall(schema.validate, "UserData", {name = 123}))
print(pcall(schema.validate, "UserData", {level = 1.234}))
print(pcall(schema.validate, "UserData", {taskrewardgetlist = {[1] = {1,2,3,false}}}))
print(pcall(schema.validate, "UserData", {itemlist = {a ="123"}}))
print(pcall(schema.validate, "UserData", {itemlist = {1,2,3}}))
print(pcall(schema.validate, "UserData", {itemlist = {[1] = {a= 123}}}))
print(pcall(schema.validate, "UserData", {itemlist = {[1] = {id= 123, count = 123, reward = {type = 123, buyrewardtimes= false, rewardtimes = 100}}}}))

--[[
2023-08-15 20:40:20.787 | :01000001 | INFO  | false     'UserData.name' string expected, got number, value '123'. trace: UserData.name  (example_proto_verify.lua:84)
2023-08-15 20:40:20.787 | :01000001 | INFO  | false     'UserData.level' int32 expected, got number, value '1.234'. trace: UserData.level       (example_proto_verify.lua:85)
2023-08-15 20:40:20.787 | :01000001 | INFO  | false     'array_int64.data.4' int64 expected, got boolean. trace: UserData.taskrewardgetlist.1.data.4    (example_proto_verify.lua:86)
2023-08-15 20:40:20.787 | :01000001 | INFO  | false     'UserData.itemlist.$key' int32 expected, got string. trace: UserData.itemlist.a (example_proto_verify.lua:87)
2023-08-15 20:40:20.788 | :01000001 | INFO  | false     'ItemData' table expected, got number. trace: UserData.itemlist.1       (example_proto_verify.lua:88)
2023-08-15 20:40:20.788 | :01000001 | INFO  | false     Attemp to index undefined field: 'ItemData.a'. trace: UserData.itemlist.1.a     (example_proto_verify.lua:89)
2023-08-15 20:40:20.788 | :01000001 | INFO  | false     'Reward.buyrewardtimes' int32 expected, got boolean, value 'false'. trace: UserData.itemlist.1.reward.buyrewardtimes    (example_proto_verify.lua:90) 
]]
Clone this wiki locally