GN 游戏框架,目前主要分为 三种 游戏服务器类型 connector服务类型,APP服务类型,master服务类型 ,另外包含一中间件nats 服务。
-
connector服务
:连接器服务-》可以理解为 客户端连接后端服务器集群的前端网关服务,主要做客户端socket通道管理,消息的路由 以及中转消息包等相关业务逻辑,不涉及具体的业务逻辑开发,目前支持websocket 连接,后续会扩展其他连接。该服务通常会根据业务需求,扩展该服务节点数。 -
APP服务
:游戏主服务-》 游戏业务的主服务,几乎所有的业务逻辑以及开发接口,都需要此对象注册,一个前端或RPC 消息包Ipack的流程处理,中间件,group 等等,也都由APP 对象管理。通常该服务类型,有多个 APP服务,来组成游戏集群,供其他服务调用。而config配置文件的servers->"id":"login-001",来确定该服务的唯一性,serverType,来确定 该APP服务类型,多个相同serverType,组成一组相同功能的游戏服务。其他服务根据路由算法确定 调用其中一个游戏服务。 -
master服务
:master服务 正如其名 其他游戏服务的master节点,主要用来 ping/pong 其他服务节点信息来检测 以及获取其他服务节点自动数据返回,如查看内存活跃用户,等等,但是需要用户自己注册handler 返回自定义数据,该服务 不是必要条件,可以根据需求 判断是否启动该服务。该服务 通常为一个节点 获取所连接子节点的相应API 调用。 -
nats 服务
:nats是一个高性能,易使用,轻量级 golang开发的 中间件服务,GN主要用来做 消息中转服务,而不需要每个服务节点之间 都连接socket。nats默认 不支持 消息落盘服务,这样也会影响性能,用户可以根据 自定义 自己选择是否需要落盘
GN可以根据自己后端API需求,轻松简单实现 该API是 单独协程处理业务逻辑,还是公用一个协程排队处理,如:
APIRouter(router string, newGoRoutine bool, handlerFunc ...HandlerFunc)
RPCRouter(router string, newGoRoutine bool, handlerFunc HandlerFunc)
使用样例:
app.APIRouter("addGroup", true, addGroup)
app.APIRouter("createGroup", true, createGroup)
app.RPCRouter("rpcGetAllGroups", false, rpcGetAllGroups)
-
IPack组件
: APP游戏服务-》 通讯消息包的封装或者路由的上下文的封装,该消息包从收到到最终逻辑处理完毕,的整个流程的上下文。 -
Group组件
: App游戏服务-》group组件 正如其名,可以把他看成一个组,比如一个聊天室,或者房间都可以做一个组,该group主要用来存储 connector 服务的session等信息,该组件支持群发消息,添加用户session,删除session,等相关方法。该group KEY-》group 方式 存储在APP 对象的内存中,重启会丢失。 -
GNMiddleWare组件
:APP游戏服-》中间件,包含Before(IPack),After(IPack) 两个方法,主要是用来全局,处理Ipack消息包的前置拦截或者后置的消息的处理 操作。 -
config组件
: APP游戏服务/connector服务/master服务-》游戏集群服务的配置文件,nats 配置文件,connector,APP,等相关服务器的配置 ,服务启动需要设置路径,并根据config模块 启动各个服务 -
Viper组件
: APP游戏服务-》业务逻辑相关 配置文件,该github-》 viper 支持json,yaml等格式的配置文件,viper 配置可以在APP服务启动前设置,比如: app.AddConfigFile("test", "../../config/", "yaml")。 -
Session组件
:APP游戏服务-》session 主要为后端业务逻辑服务,用来绑定前端connector服务的连接ID和 用户逻辑ID绑定而出现的。session:包含 1)连接服的websocket 连接ID,2)用户BindID,3)所在前端服务器的唯一serverID。该session 可用group组件 统一保存在内存,并维护。用户断线重连 该session和老session可能会重复,需要用户自己处理 -
glog Log组件
: APP游戏服务/connector服务/master服务-》 该log 模块 是根据 golang 官方log包,修改以及包装而来,log模块相关配置可以直接在 config配置文件配置,如果不配置,默认为 终端重开输出,不会输出为文件,该log模块 默认一个协程 处理log 的写入。避免因为写入log影响其他模块的性能。 -
CMDHandler方法
: APP游戏服务/connector服务-》该方法主要是用来注册cmdAPI func (a *App) CMDHandler(cmd string, handler HandlerFunc) 主要 用来 接收master 服务的cmd 命令,然后根据需求,返回 master方法,该方法master 对应的方法:SendCMD(cmd,nodeId,string data []byte),cmd为注册的唯一 API名字,需要唯一,master的SendCMD为协程阻塞方法。 -
ILinker 连接器组件
:APP游戏服务/connector服务/master服务-》ILinker 连接器,为 各个 服务节点 连接nats中间件的 封装,主要用发送和接收消息包。 该 ILinker 接口实现为 natsLinker,后续会有其他 连接器如GRPC Linker -
WSConnection 客户端连接器组件
:connector服务-》 客户端连接 服务器集群connector 服务的 ,封装,主要用来接收,和发送 消息包到 客户端! 该实现 为github.com/gorilla/websocket 的封装 -
GnExceptionDetect 异常处理组件
:APP游戏服务/connector服务/master服务-》该组件 用来 注册 各个服务异常的回调的函数注册,样例如下:
// exception handler
connector.AddExceptionHandler(func(exception *gnError.GnException) {
// close handler push msg
if exception.Exception == gnError.WS_CLOSED && len(exception.BindId) > 0 && len(exception.Id) > 0 {
handlerName := "wsclose"
serverAddress := connector.GetServerIdByRouter(handlerName, exception.BindId, exception.Id,
config.GetServerByType("login"))
connector.SendPack(serverAddress, handlerName, exception.BindId, exception.Id, nil)
}
})
所有exception异常 请在gnError包查看
目前GN master 模块 并没有 增加 启动,重启以及管理的 子节点的功能,而是简单的添加了 探知各个子节点,ping/pong心跳机制,以及master RPC 远程调用 其他子节点的注册函数,返回相应的 信息,以供开发者 获取 子节点对应的 子服务的信息。 例子如下:
// master 实例代码 master 调用 login-001 服务商的 online 方法
// routine master 不要阻塞主协程
go func() {
for {
time.Sleep(3 * time.Second)
req := message.CmdOnlineReq{
Admin: "test",
Msg: "test test test",
}
// 参数说明 cmd: API名字 ,需要唯一性, 明确指出 RPC 调用的APIname
// nodeId :服务唯一ID,具体参考 配置文件
// obj : RPC 远程调用的参数
results, err := master.SendCMDJson("online", "login-001", req)
if err != nil {
logger.Infof("master.SendCMD online error %v \n ", err)
return
}
if len(results) > 0 {
res := &message.CmdOnlineRes{}
jsonI.Unmarshal(results, res)
logger.Infof("master.SendCMD -- online results %v ", res)
}
}
}()
// 节点 login-001 实现 online 方法 实例
// master cmd command
app.CMDHandler("online", func(pack gn.IPack) {
req := &message.CmdOnlineReq{}
logger.Infof("online Cmd-- %v \n", string(pack.GetData()))
if err = jsonI.Unmarshal(pack.GetData(), req); err == nil {
logger.Infof(" online req %v \n ", req)
if req.Admin == "test" {
response := &message.CmdOnlineRes{
ServerId: "login-001",
Msg: "onLine test test",
}
logger.Infof(" online response %v \n ", response)
pack.ResultJson(response)
}
}
})
//1 app 创建 例子
config, err := config.NewConfig("../../config/development.json")
if err != nil {
logger.Infof("config error\n ", err)
return
}
app, err := gn.DefaultApp(config) // 默认创建APP,项目
if err != nil {
logger.Infof("new APP error %v \n", err)
return
}
err = app.Run()
if err != nil {
logger.Infof("loginApp run error %v \n", err)
return
}
defer app.Done()
// config 配置json 会在后续说明 或者参考 chat项目
//2 注册API 例子
func InitAPIRouter(app gn.IApp) {
app.APIRouter("login", true, Login)
app.APIRouter("logout", true, Logout)
app.APIRouter("chat", false, Chat)
app.APIRouter("wsclose", true, WsClosedHandler)
}
func InitRPCRouter(app gn.IApp) {
app.RPCRouter("rpcGetAllUsers", false, rpcGetAllUsers)
app.RPCRouter("notifyCreateGroup", false, notifyCreateGroup)
}
// 根据自己的 需求 是否单独分开文件 参数简介 :API 名字,是否开启新协程,调用此方法,函数
// 3 添加 逻辑配置文件 样例
// 具体viper 使用可以参考 "github.com/spf13/viper" 开源项目
app.AddConfigFile("test", "../../config/", "yaml")
//4 添加 中间件 文件 样例
app.UseMiddleWare(&middlerware.PackTimer{})
// 中间件样例 接口
type GNMiddleWare interface {
Before(IPack)
After(IPack)
}
// 实现样例
type PackTimer struct {
}
func (t *PackTimer) Before(pack gn.IPack) {
reqId := rand.Intn(1 << 10)
nowTime := time.Now()
pack.SetContextValue("reqId", reqId)
pack.SetContextValue("inTime", nowTime)
logger.Infof("Before reqId: %d time %v ", reqId, nowTime)
}
func (t *PackTimer) After(pack gn.IPack) {
reqId := pack.GetContextValue("reqId").(int)
nowTime := pack.GetContextValue("inTime").(time.Time)
logger.Infof("After reqId: %d diff time %v ", reqId, time.Now().Sub(nowTime))
}
//5 API 注册 接口样例 & RPC接口 样例
// 聊天
// API
app.APIRouter("chat", false, Chat)
func Chat(pack gn.IPack) {
logger.Infof("loginApp Chat pack data %v \n", string(pack.GetData()))
//unmarshal json
reqData := &message.LoginReq{}
// request data
if len(pack.GetData()) > 0 {
err := jsonI.Unmarshal(pack.GetData(), reqData)
if err != nil {
pack.ExceptionAbortJson("101", "解析前端数据失败 JSON ")
return
}
}
// logic
// response to connector
app := pack.GetAPP()
if len(reqData.Bridge) > 0 {
group, ok := app.GetGroup("userSession")
if ok && group != nil {
for _, uid := range reqData.Bridge {
s, ok := group.GetSession(uid)
if ok && s != nil {
respon := &message.ChatRes{
Router: "chat",
Date: time.Now().Format("2006-01-02 15:04:05"),
Msg: reqData.Msg,
Nickname: reqData.Nickname,
UID: reqData.UID,
Bridge: reqData.Bridge,
GroupID: reqData.GroupID,
Status: 1,
}
// push other user msg
app.PushJsonMsg(s, respon)
}
}
}
}
}
// RPC
app.RPCRouter("rpcGetAllUsers", false, rpcGetAllUsers)
func rpcGetAllUsers(pack gn.IPack) {
// get all groups return
users, ok := pack.GetAPP().GetObjectByTag("userList")
if ok && users != nil {
if mUsers, ok := users.(map[string]*model.UserMode); ok {
userList := make([]interface{}, 0, len(mUsers))
for _, item := range mUsers {
userList = append(userList, item)
}
pack.ResultJson(userList)
pack.SetRPCRespCode(0)
} else {
pack.SetRPCRespCode(102)
}
return
}
pack.SetRPCRespCode(101)
}
目前 Gn Connector 中仅仅支持 websocket 连接,并使用 github.com/gorilla/websocket 该包,connector 包中 主要是 管理前端websocket连接以及消息包的路由算法。和 连接验证等等一些方法
// 样例
config, err := config.NewConfig("../../config/development.json")
if err != nil {
logger.Infof("config error ", err)
return
}
// logger.Infof("config: %v \n ", config)
connector, err := connector.DefaultConnector(config)
if err != nil {
logger.Infof("new DefaultConnect error ", err)
return
}
// exception handler
connector.AddExceptionHandler(func(exception *gnError.GnException) {
// close handler push msg
if exception.Exception == gnError.WS_CLOSED && len(exception.BindId) > 0 && len(exception.Id) > 0 {
handlerName := "wsclose"
serverAddress := connector.GetServerIdByRouter(handlerName, exception.BindId, exception.Id,
config.GetServerByType("login"))
connector.SendPack(serverAddress, handlerName, exception.BindId, exception.Id, nil)
}
})
// set pack route 路由消息 算法
connector.AddRouterRearEndHandler("connector", connectorRoure)
err = connector.Run()
if err != nil {
logger.Infof("connector run error %v \n", err)
return
}
defer connector.Done()
func connectorRoure(cid string, bindId string, serverList []*config.ServersConfig) string {
if len(bindId) > 0 {
index := int(crc32.ChecksumIEEE([]byte(bindId))) % len(serverList)
if serverList[index] != nil {
return serverList[index].ID
}
}
return ""
}
Nats 过多的 官话,我就不说了,请参考官网 https://nats.io/ ,GitHub:https://github.com/nats-io/go-nats
// 配置文件JSON说明 后续会增加 服务 所有ID 均需唯一,来区分唯一性
{
// nats 中间件 配置 ,所有消息包的中转,等等都需要用到 nats
// 暂时是必配选项
"natsconfig":{
"host":"nats://fpp:bar@localhost",
"port":4222
},
// connector 服务器 配置,可以是多个,connector
// 根据NGINX或者其他 负载均衡到 对应 connector
// 均为必配选项
"connector":[
{
"id":"connector001",// ID必须 唯一
"host": "0.0.0.0",
"clientPort": 12007,
"frontend": true, // 是否为前端 服务器
"heartTime":5, // 心跳时间,和客户端 单位为秒
"serverType":"connector" // 服务类型
}
],
// 业务逻辑服务器
// Id 均需 唯一
// serverType 相同 会 认为是一组相同类型的业务服务器
// 时间单位 均为秒
"servers":[
{
"id":"login-001",
"serverType":"login",
"handleTimeOut":10, // handler 执行逻辑 超时时长
"rpcTimeOut":5, // RPC 远程调用 超时 时间
"maxRunRoutineNum":10240 // 服务最大 同时执行业务 ,所开启的协程最大数量,超过最大数量会卡住 channel
},
{
"id":"chat-001",
"serverType":"chat",
"handleTimeOut":10,
"rpcTimeOut":5,
"maxRunRoutineNum":10240
}
],
// master 服务 非必配选择,
// 如果不启动master 服务,可以不配置该 选项
"master":{
"id":"master",
"nodeHeartBeart":10 // 服务子节点 失联 心跳 时间 10秒
},
// log日志文件 配置
"log":{
"encoding": "utf-8", // 编码
"level":"All", // 日志级别 ,
"maxLogSize": 10485760,// 日志文件 大小 超过该大小会 再次分割文件
"numBackups":10 // 最大备份文件数量,超过 文件数,会重新回滚 覆盖
}
}
git clone https://github.com/wmyi/gnchatdemo
5):打开浏览器,输入 http://localhost:8080/#/ 如果弹出 让你输入昵称的 输入框,就证明启动OK
// serverId 是用来确认服务唯一性,必须唯一性,和配置文件里面的connector ID 必须一样 -mode debug 代表 log 输出到控住台,不输入该参数,默认写到配置文件所配置的 logs文件夹 启动成功如下
其他服务 类似connector 逐个启动就OK,切记master 不需要 输入serverId 因为默认 集群就一个 master 所以省略了,直接 go run chatMaster.go -mode debug 即可
输入 npm run dev 一切顺利 会提示 lisenter http://localhost:8080/
在浏览器 输入 http://localhost:8080/ 弹出 输入昵称 输入框。
具体详情请参考 chatdemo 的readme.md #### gnchatdemo