M3game
A GameServer framework built using Golang and GRPC
Install / Use
/learn @Tudongye/M3gameREADME
m3game
一个基于Golang和Grpc的游戏后端框架。
A GameServer framework built using Golang and GRPC
M3Game是一个采用Golang构建游戏后端的尝试,期望能探索出一条Golang游戏后台的开发方案。
框架分为GameLogic,Frame-Runtime,Custom-Plugin三层。Frame-Runtime为框架驱动层,负责消息驱动,服务网格,插件管理等核心驱动工作。Custom-Plugin为自定义插件层,框架层将第三方服务抽象为多种插件接口,插件层根据实际的基础设施来进行实现。GameLogic为游戏逻辑层,用于承载实际的业务逻辑。框架使用protobuf来生成脚手架,可以通过在pb中添加Option的方式将业务层接口自动注入到框架层。
当前外围服务的框架已经基本稳定,下一阶段会做一个简单玩法服务的框架
优势:
1,更加贴近实际业务。
2、自动化的逻辑注入。借助pb的自定义选项,业务逻辑只需要很少的代码,就可以自动的注入到框架层
3、更通用的技术和更低的门槛。M3基于golang主流的protobuf和grpc进行构建,没有繁琐的代码生成工具,上手门槛低。
4、这里有一个很有意思的数据管理模块,只需要在pb中定义好数据和标记,就可以轻松实现自动置脏&批量写回&视图过滤功能。
5、使用Nats替换了Grpc底层的http2传输协议,使Grpc支持广播和消息缓存。

Mutil,Async,Actor-Server: 游戏后台常见的业务模式,分别对应并发,单线程异步,Actor模式
App: 用于承载业务逻辑的服务实体,是服务网格中的独立个体,由“环境ID.区服ID.功能ID.实例ID”唯一标识。一个App可以承载一个或多个Server
Client:RPC客户端,由服务提供方编写,包含一些参数校验,和路由规则
ResourceLoader: 可线上热更新的资源加载器,一般用于GameLogic Config的管理
Runtime: 框架驱动器
Transport: 提供服务之间Req-Rsp式RPC调用能力,采用tcp/GrpcSer实现一对一传输
BroekerSer:提供服务之间单向Ntify式RPC调用能力,采用Broker-plugin实现一对多传输
Mesh:服务网格,内含一组路由规则,以及规则对应的选路逻辑。采用Router-Plugin实现服务发现和服务注册
ResourceMgr: 资源管理器
PluginMgr:插件管理器
Router-Plugin: 路由组件,提供服务注册和服务发现的能力。当前有一个Consul实现
DB-Plugin: 存储组件,提供数据存储能力,当前有内存数据库,redis,mongo实现
Broker-Plugin:消息队列组件,提供针对主题的发布和订阅功能,当前有一个Nats实现
Log-Plugin: 日志组件,当前有一个Zap实现。
Trace-Plugin: 链路追踪组件,当前接入opentelemetry标准。
Metric-Plugin: 监控组件,当前有一个prometheus实现
Shape-Plugin:流量治理组件,当前有一个sentinel实现
Gate-Plugin:服务网关组件,当前有一个grpc-stream实现
Lease-Plugin:租约管理组件,当前有一个etcd实现
Transport-Plugin:Grpc传输层组件,当前有一个http2(原生) 和 一个Nats的实现。
GamePlay: 一组走状态同步的大地图Gameplay框架,当前有一个单机版的World实现
集群化部署架构

M3内部依赖

感谢GPT的CR
在GPT的帮助下,对runtime做了一轮优化

HelloWorld
以example/simpleapp为例
Step1、定义服务 proto,生成pb文件
// example/proto/simpleapp.proto
syntax = "proto3";
package proto;
import "options.proto"; // 框架文件
option go_package = "proto/pb";
// 定义SimpleSer服务
service SimpleSer {
rpc HelloWorld(HelloWorld.Req) returns (HelloWorld.Rsp); // 定义接口
}
// 定义RPC
message HelloWorld {
option (rpc_option).route_key = "";
message Req {
string Req = 1;
}
message Rsp {
string Rsp = 1;
}
}
Step2、编写App代码
// example/simpleapp/simpleapp.go
package simpleapp
import (
"m3game/example/proto"
"m3game/example/simpleapp/simpleser"
_ "m3game/plugins/transport/tcptrans"
"m3game/runtime"
"m3game/runtime/app"
"m3game/runtime/server"
)
// 创建App实体
func newApp() *SimpleApp {
return &SimpleApp{
App: app.New(proto.SimpleAppFuncID), // 指定App的FuncID
}
}
type SimpleApp struct {
app.App
}
// 健康检测
func (d *SimpleApp) HealthCheck() bool {
return true
}
func Run() error {
// 启动一个 包含了simpleser的SimpleApp
runtime.Run(newApp(), []server.Server{simpleser.New()})
return nil
}
Step3、定义服务实体simpleser
// example/simpleapp/simpleser
package simpleser
import (
"context"
"fmt"
"m3game/example/proto/pb"
"m3game/runtime/rpc"
"m3game/runtime/server/mutil"
"google.golang.org/grpc"
)
func init() {
// 注册RPC信息到框架层
if err := rpc.RegisterRPCSvc(pb.File_simple_proto.Services().Get(0)); err != nil {
panic(fmt.Sprintf("RegisterRPCSvc SimpleSer %s", err.Error()))
}
}
func New() *SimpleSer {
return &SimpleSer{
Server: mutil.New("SimpleSer"), // 以MutilSer为基础构建SimpleSer
}
}
type SimpleSer struct {
*mutil.Server
pb.UnimplementedSimpleSerServer
}
// 实现HelloWorld接口
func (d *SimpleSer) HelloWorld(ctx context.Context, in *pb.HelloWorld_Req) (*pb.HelloWorld_Rsp, error) {
out := new(pb.HelloWorld_Rsp)
out.Rsp = fmt.Sprintf("HelloWorld , %s", in.Req)
return out, nil
}
// 将SimpleSer注册到grpcser
func (s *SimpleSer) TransportRegister() func(grpc.ServiceRegistrar) error {
return func(t grpc.ServiceRegistrar) error {
pb.RegisterSimpleSerServer(t, s)
return nil
}
}
step4 制作配置文件
[Plugin]
[[Plugin.Trans.trans_tcp]] // 采用http2传输层
Host = "127.0.0.1"
Port = 20051
Step5 编译运行
go build .
./main -idstr example.world1.simple.1 -conf ../../config/simpleapp.toml

TODO
1、重新梳理第三方包依赖
2、GamePlay实现
单实例开发方案(已完成)
RPC驱动
在M3中所有的跨服务调用都依托RPC进行,RPC接口通过pb-grpc生成。M3框架的附加信息都存储在RPC的metadata中。
如下是一个RPC定义的proto。
// 定义SimpleSer服务
service SimpleSer {
rpc HelloWorld(HelloWorld.Req) returns (HelloWorld.Rsp); // 定义接口
}
// 定义RPC
message HelloWorld {
option (rpc_option).route_key = "";
message Req {
string Req = 1;
}
message Rsp {
string Rsp = 1;
}
}
业务层通过编写rpc_option将RPC接口注入框架层,解析相关逻辑参看runtime/rpc。rpc_option定义如下
message M3GRPCOption {
string route_key = 1; // Hash路由时的key字段名
bool ntf = 2; // 是否是单向Nty
bool trace = 3; // 是否开启链路追踪
bool cs = 4; // 是否支持客户端访问
}
M3框架通过rpc注入和泛型编程,大大简化了业务层进行RPC调用时的操作,如下是对hello接口进行"随机选址"的RPCCall调用
func Hello(ctx context.Context, hellostr string, opts ...grpc.CallOption) (string, error) {
var in pb.Hello_Req
in.Req = hellostr
// RPCCallRandom 接受泛型参数in,返回泛型参数out。
// RPCCall通过入参in获取到对应的rpc_option,自动填充选址参数,并对常见RPC异常进行前置处理。
out, err := client.RPCCallRandom(_client, _client.Hello, ctx, &in, opts...)
if err != nil {
return "", err
} else {
return out.Rsp, nil
}
}
RPC Tranport
M3的服务之间的RPC调用采用Grpc框架,Grpc底层采用http2,不支持广播,不支持消息缓存。
M3使用Tranport组件来处理Grpc的传输协议,除了基于原生http2的tcptrans,M3还是实现了一个基于Nats的natstrans,使Grpc支持广播与消息缓存。相关实现参看plugins/transport/natstrans.

三种业务模型
游戏后台服务常见的业务模型有 Mutil 多线程,Async 单线程异步,Actor 模式 三种(暂时没见过更复杂的模型)
Mutil
Mutil 多线程模型,主要用于无状态服务,M3采用原生Grpc服务实现。参考实现 example/mutilapp/mutilser
Async
Async 单线程异步,使用这类模型的服务不允许并发的执行RPC调用。参考实现 example/asyncapp/asyncser
M3在Async服务的RPC驱动链中加入了资源锁。通过资源锁确保同一时间只有一个RPC调用再执行

Actor
Actor模型。使用这类模型的服务将RPC调用和游戏实体绑定,实体内部串行,实体之间并发。参考实现 example/actorapp/actorser
M3为每个Actor分配一个执行Goroutine,并引入ActorRuntime和ActorMgr对Actor进行管理,前者用于管理单个Actor的执行Goroutine,后者用于管理整个Actor池。
M3在Actor服务的RPC调用链中加入了Actor管理逻辑。对于Actor的RPC调用都在Actor自己的Goroutine中执行。
引入Lease-plugin可以保证一个Actor在分布式环境下至多只会在一个App上运行。参看rumtime/server/actor

服务发现与路由
Mesh
Mesh使用Router插件进行服务注册和服务发现,Router插件是M3的必要插件,plugins/router/consul是一个基于Consul的Rotuer实现。
M3使用Grpc的Resolver & Picker方式将Mesh与RPC路由相关联,相关逻辑参看runtime/mesh/resolver.go,balance.go
当前支持 P2P,Random,Hash,BroadCast,Single路由模式
| 路由模式 | 选路参数 | 选路规则 | | ---- | ---- | ---- | | P2P | 目标实例ID | 直接寻路 | | Random | 目标服务ID | 在目标服务中随机 | | Hash | 目标服务ID & 哈希Key | 在目标服务中按哈希key,一致性哈希映射寻路 | | BroadCast | 目标服务ID | 对目标服务所有实例广播 | | Single | 目标服务ID | 对目标服务中ID最小的实例寻路 |
资源管理
M3中的资源指由GameLogic定义,在服务运行过程中需要实时热更新的资源文件。一般用于GameLogic的配置管理。
ResourceMgr使用双缓冲区模型,一主一备,主缓冲区用于资源访问,备缓冲区用于资源更新,每次热更新后主备缓冲区交换。相关逻辑参看resource/resourcemgr.go
M3对于资源的访问需要附带上下文context用于确认是资源访问还是资源更新
M3对于资源文件格式没有要求,只要求资源管理器提供Load接口,example/loader/titlecfgloader.go是一个对于json配置文件的资源加载器样例。

实体存储
M3采用pb来定义游戏实体的DB存储结构。如下是一个简单实体的结构定义。相关实现参看example/actorapp/actor
message ActorDB {
string ActorID = 1 [(dbfield_option) = { flag: "FActorID", primary: true }]; // 主键
string Name = 2 [(dbfield_option) = { flag: "FActorName" }];
int32 Level = 3 [(dbfield_option) = { flag: "FActorLevel" }];
}
enum AcFlag {
FActorMin = 0;
FActorID = 1;
FActorName = 2;
FActorLevel = 3;
}
DB结构注入
M3使用DB插件来对实体数据进行落地,M3根据实体的PB结构生成对应的dbmeta,DB插件不用感知业务数据的具体类型,直接根据Meta就可以对实体数据进行CRUD操作。
DBMeta的生成逻辑参看 db/dbmeta.go
type DBMetaInter interface {
Setter(msg proto.Message, flag int32, data interface{}) // 赋值
Getter(msg proto.Message, flag int32) interface{} // 读取
FlagKind(flag int32) protoreflect.Kind // 获取字段类型
FlagName(flag int32) string // 获取字段类型
KeyFlag() int32 // 主键字段
AllFlags() []int32 // 所有字段名
New() proto.Message
Table() string
}
type DB interface {
plugin.PluginIns
Read(ctx context.Context, meta DBMetaInter, key interface{}, flags ...int32) (proto.Message, error)
Update(ctx context.Context, meta DBMetaInter, key interface{}, obj proto.Message, flags ...int32) error
Create(ctx context.Context, meta DBMetaInter, key interface{}, obj proto.Message) error
Delete(ctx context.Context, meta DBMetaInter, key interface{}) error
ReadMany(ctx context.Context, meta DBMetaInter, filters interface{}, flags ...int32) ([]proto.Message, error)
}
数据管理
数据管理指对游戏实体数据的管理功能,M3的Wraper和Viewer提供了自动置脏 和 视图过滤功能。实现了一套类似重返帝国的属性系统。
https://mp.weixin.qq.com/s/rKpHb9WNkYh7rN_DNqC5xw 天美干货分享:怎么解决大地图SLG的技术痛点?
Wraper
Wraper,对数据的ORM级封装,采用反射&泛型极大的简化了DB操作,同时封装了一套自动化的置脏管理。example/actorapp/actor是一个基于Wraper的实体样例
如下是Wraper定义
type Wraper[TM proto.Message, TF Flag] struct {
meta *WraperMeta[TM, TF] // Meta
key interface{} // 主键值
obj TM // 原始数据
dirtys map[TF]bool // 脏标记
}
func (w *Wraper[TM, TF]) Set(flag TF, value interface{})
func (w *Wraper[TM, TF]) Get(flag TF) interface{}
func (w *Wraper[TM, TF]) Update(db db.DB) error // CRUD操作
func (w *Wraper[TM, TF]) Create(db db.DB) error
func (w *Wraper[TM, TF]) Delete(db db.DB) error
func (w *Wraper[TM, TF]) Read(
