-
Notifications
You must be signed in to change notification settings - Fork 172
Home
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。
消息的数据
根据不同的type_
会进行对应的编码,逻辑层常用到的是lua编码,是对lua对象的一种序列化方式。
sessionid 用于请求回应模式,方便像写同步代码一样写异步。有时我们发送一条消息,并希望得到消息的处理结果:发送消息时附带一个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)
- 注册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", ...)
- 注册自定义的消息类型
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, 常用于性能相关场合
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中服务间通信需要发送消息,这是个异步过程,通常是注册回调函数来处理逻辑,如:
--发送方
--先注册处理结果回调
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
- 使用Lua协程取代回调函数
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
moon中的服务是线程调度的最小单元。Lua Service用一个LuaVM表示,它们可以独占或者共享线程。不同Lua Service之间不能直接访问,它们只能通过消息通信来交互。服务主要分为唯一服务
和普通服务
。moon中创建服务的API是moon.new_service(stype, config)
。
游戏服务器经常会有一些运行期常驻的模块,如 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 --唯一服务标识
})
assert(id>0,"create service failed")
end)
moon.async(function()
local id = moon.new_service({
name = "worldmap",
file = "worldmap.lua",
unique = true,
threadid = 1, --独占id为1的线程
})
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 = {}
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 = 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) --打印上面传递的服务配置