-
Notifications
You must be signed in to change notification settings - Fork 172
Home
moon
是一个使用Actor
模型实现的轻量级在线游戏服务器框架,遵循Keep it simple, stupid
原则, 使用少量的核心代码实现了Actor
的调度和Lua
层API
的封装。一个线程可以拥有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 用于排行榜的库
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
使用了调度队列和服务消息队列实现了近似均衡的调度,同一个服务可能会切换不同的线程间调度(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.1 安装vscode和lua-language-server插件
moon框架层API基本都有代码注解,推荐使用vscode获得代码提示能力
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
框架提供的常用服务
- 安装支持C++17的GCC编译器, GCC 9.3+版本即可
- 编译
make config=release -j4
./moon test/main_test.lua
- 安装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
- 编译
make config=release -j4
./moon test/main_test.lua
- 更新最新系统
- 编译
brew install premake
premake5 gmake --cc=clang
make config=release -j4
./moon test/main_test.lua
- 安装
VS2022 Community
, 并更新到最新,确保安装了windows c++开发组件 - 双击
build.bat
编译
每个moon
进程都是一个Node
, 每个Node
通过组合不同功能的服务
来组成特定类型的进程(游戏逻辑服, 跨服战场),不同的Node
组成游戏服务器集群。使用如下命令就创建一个Node
:
moon script.lua [args...]
script.lua
被称为启动脚本,同时它也是初始化第一个服务的脚本。第一个服务被称为bootstrap
服务,它有一些特殊性,通常用来创建和管理其它唯一服务
, 和控制节点进程的退出流程,将会在服务部分详细介绍。注意: moon启动后会把工作目录切换到script.lua
所在的路径。
启动脚本可以分为默认启动
和带选项启动
, 为了先不引入过度复杂度, 这里暂时介绍默认启动
的方式,带选项启动
会在节点间通信
部分详细介绍。默认启动
方式创建一个节点, 把下面代码保存成文件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]
主要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
退出码, 方便进程管理工具判断是否启动成功
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, 如果ID为0则表示服务创建失败。
moon.new_service(config)
获取当前服务的ID
moon.id
获取当前服务的name
moon.name
根据名字查询唯一服务的ID
local id = moon.queryservice("unique_service_name")
服务主动退出
moon.quit()
强制某个服务退出
moon.kill(id)
Actor模型中一切皆服务,bootstrap
服务是Node启动时自动创建第一个服务,它有一些特点:
-
bootstrap
服务会在threadid=1
的线程中创建(可以用来监控当前Node
的状态,此时指定其它独占线的程服务时,尽量不要指定1号线程) -
bootstrap
服务不是唯一服务
, 方便编写工具脚本时自动退出进程 - 通常用来创建和管理其它唯一服务, 和控制
Node
进程的退出流程。 - 如果bootstrap服务退出了(初始化失败或者调用
moon.quit()
主动退出),则Node
进程也会退出。 - 作为当前Node最后退出的服务, 可以初始化一些其它服务只读的状态,如加载
lua-protobuf
协议文件
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
游戏服务器经常会有一些运行期常驻的服务,如 Gate, Center, WorldMap, DB proxy。这些服务拥有共同的特点:
- 长时间运行,直到进程关闭
- 经常被其它服务访问
- 是当前进程节点的重要组件,缺少它们就不能正常运行
- 它们之间有一定的依赖规则,需要制定启动和关闭顺序
这些服务就可以用唯一服务
来编写,moon中的唯一服务有如下特点
- 唯一服务通常非常重要,如果启动失败,服务器就不应该继续运行。
- 名字唯一, 可以使用
moon.queryservice(name)
查询到服务ID, 方便被其他服务访问。 - 使用者需要控制唯一服务的退出,方便在进程正常关闭时,制定多个唯一服务的退出顺序。如数据库连接服务应该最后退出。
- 可以独占线程提高处理和响应能力。
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)
- 优雅退出
注册一个消息处理,进程关闭时,bootstrap 服务向其它唯一服务,按逻辑顺序,发送消息。
function CMD.Shutdown()
moon.quit()
end
- 自动退出
---进程正常关闭时会调用此函数
moon.shutdown(function()
--- do something
moon.quit()
end)
- 强制退出
--other service
moon.kill(id)
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 = {} ---保存房间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)
--然后在管理服务中给它发送消息,优雅的关闭
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) --打印上面传递的服务配置
服务内部采用消息通信,通常是传递一个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
消息类型编码后的数据。使用共享智能指针是为了发送数据给多个目标时减少拷贝。
注意:内置的消息类型都已经自动注册过, 正常情况下无需再次注册
注册消息类型的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)
---设置指定协议类型的消息处理函数
---@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"))
---
---向指定服务发送消息,消息内容会根据`PTYPE`类型调用对应的`pack`函数。
---@param PTYPE string @protocol type. e. "lua"
---@param receiver integer @receiver's service id
moon.send(PTYPE, receiver, ...)
---向指定服务发送消息, 不会调用对应的`pack`函数。
---@param PTYPE string @协议类型
---@param receiver integer @接收者服务id
---@param data? string|buffer_ptr @消息内容
---@param sessionid? integer
moon.raw_send(PTYPE, receiver, data, sessionid)
--- 向目标服务发送消息, 然后等待返回值, 接收方必须调用`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, ...)
--- 用来响应moon.call的请求
---@param PTYPE string @protocol type
---@param receiver integer @receiver service's id
---@param sessionid integer
moon.response(PTYPE, receiver, sessionid, ...)
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
moon.call
和 moon.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
---创建一个新的协程并立即开始执行, 带有`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, ...)
---阻塞当前协程至少`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)
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)
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)
---创建一个定时器,等待的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)
local timerid = moon.timeout(1000, function()
error("must not print")
end)
moon.remove_timer(timerid)
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)
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)
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)
使用默认启动选项创建的进程,适合用来编写一些工具脚本,对于游戏服务器进程,通常需要设置一些选项的:
- thread: 工作线程数 默认是cpu核心数
- enable_stdout: 是否打印标准输出 默认是true
- logfile: 日志文件路径 默认不输出日志文件
- loglevel: 日志等级, 默认
DEBUG
. 可选DEBUG
,INFO
,WARN
,ERROR
- path: lua模块搜索路径,默认会包含
lualib
和service
路径
想要设置启动选项,必须在启动脚本第一行开始编写如下代码:
---__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__
,这样运行脚本时就拿到了启动选项。然后再次运行启动脚本,使用它创建第一个服务。
---__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)
对于搭建分布式游戏服务器,就需要节点间通信。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
函数
node.json
[
{
"node": 1,
"host":"127.0.0.1",
"port":42345
},
{
"node": 2,
"host":"127.0.0.1",
"port":42346
}
]
cluster_etc.lua
, 提供配置中心http服务,cluster
服务通过它获取其它节点的端口地址。这里用moon开启了一个简单的http-server实现,也可以使用其它方式。
node1.lua 调用 node2 bootstrap
函数提供的函数
node2.lua
按照如下顺序,开启三个终端运行
./moon cluster_etc.lua node.json
./moon node2.lua 2
./moon node1.lua 1
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)
]]