# dcgos **Repository Path**: azw/dcgos ## Basic Information - **Project Name**: dcgos - **Description**: 分布式集群游戏对象服务器 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2025-12-27 - **Last Updated**: 2026-04-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # dcgos dcgos分布式集群游戏对象服务器(distributed cluster game object server) ## 架构总图 ![架构总图](doc/%E6%9E%B6%E6%9E%84%E6%80%BB%E5%9B%BE.png) ## logic微服务架构 ![logic微服务架构](doc/logic%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%9E%B6%E6%9E%84.png) ## 设计目标 支持动态扩展 用户无感知重启/更新 模块化组件化 简化后端开发快速交付游戏功能 ## 基础库实现cft cft: core framework template cftc: 基础函数/实用功能(纯函数无全局数据) cftf: 开发框架 template: 项目模板 ## 依赖的中间件 openresty: 网络接入层,负载均衡 consul: 配置中心服务器发现 rabbitMQ: 消息队列 redis: 缓存层 mysql: 数据库存储层 pm2: 进程管理工具 ## 设计思路 php是分布式集群后端的很好借鉴,但是其基本以http短连接为主游戏交互性比较强不大合适,所以php加上长连接的功能则可以达到传统c++/java游戏后端的效果同时有具备分布式集群架构的优点。 具体实现如下 gate: 维护客户端和server的连接(有状态) logic: 业务的实现,本身不存状态,数据从mysql/redis/gos中存取(无状态) gos: 游戏对象服务器(game object server),游戏登录后会给玩家分配一个gos并且在玩家会话期间gos不会发生变化(只有gos更新的时候才会切换到新的gos,切换是自动的用户无感知),db和redis是纯粹的数据gos则是数据+逻辑,gos是对游戏里固定逻辑的提炼优化,相对稳定不大变化的逻辑可抽象到gos,重启更新的时候相对较少,logic则更新频繁。示例:游戏的背包、任务数据等,逻辑固定后续也不怎么变化是一大块数据用redis存储的话影响性能(介于有状态/无状态之间) service: 抽象概念表示业务,logic是service的集合,logic必须实现一个以上的service,并且serviceId==1的是入口service(类似golang里的main函数),登录逻辑完成后gate里的session标记为已登录,只有已登录的session,gate才可以调用其他的service ### 模块/组件化 借鉴pc电脑的设计,主板:主service(serviceId==1) 模块:其他service,单体:所有的service都写到一个logic里 微服务:把service独立到单独的进程,主service是必要的 ## 关键功能/概念 配置中心: 服务发现 分布式锁: redis实现 grpc: 用于集群通信 全局唯一id: 每个集群里的进程都具备生成唯一id的功能 jwt: 登录离线认证,consul里每隔固定的时间刷新公钥 service接口: ``` type LogicServiceServer interface { Login(context.Context, *LoginRequest) (*LoginReply, error) Logout(context.Context, *LogoutRequest) (*LoginReply, error) Reconnect(context.Context, *ReconnectRequest) (*ReconnectReply, error) ProcessClientMsg(context.Context, *ProcessClientMsgRequest) (*ProcessClientMsgReply, error) mustEmbedUnimplementedLogicServiceServer() } ``` ## 统一数据模型 db和redis公用一套model,提供统一的接口实现curd和自动处理底层分库逻辑,实现增量更新局部获取局部更新,隐藏底层的关键数据字段并提供只读保护 ``` type IDBEntity interface { GetTableName() string GetFieldNames() immutable.ISlice[string] LoadData(map[string]interface{}) error GetDeltaData() immutable.IMap[string, string] GetRawData() immutable.IMap[string, string] GetPrimaryKeyFields() immutable.ISlice[string] GetShardingFields() immutable.ISlice[string] IsDirty() bool } ``` db表和redis在底层的表示本质为map结构,在此基础上抽象如下几种键关系(键是字段的集合数量>=1) sharding_key:由1个或多个字段组合,用于分库/分redis时hash用决定数据落在那个db/redis实例,并且一个DBEntity有且只有一个sharding_key unique_key:由1个或多个字段组合,表的unique_key数量>=1 primary_key:因为要在加载到redis构成唯一,所以primary_key必然是unique_key其中一个,primary_key相当于数据库里的主键或者mangodb里的ObjectId 决定如何定位到数据, 由上面的定义可得,任何一次取数据只要同时取到sharding_key+primary_key,那么数据就可定位,统一的curd即可实现 登录服账号表示例: ``` CREATE TABLE `t_account` ( `idx` bigint NOT NULL AUTO_INCREMENT COMMENT '自增id', `uid` bigint NOT NULL DEFAULT '0', `app_id` int(11) NOT NULL DEFAULT '0', `plat_id` int(11) NOT NULL DEFAULT '0', `plat_openid` varchar(60) COLLATE utf8mb4_bin NOT NULL DEFAULT '', `plat_unionid` varchar(60) COLLATE utf8mb4_bin NOT NULL DEFAULT '', `create_at` bigint NOT NULL DEFAULT '0', `update_at` bigint NOT NULL DEFAULT '0', UNIQUE KEY `uk_uid` (`uid`), UNIQUE KEY `uk_app_id_plat_id_plat_openid` (`app_id`,`plat_id`,`plat_openid`), PRIMARY KEY (`idx`) ) ENGINE=InnoDB AUTO_INCREMENT=10001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; ``` sharding_key: plat_openid unique_key: uk_uid,uk_app_id_plat_id_plat_openid primary_key: uk_app_id_plat_id_plat_openid 用框架里提供的脚本生成后的golang代码 ``` //包含sharding_key,primary_key所有字段,任何一次对db或redis的获取这些字段都需要获取后续才好curd,并且这些自动除了在Create的时候需要写操作,其他的时候都应该只读 type IAccountCommon interface { DBEntityInterface() dbentity.IDBEntity GetUid() int64 GetAppId() int32 GetPlatId() int32 GetPlatOpenid() string } type IAccountGetter interface { IAccountCommon GetPlatUnionid() string GetCreateAt() int64 GetUpdateAt() int64 } type IAccountSetter interface { IAccountGetter SetPlatUnionid(string) SetCreateAt(int64) SetUpdateAt(int64) } type IAccount interface { IAccountSetter } //只有在创建的时才提供对保护字段的写操作 type IAccountCreate interface { IAccount SetUid(int64) SetAppId(int32) SetPlatId(int32) SetPlatOpenid(string) } //Reference ctfgo/cftf/dbentity/dbentity.go type DBEntity struct { tableName string data map[string]string fields immutable.ISlice[string] primaryKeyFields immutable.ISlice[string] shardingFields immutable.ISlice[string] changedFields map[string]string //保存发生了变更的字段,实现增量更新 } type account struct { entity *dbentity.DBEntity } func NewAccountCreate() IAccountCreate { return &account{ entity: dbentity.NewDBEntity("t_account", entity_fields_define.Account, entity_fields_define.AccountPrimaryKey, entity_fields_define.AccountSharding), } } type Facade struct { dbNamePrefix string //分库数前缀 dbSize int32 //分库数量 redisNamePrefix string //分库redis前缀 redisSize int32 //分库redis数量 Account struct { //指定shardingKeys分库查询 DBShardingQuery func(shardingKeys []string, whereSql string, params map[string]interface{}) ([]IAccount, error) //任意分库查询(只要其中一个分库查到数据就返回) DBAnyQuery func(whereSql string, params map[string]interface{}) (IAccount, error) //查询所有分库 DBAllQuery func(whereSql string, params map[string]interface{}) ([]IAccount, error) //创建/更新 DBCreate func(IAccount) *syncdb.ExecResult //更新的时候只需实现IAccountCommon包含必要的接口即可,所有Get函数返回的数据必须实现IAccountCommon DBUpdate func(IAccountCommon) *syncdb.ExecResult RedisCreate func(IAccount) *redis.IntCmd RedisExpireAndCreate func(expiration time.Duration, m IAccount) *redis.IntCmd //更新的时候只需实现IAccountCommon包含必要的接口即可,所有Get函数返回的数据必须实现IAccountCommon RedisUpdate func(IAccountCommon) *redis.IntCmd RedisExpireAndUpdate func(expiration time.Duration, m IAccountCommon) *redis.IntCmd //设置redis过期 RedisExpire func(expiration time.Duration, m IAccountCommon) *redis.IntCmd //Get函数,必须是由包含了sharding_key所有字段的unique_key生成,uk_uid不包含sharding_key所有字段所以不生成Get函数 //unique_key符合生成Get函数的必要条件: unique_key的字段组成包含sharding_key所有字段,这样才可以完成分库的定位,定位到分库后用unique_key就能定位到数据 //加载到redis的key构成: db:${tableName}:${app_id}_${plat_id}_${plat_openid} DBGetByAppIdPlatIdPlatOpenid func(app_id int32, plat_id int32, plat_openid string) (IAccount, error) RedisGetByAppIdPlatIdPlatOpenid func(app_id int32, plat_id int32, plat_openid string) (IAccount, error) RedisExpireAndGetByAppIdPlatIdPlatOpenid func(expiration time.Duration, app_id int32, plat_id int32, plat_openid string) (IAccount, error) } } ``` 为什么不分表: 因为每个表都有sharding_key,所以sharding_key也可以用于mysql分区表