SkillAgentSearch skills...

M3game

A GameServer framework built using Golang and GRPC

Install / Use

/learn @Tudongye/M3game
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

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支持广播和消息缓存。

未命名文件 (2)

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实现

集群化部署架构

未命名文件 (15)

M3内部依赖

graphviz

感谢GPT的CR

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

企业微信截图_167946848074

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

image

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.

未命名文件 (16)

三种业务模型

游戏后台服务常见的业务模型有 Mutil 多线程,Async 单线程异步,Actor 模式 三种(暂时没见过更复杂的模型)

Mutil

Mutil 多线程模型,主要用于无状态服务,M3采用原生Grpc服务实现。参考实现 example/mutilapp/mutilser

Async

Async 单线程异步,使用这类模型的服务不允许并发的执行RPC调用。参考实现 example/asyncapp/asyncser

M3在Async服务的RPC驱动链中加入了资源锁。通过资源锁确保同一时间只有一个RPC调用再执行

未命名文件 (12)

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

未命名文件 (13)

服务发现与路由

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配置文件的资源加载器样例。

未命名文件 (7)

实体存储

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(
View on GitHub
GitHub Stars111
CategoryDevelopment
Updated3mo ago
Forks14

Languages

Go

Security Score

77/100

Audited on Dec 28, 2025

No findings