├── .editorconfig ├── .gitignore ├── .vscode ├── README.md ├── g.bat └── settings.json ├── LICENSE ├── README.md ├── common ├── config │ ├── framework.go │ ├── framework.toml │ └── server_type_framework.go ├── context.go ├── context │ ├── config.go │ ├── gateway.go │ ├── log.go │ ├── login.go │ ├── node.go │ ├── rand.go │ ├── role2account.go │ ├── tcpserver.go │ ├── time.go │ └── uid.go └── plugin.go ├── doc ├── ISSUE-插件工程独立建库问题.md ├── WIKI-Go国内代理.md ├── WIKI-在Win10中Linux环境搭建.md ├── assets │ ├── login.jpeg │ ├── login.md │ ├── logout.md │ ├── logout.png │ ├── mgrserver.jpeg │ ├── mgrserver.md │ ├── topology.jpeg │ └── topology.md ├── 框架层功能-心跳机制.md ├── 框架层功能-服务发现.md ├── 框架层功能-消息中继.md ├── 框架层功能-登出模块.md ├── 框架层功能-登陆模块.md ├── 框架层功能-网关模块.md ├── 框架层功能-闲置连接处理.md ├── 规范-代码框架.md ├── 规范-协议.md ├── 规范-数据库.md ├── 规范-服务器架构.md ├── 规范-服务类型.md ├── 规范-消息号.md ├── 规范-脚本规范.md ├── 规范-配置文件_框架层.md └── 规范-配置文件_逻辑层.md ├── go.mod ├── go.sum ├── internal ├── app.go ├── components │ ├── config.go │ ├── log.go │ ├── misc │ │ └── context.go │ ├── node │ │ ├── common │ │ │ ├── default_node_interface_impl.go │ │ │ ├── node.go │ │ │ ├── session.go │ │ │ ├── session_mgr.go │ │ │ ├── userdata.go │ │ │ └── uuid_helper.go │ │ ├── gateway │ │ │ ├── gateway.go │ │ │ ├── session.go │ │ │ └── user_mgr.go │ │ ├── login │ │ │ └── login.go │ │ ├── mgr │ │ │ ├── mgr.go │ │ │ └── session.go │ │ └── normal │ │ │ ├── intranet_session.go │ │ │ ├── intranet_session_mgr.go │ │ │ ├── normal.go │ │ │ └── session.go │ ├── plugin.go │ ├── pprof.go │ ├── rand.go │ ├── redis.go │ ├── role2account.go │ ├── signal.go │ ├── tcpserver.go │ ├── time.go │ └── uid.go ├── db │ ├── account.go │ ├── account_servers.go │ ├── g.sh │ ├── init.go │ ├── mgrserver.go │ ├── redis_atomic.go │ ├── redis_def │ │ ├── account.json │ │ ├── mgrserver.json │ │ ├── rolename.json │ │ └── token.json │ ├── rolename.go │ ├── token.go │ ├── token.pb.go │ └── token.proto ├── protocol │ ├── common.pb.go │ ├── common.proto │ ├── g.sh │ ├── gateway.pb.go │ ├── gateway.proto │ ├── init.go │ ├── mgr.pb.go │ └── mgr.proto └── utils │ ├── component.go │ ├── ip_helper.go │ └── ticker.go ├── main.go ├── make.sh └── services ├── internal ├── db │ ├── g.sh │ ├── init.go │ ├── redis_def │ │ ├── rolebase.json │ │ └── rolelist.json │ ├── rolebase.go │ ├── rolelist.go │ ├── rolelist.pb.go │ └── rolelist.proto ├── protocol │ ├── g.sh │ ├── init.go │ ├── lobby.pb.go │ ├── lobby.proto │ ├── lobby_custom.pb.go │ ├── lobby_custom.proto │ ├── lobby_custom_pb2.py │ ├── lobby_pb2.py │ ├── match.pb.go │ ├── match.proto │ └── match_pb2.py └── utility │ └── send_msg.go ├── lobby ├── account.go ├── account_mgr.go ├── lobby.go ├── main.go ├── msg.go ├── msg_chat.go ├── msg_login.go ├── msg_match.go └── role.go ├── match ├── main.go ├── match.go └── msg_lobby.go └── server_type_custom.go /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # all files 5 | [*] 6 | indent_style = tab 7 | indent_size = 4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | bin 14 | /config 15 | /default_plugins 16 | -------------------------------------------------------------------------------- /.vscode/README.md: -------------------------------------------------------------------------------- 1 | ## g.bat 2 | 3 | 如果无法翻墙,则 vscode 内安装 vscode-go 所需的插件会失败 4 | 5 | 则使用 g.bat 来安装 -------------------------------------------------------------------------------- /.vscode/g.bat: -------------------------------------------------------------------------------- 1 | set CUR_DIR=%~dp0 2 | set GOBIN=%GOPATH%\bin 3 | mkdir %GOBIN% 4 | cd /d c: 5 | cd %GOBIN% 6 | go get -u github.com/gpmgo/gopm 7 | gopm bin -u -v github.com/ramya-rao-a/go-outline 8 | gopm bin -u -v github.com/acroca/go-symbols 9 | gopm bin -u -v github.com/mdempsky/gocode 10 | gopm bin -u -v github.com/zmb3/gogetdoc 11 | gopm bin -u -v github.com/fatih/gomodifytags 12 | gopm bin -u -v golang.org/x/tools/cmd/gorename 13 | gopm bin -u -v golang.org/x/tools/cmd/goimports 14 | gopm bin -u -v golang.org/x/tools/cmd/guru 15 | gopm bin -u -v github.com/josharian/impl 16 | gopm bin -u -v github.com/haya14busa/goplay/cmd/goplay 17 | gopm bin -u -v github.com/uudashr/gopkgs/cmd/gopkgs 18 | gopm bin -u -v github.com/davidrjenni/reftools/cmd/fillstruct 19 | gopm bin -u -v github.com/cweill/gotests/gotests 20 | gopm bin -u -v golang.org/x/tools/cmd/gopls 21 | gopm bin -u -v github.com/sqs/goreturns 22 | gopm bin -u -v golang.org/x/lint/golint 23 | go get -u -v github.com/alecthomas/gometalinter 24 | gometalinter --install 25 | go get -u -v github.com/rogpeppe/godef 26 | gopm bin -u -v golang.org/x/tools/cmd/godoc 27 | 28 | cd %CUR_DIR% -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "terminal.integrated.shell.windows": "C:\\Windows\\System32\\bash.exe", 3 | "editor.formatOnPaste": true, 4 | "editor.formatOnSave": true, 5 | "go.formatTool": "goimports" 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 fananchong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-xserver 2 | 3 | **go-xserver 是一个 Golang 服务器框架(go-x.v2)** 4 | 5 | 致力于实现 1 个高可用、高易用的 Golang 服务器框架 6 | 7 | 并以插件的方式,来丰富框架内容 8 | 9 | ## 编译 10 | 11 | - [安装 golang 1.12+](https://golang.google.cn/dl/) 12 | - [安装 docker](https://docs.docker.com/install/linux/docker-ce/centos/) 13 | - 编译执行以下语句即可: 14 | 15 | ```shell 16 | ./make.sh 17 | ``` 18 | 19 | ## 运行 20 | 21 | - 安装 Redis ,并修改 config/config.toml 相关配置 22 | 23 | - All In One 例子 24 | ```shell 25 | ./make.sh start 26 | ./make.sh stop 27 | ``` 28 | 29 | ## 测试客户端 30 | 31 | - [pyclient](https://github.com/fananchong/go-xclient/tree/master/pyclient) 32 | 33 | 34 | ## 缺省插件 35 | 36 | - [go-xserver-plugins](https://github.com/fananchong/go-xserver-plugins) 37 | - mgr 38 | - login 39 | - gateway 40 | 41 | 42 | ## v0.1 43 | 44 | - 管理服务器 45 | - 登陆服务器 46 | - 网关服务器 47 | - 客户端消息中继 48 | - 服务器组内消息中继 49 | - 大厅服务器 50 | - 获取角色列表(登录大厅服务) 51 | - 创建角色 52 | - 获取角色详细信息(进入游戏) 53 | - 登出游戏 54 | - 角色聊天(世界聊天、私聊) 55 | 56 | 57 | ## v0.2 58 | 59 | - 基于 gRPC-go 改造框架层代码 60 | - 分布式事务框架 61 | - 支持 istio 部署 62 | 63 | 64 | ## WIKI 65 | 66 | - [主体框架](doc/规范-代码框架.md) 67 | - 配置模块 68 | - [框架层配置](doc/规范-配置文件_框架层.md) 69 | - [逻辑层配置](doc/规范-配置文件_逻辑层.md) 70 | - [服务发现](doc/框架层功能-服务发现.md) 71 | - [登陆模块](doc/框架层功能-登陆模块.md) 72 | - [闲置连接处理](doc/框架层功能-闲置连接处理.md) 73 | - [登出模块](doc/框架层功能-登出模块.md) 74 | - [服务器组内互联](doc/规范-服务器架构.md) 75 | 76 | 77 | ## ISSUE 78 | 79 | - [插件工程独立建库问题](doc/ISSUE-插件工程独立建库问题.md) 80 | 81 | ## 将要实现的功能 82 | 83 | - 框架层功能 84 | - 灰度更新 85 | - 服务器健康监测 86 | - 逻辑层功能 87 | - 匹配服务 88 | - 房间服务 89 | - 压测工具 90 | -------------------------------------------------------------------------------- /common/config/framework.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // WARNNING : 请勿修改本文件,本文件由框架层程序来维护 4 | 5 | // FrameworkConfig : 配置类 6 | type FrameworkConfig struct { 7 | Common frameworkConfigCommon // 一些基础参数 8 | Network frameworkConfigNetwork // 网络配置 9 | DbAccount FrameworkConfigRedis // 帐号数据库(Redis) 10 | DbToken FrameworkConfigRedis // Token 数据库(Redis) 11 | DbServer FrameworkConfigRedis // Server 数据库(Redis) 12 | DbMgr FrameworkConfigRedis // Mgr 数据库(Redis) 13 | DbRoleName FrameworkConfigRedis // 角色名-账号数据库(Redis) 14 | Role frameworkConfigRole // 角色相关配置 15 | } 16 | 17 | type frameworkConfigCommon struct { 18 | Version string `default:"0.0.1" desc:"版本号"` 19 | LogDir string `default:"./logs" desc:"Log 路径"` 20 | LogLevel int `default:"1" desc:"Log 等级, 1 infoLog; 2 warningLog; 3 errorLog; 4 fatalLog"` 21 | LogFlushInterval int `default:"1000" desc:"Log 写入到文件的时间间隔,单位:毫秒"` 22 | Debug bool `default:"true" desc:"Debug 版本标志"` 23 | IntranetToken string `default:"6d8f1f3a-739f-47fe-9ed1-ea39276cd10d" desc:"内部服务器验证 TOKEN"` 24 | MsgCmdOffset int `default:"1000" desc:"消息号 = 服务类型 * MsgCmdOffset + 数字"` 25 | Pprof string `default:"" desc:"Http pprof 地址"` 26 | } 27 | 28 | type frameworkConfigNetwork struct { 29 | IPType int `default:"1" desc:"类型: 1 表示使用 IP;类型: 0 表示使用网卡名"` 30 | IPInner string `default:"127.0.0.1" desc:"内网 IP"` 31 | IPOuter string `default:"127.0.0.1" desc:"外网 IP"` 32 | Port []int32 `default:"[7500, 30000]" desc:"第一个端口为对外提供服务的端口;第二个端口为服务器组内提供服务的端口;若有其他继续填充,自定义"` 33 | } 34 | 35 | // FrameworkConfigRedis : redis 配置 36 | type FrameworkConfigRedis struct { 37 | Name string `desc:"Redis 数据库名称"` 38 | Addrs []string `default:"[127.0.0.1:6379]" desc:"Redis 数据库地址"` 39 | Password string `default:"123456" desc:"Redis 数据库密码"` 40 | DBIndex int `default:"1" desc:"Redis DB 索引"` 41 | } 42 | 43 | type frameworkConfigRole struct { 44 | IdleTime int64 `default:"300" desc:"角色闲置该时间间隔后账号、角色对象内存中清除。单位:秒"` 45 | SessionAffinityInterval int64 `default:"300" desc:"账号登出后,分配的服务器资源信息保留时间。单位:秒"` 46 | } 47 | -------------------------------------------------------------------------------- /common/config/framework.toml: -------------------------------------------------------------------------------- 1 | [common] 2 | Version = "0.0.1" 3 | LogDir = "./logs" 4 | LogLevel = 1 5 | Debug = true 6 | 7 | [network] 8 | IPType = 1 9 | IPInner = "127.0.0.1" 10 | IPOuter = "127.0.0.1" 11 | Port = [7500, 30000] 12 | 13 | [dbaccount] 14 | Name = "account" 15 | Addrs = ["127.0.0.1:6379"] 16 | Password = "123456" 17 | DBIndex = 1 18 | 19 | [dbtoken] 20 | Name = "token" 21 | Addrs = ["127.0.0.1:6379"] 22 | Password = "123456" 23 | DBIndex = 1 24 | 25 | [dbserver] 26 | Name = "server" 27 | Addrs = ["127.0.0.1:6379"] 28 | Password = "123456" 29 | DBIndex = 1 30 | 31 | [dbmgr] 32 | Name = "mgr" 33 | Addrs = ["127.0.0.1:6379"] 34 | Password = "123456" 35 | DBIndex = 1 36 | 37 | [dbrolename] 38 | Name = "rolename" 39 | Addrs = ["127.0.0.1:6379"] 40 | Password = "123456" 41 | DBIndex = 1 -------------------------------------------------------------------------------- /common/config/server_type_framework.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // WARNNING : 请勿修改本文件,本文件由框架层程序来维护 4 | 5 | // NodeType : 节点类型 6 | type NodeType int 7 | 8 | const ( 9 | 10 | // Client : 类型 0 ,客户端(虚拟) 11 | Client NodeType = 0 12 | 13 | // Mgr : 类型 1 ,管理服 14 | Mgr NodeType = 1 15 | 16 | // Login : 类型 2 ,登录服 17 | Login NodeType = 2 18 | 19 | // Gateway : 类型 3 ,网关服 20 | Gateway NodeType = 3 21 | 22 | // Unknow : 未知 23 | Unknow NodeType = 9999 24 | ) 25 | -------------------------------------------------------------------------------- /common/context.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | gocontext "context" 5 | 6 | "github.com/fananchong/go-xserver/common/context" 7 | ) 8 | 9 | // Context : 应用程序上下文 10 | type Context struct { 11 | gocontext.Context // golang context ,可以用于控制并发,传递全局变量等 12 | context.ITime // 时间对象 13 | context.IRand // 随机数生成器 14 | context.IConfig // 配置对象 15 | context.ILogger // 日志对象 16 | context.INode // 提供消息中继等功能 17 | context.IRole2Account // 提供`根据角色名查找账号`的功能;角色名重名检查也可以用该接口 18 | context.IUID // UID 生成器 19 | context.ITCPServer // 提供对外的 TCP 服务。 注册该字段相应接口,才会开启 20 | context.ILogin // 节点类型为 Login,才会开启 21 | context.IGateway // 节点类型为 Gateway ,才会开启 22 | } 23 | -------------------------------------------------------------------------------- /common/context/config.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import "github.com/fananchong/go-xserver/common/config" 4 | 5 | // IConfig : 配置类接口 6 | type IConfig interface { 7 | LoadConfig(cfgfile string, cfgobj interface{}) bool // 逻辑层可以用该接口加载配置文件到 cfgobj 结构体对象, cfgobj 为指针类型 8 | Config() *config.FrameworkConfig // 获取框架层配置 9 | PrintUsage() // 打印命令行参数 10 | } 11 | -------------------------------------------------------------------------------- /common/context/gateway.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | // IGateway 接口暴露框架层网关模块的使用方法 4 | // 完整的网关,由框架层登陆模块、逻辑层交互模块共同完成 5 | 6 | // 网关模块框架层负责的工作: 7 | // 1. 提供服务器组内节点互连、接入验证 8 | // 2. 提供 Client(s) <-> Gateway <-> Node 路径的消息转发 9 | // 3. 提供 Node <-> Gateway <-> Other Node(s) 路径的消息转发 10 | // 4. 框架层会接管几乎所有的网关的功能(网关会很纯粹,主要就是消息转发) 11 | 12 | // 网关模块逻辑层负责的工作: 13 | // 1. 自定义客户端交互协议(如使用 TCP 、 HTTP ; 如使用 proto 消息、 struct 消息 等等) 14 | // 2. 自定义加解密算法 15 | 16 | // 经过网关的消息规则,请参见:[规范-消息号.md](go-xserver/doc/规范-消息号.md) 17 | 18 | // FuncTypeEncode : 数据加密函数声明 19 | type FuncTypeEncode func(data []byte) []byte 20 | 21 | // FuncTypeDecode : 数据解密函数声明 22 | type FuncTypeDecode func(data []byte) []byte 23 | 24 | // FuncTypeSendToClient : 发送客户端数据函数声明 25 | type FuncTypeSendToClient func(account string, cmd uint64, data []byte, flag uint8) bool 26 | 27 | // FuncTypeSendToAllClient : 发送所有客户端数据函数声明 28 | type FuncTypeSendToAllClient func(cmd uint64, data []byte, flag uint8) bool 29 | 30 | // IClientSesion : 客户端会话类 31 | type IClientSesion interface { 32 | Close() 33 | } 34 | 35 | // IGateway : 网关模块接口 36 | type IGateway interface { 37 | VerifyToken(account, token string, clientSession IClientSesion) uint32 // 令牌验证。返回值: 0 成功;1 令牌错误; 2 系统错误 38 | OnRecvFromClient(account string, cmd uint32, data []byte, flag uint8) (done bool) // 可自定义客户端交互协议。 逻辑层代码处理好协议相关事宜后,主动调用该函数,把数据投递给框架层。 done 为 true ,表示框架层接管处理该消息 39 | RegisterSendToClient(f FuncTypeSendToClient) // 可自定义客户端交互协议。 框架层收到其他服务节点来的消息,调用此函数注册的回调,把数据投递给逻辑层。 逻辑层可处理协议相关事宜 40 | RegisterSendToAllClient(f FuncTypeSendToAllClient) // 可自定义客户端交互协议 41 | RegisterEncodeFunc(f FuncTypeEncode) // 可自定义加解密算法 42 | RegisterDecodeFunc(f FuncTypeDecode) // 可自定义加解密算法 43 | } 44 | -------------------------------------------------------------------------------- /common/context/log.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | // ILogger : 日志类接口 4 | type ILogger interface { 5 | Debug(args ...interface{}) 6 | Debugln(args ...interface{}) 7 | Debugf(format string, args ...interface{}) 8 | Print(args ...interface{}) 9 | Println(args ...interface{}) 10 | Printf(format string, args ...interface{}) 11 | Info(args ...interface{}) 12 | Infoln(args ...interface{}) 13 | Infof(format string, args ...interface{}) 14 | Warning(args ...interface{}) 15 | Warningln(args ...interface{}) 16 | Warningf(format string, args ...interface{}) 17 | Error(args ...interface{}) 18 | Errorln(args ...interface{}) 19 | Errorf(format string, args ...interface{}) 20 | Fatal(args ...interface{}) 21 | Fatalln(args ...interface{}) 22 | Fatalf(format string, args ...interface{}) 23 | Flush() 24 | SetLogLevel(level int) 25 | SetLogDir(dir string) 26 | } 27 | -------------------------------------------------------------------------------- /common/context/login.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import "github.com/fananchong/go-xserver/common/config" 4 | 5 | // ILogin 接口暴露框架层登陆模块的使用方法 6 | // 完整的登陆过程,由框架层登陆模块、逻辑层交互模块共同完成 7 | 8 | // 登陆模块框架层负责的工作: 9 | // 1. 提供缺省账号验证 10 | // 2. 提供账号正常登陆 11 | // a. 提供账号正常上下线,不会回档等错误 12 | // b. 提供同账号多端同时登陆,不会异常 13 | // c. 提供账号服务器资源分配 14 | // 3. 提供自定义验证接口 15 | 16 | // 登陆模块逻辑层负责的工作: 17 | // 1. 自定义登陆协议 18 | // 2. 自定义客户端交互流程 19 | // 3. 自定义账号验证过程 20 | // 4. 自定义分配哪些服务器资源给账号 21 | 22 | // go-xserver/services/login : 23 | // 1. 一个缺省的 Login Server 实现例子 24 | // 2. 主要展示逻辑层如何与框架层交互 25 | // 3. 可以参考实现自己的项目要求的 Login Server 26 | 27 | // LoginErrCode : 登陆错误 28 | type LoginErrCode int 29 | 30 | const ( 31 | // LoginSuccess : 登陆成功 32 | LoginSuccess LoginErrCode = iota 33 | 34 | // LoginVerifyFail : 验证错误 35 | LoginVerifyFail 36 | 37 | // LoginSystemError : 系统错误 38 | LoginSystemError 39 | ) 40 | 41 | // FuncTypeAccountVerification : 账号验证函数声明 42 | type FuncTypeAccountVerification func(account, password string, userdata []byte) (errcode LoginErrCode) 43 | 44 | // ILogin : 登陆模块接口 45 | type ILogin interface { 46 | RegisterCustomAccountVerification(f FuncTypeAccountVerification) // 注册自定义账号验证 47 | RegisterAllocationNodeType(types []config.NodeType) // 注册自定义要分配的服务器节点类型 48 | Login(account, password string, defaultMode bool, userdata []byte) (token string, 49 | address []string, port []int32, nodeType []config.NodeType, nodeIDs []NodeID, errcode LoginErrCode) // 登录。 框架层处理登录事宜,并返回 ip list / type list / port list / token 等 50 | } 51 | -------------------------------------------------------------------------------- /common/context/node.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import "github.com/fananchong/go-xserver/common/config" 4 | 5 | // NodeID : 服务节点ID类型 6 | type NodeID uint32 7 | 8 | // FuncTypeOnRelayMsg : 处理中继消息的函数声明。 客户端来的消息 nodeID 为 0; 服务器端间的消息 account 为空 9 | type FuncTypeOnRelayMsg func(source config.NodeType, nodeID NodeID, account string, cmd uint64, data []byte, flag uint8) 10 | 11 | // FuncTypeOnLoseAccount : 处理丢失账号的函数声明 12 | type FuncTypeOnLoseAccount func(account string) 13 | 14 | // INode : 节点类接口(其实现,封装自动接入服务器组、服务发现、服务消息传递等细节) 15 | type INode interface { 16 | EnableMessageRelay(v bool) // 【4】开启消息中继功能。开启该功能的节点,会连接 Gateway 。 C -> Gateway -> Node ; Node1 -> Gateway -> Node2(s) 17 | RegisterFuncOnRelayMsg(f FuncTypeOnRelayMsg) // 【4】注册自定义处理 Gateway 中继过来的消息 18 | SendMsgToClient(account string, cmd uint64, data []byte, flag uint8) bool // 【5】发送消息给客户端,通过 Gateway 中继 19 | BroadcastMsgToClient(cmd uint64, data []byte, flag uint8) bool // 【5】广播消息给客户端,通过 Gateway 中继 20 | SendMsgToServer(t config.NodeType, cmd uint64, data []byte, flag uint8) bool // 【5】发送消息给某类型服务(随机一个) 21 | ReplyMsgToServer(targetID NodeID, cmd uint64, data []byte, flag uint8) bool // 【5】回发消息给请求服务器 22 | BroadcastMsgToServer(t config.NodeType, cmd uint64, data []byte, flag uint8) bool // 【5】广播消息给某类型服务 23 | RegisterFuncOnLoseAccount(f FuncTypeOnLoseAccount) // 【6】注册自定义处理`丢失账号` 24 | } 25 | -------------------------------------------------------------------------------- /common/context/rand.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | // IRand : 随机类接口 4 | type IRand interface { 5 | // Int63n returns, as an int64, a non-negative pseudo-random number in [0,n). 6 | // It panics if n <= 0. 7 | Int63n(n int64) int64 8 | 9 | // Float64 returns, as a float64, a pseudo-random number in [0.0,1.0). 10 | Float64() float64 11 | } 12 | -------------------------------------------------------------------------------- /common/context/role2account.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | // IRole2Account : 提供`根据角色名查找账号`的功能;角色名重名检查也可以用该接口 4 | type IRole2Account interface { 5 | Add(role, account string) // 加入本地缓存 6 | AddAndInsertDB(role, account string) bool // 加入本地缓存并保存数据库 7 | GetAndActive(role string) string // 根据角色名,查找账号。先从本地缓存中找,没有则数据库中找 8 | } 9 | -------------------------------------------------------------------------------- /common/context/tcpserver.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | // 框架层内置的 1 个 TCP 服务 4 | // 对外 TCP 服务, IP 配置对应 --network-ipouter 参数; PORT 配置对应 --network-port 参数的第一个值 5 | // 逻辑层只有调用 ITCPServer.RegisterSessType ,注册 Session 类,框架层才会开启服务 6 | // ITCPServer.RegisterSessType ,注册 Session 类,必须是组合嵌入 gotcp.Session 类 7 | 8 | // ITCPServer : TCP 服务接口 9 | type ITCPServer interface { 10 | RegisterSessType(v interface{}) 11 | } 12 | -------------------------------------------------------------------------------- /common/context/time.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | // ITime : 时间类接口 4 | type ITime interface { 5 | GetTickCount() int64 // 毫秒数 6 | SetDelta(delta int64) // 设置时间差(单位毫秒),用来快进或后退当前时间(调试时间相关功能时,会用到) 7 | } 8 | -------------------------------------------------------------------------------- /common/context/uid.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | // IUID : 唯一ID接口 4 | type IUID interface { 5 | GetUID(key string) (uint64, error) // 根据 KEY , 获取 UID 6 | } 7 | -------------------------------------------------------------------------------- /common/plugin.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // IPlugin : 插件接口 4 | type IPlugin interface { 5 | Start() bool 6 | Close() 7 | } 8 | -------------------------------------------------------------------------------- /doc/ISSUE-插件工程独立建库问题.md: -------------------------------------------------------------------------------- 1 | # 插件工程独立建库问题 2 | 3 | 目前 golang 尚不支持主程序、插件程序分别建库 4 | 5 | 详细请参考关注官方 issue : https://github.com/golang/go/issues/27751 6 | 7 | go-xserver 已经把缺省插件移至 https://github.com/fananchong/go-xserver-plugins.git 8 | 9 | 因此如果分别编译 go-xserver 、 go-xserver-plugins 出来的 go-xserver 程序是无法加载如 login.so 的 10 | 11 | 12 | ## 解决方法 1 (推荐) 13 | 14 | 1. 下载 go 源码 https://github.com/golang/go.git 15 | 2. 切最新稳定版 16 | 3. 注释 [go/src/runtime/plugin.go](https://github.com/golang/go/blob/50bd1c4d4eb4fac8ddeb5f063c099daccfb71b26/src/runtime/plugin.go) 文件中: 17 | ```go 18 | for _, pkghash := range md.pkghashes { 19 | if pkghash.linktimehash != *pkghash.runtimehash { 20 | md.bad = true 21 | return "", nil, "plugin was built with a different version of package " + pkghash.modulename 22 | } 23 | } 24 | ``` 25 | 4. 编译 go 代码 26 | 5. 使用编译出来的 go 程序编译 go-xserver 27 | 28 | 29 | ## 解决方法 2 30 | 31 | 拷贝 [go-xserver-plugins](https://github.com/fananchong/go-xserver-plugins.git) 到 go-xserver 工程中编译 32 | 33 | 目前 [make.sh](make.sh) 、 [main.go](main.go) 中正是干了这件事 34 | 35 | 如果是直接 git clone go-xserver 的,是不需要做额外工作,即可完整编译的 36 | 37 | 38 | ## 解决方法 3 39 | 40 | 等待官方解决 [issue#27751](https://github.com/golang/go/issues/27751) 41 | -------------------------------------------------------------------------------- /doc/WIKI-Go国内代理.md: -------------------------------------------------------------------------------- 1 | ## go get 2 | 3 | gopm : https://github.com/gpmgo/gopm 4 | 5 | - 安装 gopm 6 | 7 | ```sh 8 | go get -u github.com/gpmgo/gopm 9 | ``` 10 | 11 | - 下载包,使用 `gopm get` 代替 `go get` 12 | 13 | ```sh 14 | gopm get -u -v [包地址] 15 | ``` 16 | 17 | ## go module 18 | 19 | goproxy : https://goproxy.io/ 20 | 21 | 在执行诸如 `go build` 等命令前,先执行以下语句: 22 | 23 | - Bash (MAC/Linux) 24 | ```sh 25 | export GOPROXY=https://goproxy.io 26 | ``` 27 | 28 | - PowerShell (Windows) 29 | ```sh 30 | $env:GOPROXY = "https://goproxy.io" 31 | ``` 32 | -------------------------------------------------------------------------------- /doc/WIKI-在Win10中Linux环境搭建.md: -------------------------------------------------------------------------------- 1 | ## 安装 WSL 2 | 3 | - 控制面板->程序和功能->启用或关闭Windows功能->勾选 适用于Linux的Windows子系统 4 | 5 | - 打开应用商城搜索“WSL”,可根据自己需求选择安装一个或多个Linux系统(推荐:Ubuntu 18) 6 | 7 | - Shift + 鼠标右键,点击`在此处打开 Linux Shell(L)` ,即可打开 WSL 8 | 9 | ## 安装 VSCode 10 | 11 | 主要步骤: 12 | 13 | - 安装 VSCode 14 | - 设置代理 15 | - 安装 go 插件 16 | - WSL 嵌入 VSCode 17 | - WSL 内安装 go (参考官网) 18 | - WSL 内设置代理 (参考[编译-翻墙设置](编译-翻墙设置.md)) 19 | - WSL 内安装 docker (参考下节内容) 20 | - WSL 内安裝 gcc ( sudo apt-get install gcc ) 21 | - VSCode 配置默认 TERMINAL 为 WSL 22 | 23 | 详细请参考:https://blog.csdn.net/u013272009/article/details/84971807 24 | 25 | 26 | ## 安装 Docker for Windows 27 | 28 | - 按照 https://www.docker.com/products/docker-desktop 中提示安装之 29 | 30 | - 打开 Docker for Windows - General ,勾选`Expose daemon on tcp://localhost:2375 without TLS` 31 | 32 | - 打开 Docker for Windows - Shared Drives ,勾选所有盘符 33 | 34 | - 打开 WSL ,安装 docker.io 35 | - 请参考 https://blog.csdn.net/u013272009/article/details/81221661 36 | 37 | - 修正挂接目录问题 38 | - 请参考 https://blog.csdn.net/u013272009/article/details/81222689 39 | 40 | - Docker 容器开启失败 41 | - 请参考 https://blog.csdn.net/u013272009/article/details/85002613 42 | -------------------------------------------------------------------------------- /doc/assets/login.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fananchong/go-xserver/96491ade4a78371539e4baed3165a05e7de1aea1/doc/assets/login.jpeg -------------------------------------------------------------------------------- /doc/assets/login.md: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | sequenceDiagram 3 | participant Client 4 | participant LoginServer 5 | participant Redis 6 | participant Gateway 7 | Client->>LoginServer: 1. 账号登陆 8 | Note right of LoginServer: 1. 账号相关验证(略) 9 | Note right of LoginServer: 2. 本地缓存中选取一个 Gateway (以Gateway为例,方便理解;实际上可定制服务列表) 10 | LoginServer->>Redis: 2.1 SETGETX { uid, gatewayid } 键值对 11 | Redis-->>LoginServer: 2.1 返回 gatewayid' 12 | alt gateway' 已失效 13 | LoginServer-->>Redis: DELX { uid, gatewayid' } 键值对 14 | LoginServer-->>LoginServer: goto 2.1 15 | end 16 | LoginServer->>Redis: 3. SET { uid, token } 键值对 17 | LoginServer->>Client: 4. 返回OK, IP/Port/Token 18 | Note right of Gateway: 5. 登陆 Gateway(略) 19 | Gateway->>Redis: 5.1 EXPIRE { uid, gatewayid } 键值对(设置过期1年) 20 | Note right of Gateway: 6. 连接断开事件触发 21 | Gateway->>Redis: 6. EXPIRE { uid, gatewayid } 键值对(设置过期10分钟) 22 | ``` 23 | -------------------------------------------------------------------------------- /doc/assets/logout.md: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | sequenceDiagram 3 | participant Gateway 4 | participant LoginServer 5 | Gateway-->>Gateway: 检查闲置连接 6 | alt 有闲置连接 7 | Gateway-->>Gateway: 本地闲置连接处理 8 | Gateway-->>LobbyServer: 通知账号登出 9 | LobbyServer-->>LobbyServer: 账号登出处理 10 | end 11 | ``` 12 | -------------------------------------------------------------------------------- /doc/assets/logout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fananchong/go-xserver/96491ade4a78371539e4baed3165a05e7de1aea1/doc/assets/logout.png -------------------------------------------------------------------------------- /doc/assets/mgrserver.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fananchong/go-xserver/96491ade4a78371539e4baed3165a05e7de1aea1/doc/assets/mgrserver.jpeg -------------------------------------------------------------------------------- /doc/assets/mgrserver.md: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | sequenceDiagram 3 | participant Server 4 | participant MgrServer 5 | participant Other Servers 6 | participant Redis 7 | MgrServer-->>Redis: 写入自己的 IP/PORT 8 | loop 获取 MgrServer IP/PORT 9 | Server-->>Redis: 获取 10 | Redis-->>Server: 返回 11 | Server-->>Server: 获取 MgrServer IP/PORT,跳出loop 12 | end 13 | Server-->>MgrServer: TCP 连接 MgrServer 14 | MgrServer-->>Server: 连接成功 15 | Server-->>MgrServer: 注册自己 16 | MgrServer-->>Other Servers: 广播新连接事件 17 | MgrServer-->>Server: 发送所有服务信息列表 18 | Server-->>MgrServer: 维持心跳 Ping 19 | MgrServer-->>Server: 维持心跳 Pong 20 | MgrServer-->>MgrServer: TCP 连接断开事件触发 21 | MgrServer-->>Other Servers: 广播连接丢失事件 22 | Server-->>Server: MgrServer 链接断开,goto loop 获取 MgrServer IP/PORT 23 | ``` 24 | -------------------------------------------------------------------------------- /doc/assets/topology.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fananchong/go-xserver/96491ade4a78371539e4baed3165a05e7de1aea1/doc/assets/topology.jpeg -------------------------------------------------------------------------------- /doc/assets/topology.md: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | graph BT 3 | 中继型-->公共型 4 | 逻辑2型-->公共型 5 | 逻辑2型-->中继型 6 | 逻辑1型-->公共型 7 | ``` 8 | -------------------------------------------------------------------------------- /doc/框架层功能-心跳机制.md: -------------------------------------------------------------------------------- 1 | 待写 -------------------------------------------------------------------------------- /doc/框架层功能-服务发现.md: -------------------------------------------------------------------------------- 1 | 2 | ## 时序图 3 | 4 | ![图](assets/mgrserver.jpeg) 5 | 6 | 说明: 7 | 1. mgrserver 把自己 ip/port 写入 Redis 8 | 2. 其他服务一直读 redis ,得到 mgrserver ip/port 后,链接 mgrserver。最终直到链接成功为止,发送自身信息。 9 | 3. mgrserver 感知其他服务链入或断开, 下发服务变化消息 10 | 4. 其他服务感知 mgrserver链接断开,则重复 2 操作 11 | 5. mgrserver 与其他服务定时 pingpong 12 | -------------------------------------------------------------------------------- /doc/框架层功能-消息中继.md: -------------------------------------------------------------------------------- 1 | ## 中继类型 2 | 3 | 根据`玩家`对象在其生命周期内,是否常驻于某些服务节点上。 4 | 5 | 可以把服务分为: 6 | - 有状态服务 7 | - 无状态服务 8 | 9 | 因此,消息中继也分为两种类型: 10 | - 状态中继 11 | - 随机中继 12 | 13 | ## 状态服务资源 14 | 15 | 框架层在 ILogin 接口上,提供了制定`玩家`需要分配哪些有状态的服务: 16 | 17 | ```go 18 | // ILogin : 登陆模块接口 19 | type ILogin interface { 20 | // ... (其他接口略) ... 21 | RegisterAllocationNodeType(types []config.NodeType) // 注册自定义要分配的服务器节点类型 22 | // ... (其他接口略) ... 23 | } 24 | ``` 25 | 26 | 具体实现,如 services/login/login.go : 27 | 28 | ```go 29 | // Start : 启动 30 | func (login *Login) Start() bool { 31 | // ... (其他代码略) ... 32 | Ctx.Login.RegisterAllocationNodeType([]config.NodeType{config.Gateway, services.Lobby}) 33 | // ... (其他代码略) ... 34 | return true 35 | } 36 | ``` 37 | 38 | 上述 RegisterAllocationNodeType 代码调用,让框架层知道`玩家`登录时,会给玩家分配 `Gateway` 、 `Lobby` 39 | 40 | 有消息让 `Gateway` 中继到 `Lobby` 时,必定中继到同一个 `Lobby` 上 41 | -------------------------------------------------------------------------------- /doc/框架层功能-登出模块.md: -------------------------------------------------------------------------------- 1 | ## 登出模块 2 | 3 | 账号登出游戏,涉及到资源释放、游戏存档等问题 4 | 5 | 且每个服务器都可能有特殊的登出机制 6 | 7 | 这里详细罗列下: 8 | - 框架层对每个服务的登出处理 9 | - servers 例子服务实现中的登出处理 10 | 11 | 12 | ## Login 服务 13 | 14 | - 框架层 15 | - 无 16 | 17 | - servers 18 | - [user.go](../services/login/user.go) ,TCP 客户端会话类,在连接断开时会触发关闭,并销毁 19 | 20 | ## Gateway 服务 21 | 22 | - 框架层 23 | - [user_mgr.go](../internal/components/node/gateway/user_mgr.go) ,[闲置连接处理](框架层功能-闲置连接处理.md) 24 | - 默认连接 5 分钟不活跃,则判断为闲置连接。触发关闭连接、删除等操作 25 | - 账号被判断为闲置连接的处理中,会发送`丢失账号`消息通知账号相关联的服务 26 | - Gateway `登录亲和性`,默认闲置删除后 5 分钟内还存在。即 5 + 5 , 10 分钟内账号对 Gateway 有`登录亲和性` 27 | - servers 28 | - [user.go](../services/gateway/user.go) ,TCP 客户端会话类,在连接断开时会触发关闭,并销毁 29 | - [user_mgr.go](../services/gateway/user_mgr.go) ,TCP 客户端会话管理类,在连接断开时会触发删除操作 30 | 31 | 32 | ## Lobby 服务 33 | 34 | - 框架层 35 | 36 | ![图](assets/logout.png) 37 | 38 | - servers 39 | - [account_mgr.go](../services/lobby/account_mgr.go) ,框架层触发账号登出时,触发该函数账号登出处理 40 | -------------------------------------------------------------------------------- /doc/框架层功能-登陆模块.md: -------------------------------------------------------------------------------- 1 | ## 框架层负责的工作 2 | 3 | - 提供缺省账号验证 4 | - 提供账号正常登陆 5 | - 提供账号正常上下线,不会回档等错误 6 | - 提供同账号多端同时登陆,不会异常 7 | - 提供账号服务器资源分配(可自定义分配哪些资源) 8 | - 提供自定义验证接口,方便定制化`账号验证`逻辑 9 | 10 | 11 | ## 逻辑层负责的工作 12 | 13 | - 自定义登陆协议(如使用 TCP 、 HTTP ; 如使用 proto 消息、 struct 消息 等等) 14 | - 自定义客户端交互流程 15 | - 自定义账号验证过程 16 | - 自定义分配哪些服务器资源给账号 17 | 18 | 19 | ## 时序图 20 | 21 | ![图1](assets/login.jpeg) 22 | 23 | 补充说明: 24 | - `SETGETX` 25 | - 原子操作,封装`不管 SETGETX 有没有设置成功,都重置过期时间为 n` 26 | - 返回值,`空`表示设置成功;`非空`表示设置失败,并返回原先已设置的值 27 | - `DELX` 28 | - 原子操作,封装`条件删除, if value = "xxx" then del key` 29 | - 返回值, 1 有删除操作; 0 不需要删除 30 | - 1 、 4 逻辑层自定义处理 31 | - 2 、 3 框架层处理 32 | - 5 、 6 防止`同 1 账号开服期间只登陆同一个 Gateway` 33 | -------------------------------------------------------------------------------- /doc/框架层功能-网关模块.md: -------------------------------------------------------------------------------- 1 | ## 框架层负责的工作 2 | 3 | - 提供服务器组内节点互连、接入验证 4 | - 提供 Client(s) <-> Gateway <-> Node 路径的消息转发 5 | - 提供 Node <-> Gateway <-> Other Node(s) 路径的消息转发 6 | - 框架层会接管几乎所有的 Gateway 的功能(Gateway 会很纯粹,主要就是消息转发) 7 | 8 | 9 | ## 逻辑层负责的工作 10 | 11 | - 自定义客户端交互协议(如使用 TCP 、 HTTP ; 如使用 proto 消息、 struct 消息 等等) 12 | - 自定义加解密算法 13 | 14 | ## 面向逻辑层的接口 15 | 16 | ```go 17 | // FuncTypeEncode : 数据加密函数声明 18 | type FuncTypeEncode func(data []byte) []byte 19 | 20 | // FuncTypeDecode : 数据解密函数声明 21 | type FuncTypeDecode func(data []byte) []byte 22 | 23 | // FuncTypeSendToClient : 发送客户端数据函数声明 24 | type FuncTypeSendToClient func(account string, cmd uint64, data []byte, flag uint8) bool 25 | 26 | // FuncTypeSendToAllClient : 发送所有客户端数据函数声明 27 | type FuncTypeSendToAllClient func(cmd uint64, data []byte, flag uint8) bool 28 | 29 | // IClientSesion : 客户端会话类 30 | type IClientSesion interface { 31 | Close() 32 | } 33 | 34 | // IGateway : 网关模块接口 35 | type IGateway interface { 36 | VerifyToken(account, token string, clientSession IClientSesion) uint32 // 令牌验证。返回值: 0 成功;1 令牌错误; 2 系统错误 37 | OnRecvFromClient(account string, cmd uint32, data []byte, flag uint8) (done bool) // 可自定义客户端交互协议。 逻辑层代码处理好协议相关事宜后,主动调用该函数,把数据投递给框架层。 done 为 true ,表示框架层接管处理该消息 38 | RegisterSendToClient(f FuncTypeSendToClient) // 可自定义客户端交互协议。 框架层收到其他服务节点来的消息,调用此函数注册的回调,把数据投递给逻辑层。 逻辑层可处理协议相关事宜 39 | RegisterSendToAllClient(f FuncTypeSendToAllClient) // 可自定义客户端交互协议 40 | RegisterEncodeFunc(f FuncTypeEncode) // 可自定义加解密算法 41 | RegisterDecodeFunc(f FuncTypeDecode) // 可自定义加解密算法 42 | } 43 | ``` 44 | 45 | ## 拓扑结构 46 | 47 | Gateway 在服务器拓扑结构中,起着重要的作用:**把不同的节点连接起来** 48 | 49 | 详细的拓扑结构,请参见[规范-服务器架构.md](规范-服务器架构.md) 50 | 51 | ## 消息号规范 52 | 53 | 发给 Gateway 的消息, Gateway 需要知道往哪里发 54 | 55 | 详细规范,请参见[规范-消息号.md](规范-消息号.md) 56 | 57 | ## 消息中继 58 | 59 | 消息中继分为几种类型,详细请参见[框架层功能-消息中继](框架层功能-消息中继.md) 60 | -------------------------------------------------------------------------------- /doc/框架层功能-闲置连接处理.md: -------------------------------------------------------------------------------- 1 | ## 闲置连接处理 2 | 3 | 客户端连接 Gateway 后, 5 分钟不发送消息,会被判断为`闲置连接` 4 | 5 | ## 闲置间隔配置 6 | 7 | 默认值 : 5 分钟 8 | 9 | 更改配置,可以以下方法之一: 10 | - go-xserver/common/config.go 11 | - 修改 ConfigRole::IdleTime 字段 Tag 中的缺省值 12 | - 程序启动命令行添加参数 --role-idletime ,来指定具体值 13 | - 设置环境变量 GOXSERVER_ROLE_IDLETIME 14 | 15 | 16 | ## 相关代码 17 | 18 | [user_mgr.go](../internal/components/node/gateway/user_mgr.go) 文件中的 checkActive 函数 19 | 20 | ```go 21 | func (userMgr *UserMgr) checkActive() { 22 | userMgr.mutex.Lock() 23 | defer userMgr.mutex.Unlock() 24 | 25 | // 检查闲置连接 26 | now := userMgr.ctx.GetTickCount() 27 | var dels []*User 28 | for _, user := range userMgr.users { 29 | if now-user.ActiveTimestamp >= userMgr.ctx.Config().Role.IdleTime*1000 { 30 | dels = append(dels, user) 31 | } 32 | } 33 | 34 | // TODO: 删除操作现在是 1 条 1 条执行,会很慢,极端情况下,是卡玩家登录的。 35 | // 待优化为 REDIS PIPELINING 模式 36 | // 参考 : https://godoc.org/github.com/gomodule/redigo/redis#hdr-Pipelining ,类似: 37 | // c.Send("SET", "foo", "bar") 38 | // c.Send("GET", "foo") 39 | // c.Flush() 40 | // c.Receive() // reply from SET 41 | // v, err = c.Receive() // reply from GET 42 | 43 | // 删除闲置连接 44 | for _, user := range dels { 45 | for nodeType, serverID := range user.Servers { 46 | _ = serverID 47 | key := db.GetKeyAllocServer(nodeType, user.Account) 48 | ttl := userMgr.ctx.Config().Role.SessionAffinityInterval 49 | if _, err := userMgr.ServerRedisCli.Do("EXPIRE", key, ttl); err != nil { // 设置账号分配的服务器资源信息,过期时间 5 分钟 50 | userMgr.ctx.Errorln(err, "account:", user.Account) 51 | } 52 | } 53 | delete(userMgr.users, user.Account) 54 | user.ClientSession.Close() 55 | userMgr.ctx.Infoln("Delete user information, account:", user.Account) 56 | } 57 | } 58 | ``` 59 | 60 | ## 彩蛋 61 | 62 | checkActive 函数中,留一个优化操作,优先级比较低,修改也超简单,暂时注释下,待写 63 | -------------------------------------------------------------------------------- /doc/规范-代码框架.md: -------------------------------------------------------------------------------- 1 | ## 代码框架 2 | 3 | go-xserver 严格区分框架层与逻辑层 4 | 5 | go-xserver 加载逻辑插件的方式运行 6 | 7 | 8 | ## 框架层 9 | 10 | 主要交付内容如下: 11 | - go-xserver 可执行文件 12 | - go-xserver 是入口程序,会加载逻辑插件,启动服务 13 | - github.com/fananchong/go-xserver/common 包 14 | - common 包都是接口文件,会尽力保持接口不变 15 | 16 | ## 逻辑层 17 | 18 | 使用框架的程序,按一定规范,制作一个插件,即可使用 go-xserver 19 | 20 | ## 代码分析 21 | 22 | 代码主要分布在 3 个目录: 23 | 24 | - services 25 | - common 26 | - internal 27 | 28 | #### services 目录 29 | 30 | 本目录下的每个子目录都是一个例子服务或默认服务 31 | 32 | 它会用 common 目录中的一些公共代码、接口 33 | 34 | internal 目录在不在都不会影响 services 目录中代码的编译、运行 35 | 36 | 即 internal 目录可以对 services 目录不可见 37 | 38 | #### common 目录 39 | 40 | 通常为 interface 接口声明为主,细节实现可以放入 internal 目录 41 | 42 | 其实现,会被编译进 go-xserver 程序内 43 | 44 | #### internal 目录 45 | 46 | 框架层代码。可以对 services 目录不可见 47 | 48 | 49 | #### 例子参考 50 | 51 | 比如对 log 的封装代码: 52 | 53 | - [common/log.go](../common/context/log.go) 54 | - [internal/log.go](../internal/components/log.go) 55 | - [internal/app.go](../internal/app.go) 函数 Run 56 | -------------------------------------------------------------------------------- /doc/规范-协议.md: -------------------------------------------------------------------------------- 1 | ## 协议类型 2 | 3 | 有两类协议: 4 | - 框架层内部协议 5 | - 逻辑层用户协议 6 | 7 | go-xserver 力求不限制用户的协议规范,即比如: 8 | - Login 可以自定义协议,不管是使用 TCP 、 HTTP ; 还是使用 struct 协议、 proto 协议 等 9 | - Gateway 也是一样 10 | - 后续其他服务器同样力求不限制用户的协议规范 11 | 12 | ## 服务器默认实现 13 | 14 | 常见的逻辑服务器,都会提供一个默认实现。以供参考、或者直接使用 15 | -------------------------------------------------------------------------------- /doc/规范-数据库.md: -------------------------------------------------------------------------------- 1 | ## 数据库使用规范 2 | 3 | - 以 Redis 为主 4 | - 每种类型数据,使用一个 Redis 库(或集群),并在配置文件中定义 5 | 6 | 7 | ## 目前已定义数据库 8 | 9 | 数据库 | 用途 | 是否持久化 10 | ------------|----------------------------|----------------------- 11 | dbaccount | 账号角色信息库 | 是。必须维护好,定时归档 12 | dbtoken | 令牌库 | 否 13 | dbserver | 给账号分配的服务器信息 | 否 14 | dbmgr | 服务器信息 | 是。不需要归档,临时持久化数据 15 | dbrolename | 角色名-账号对应表 | 是。必须维护好,定时归档 16 | -------------------------------------------------------------------------------- /doc/规范-服务器架构.md: -------------------------------------------------------------------------------- 1 | ## 服务器类型 2 | 3 | 从`连接关系`上分,有三类服务器: 4 | 5 | - 公共型 6 | - 其他服务器都会连接公共型 7 | - 目前有: 8 | - 管理服务器 9 | - 数据库服务器( Redis ) 10 | 11 | 12 | - 中继型 13 | - 中继消息的服务器 14 | - 目前有: 15 | - 网关, Client(s) <-> 中继型 <-> Node 16 | - 中继服务器, Node <-> 中继型 <-> Other Node(s) 17 | - 网关、中继服务器可以简化合进一个进程,亦可拆分 18 | - 本实作将网关、中继服务器合为 1 个进程,并称为 Gateway 19 | 20 | 21 | - 逻辑型 22 | - 提供某种功能的逻辑服务器 23 | - 需要消息中继,则会连接中继型服务器 24 | 25 | 26 | ## 拓扑图 27 | 28 | 三层结构,横向扩展: 29 | 30 | ![拓扑图](assets/topology.jpeg) 31 | 32 | 33 | 有以下特点: 34 | 35 | - 横向扩展 36 | - 简单连接 37 | - 单向,非网状 38 | - 中继型起桥梁作用 39 | - 逻辑型接入简单 40 | -------------------------------------------------------------------------------- /doc/规范-服务类型.md: -------------------------------------------------------------------------------- 1 | ## 服务类型 2 | 3 | 有部分特殊功能的服务,框架层会涉及,并提供完整的服务(可定制的): 4 | - 管理服务器 5 | - 服务发现 6 | - 网关服务器 7 | - 客户端消息中继 8 | - 服务器组内消息中继 9 | - 登录服务器 10 | - 账号验证 11 | - 服务器资源分配 12 | 13 | 服务类型定义在 2 个文件中 14 | - common/config/server_type_framework.go 15 | - 定义了框架层定义的服务类型 16 | - services/server_type_custom.go 17 | - 定义逻辑层程序自己需要扩展的服务类型 18 | -------------------------------------------------------------------------------- /doc/规范-消息号.md: -------------------------------------------------------------------------------- 1 | # 消息号规范 2 | 3 | 经过 Gateway 转发的消息,需要一定的规则,让 Gateway 知道往哪里发 4 | 5 | ## 中继消息号 6 | 7 | 消息号 = 服务类型 * 偏移 + 数字 8 | 9 | 举例: Lobby 服务类型为 4 ,偏移为 1000,则 Lobby 的消息请定义为: 4001、4002、... 等等 10 | 11 | ## 消息号偏移量如何更改 12 | 13 | 默认值:1000 14 | 15 | 需要修改 2 处地方: 16 | - go-xserver/common/config.go 17 | - 修改 ConfigCommon::MsgCmdOffset 字段 Tag 中的缺省值 18 | 19 | - go-xserver/services/internal/protocol 目录下部分 proto 文件 20 | - lobby.proto 文件中的 CMD_LOBBY.ENUM.MSGCMDOFFSET 字段 21 | 22 | ## 消息号偏移量对业务逻辑不可见 23 | 24 | 网络数据包中, Client <-> Server ,消息号都必须做过偏移。但不影响业务代码中使用真实的消息号 25 | 26 | - 框架层传递给逻辑层时,已经`脱掉`偏移量。即逻辑层收到的时真是的消息号 27 | - 客户端可封装下`网络收发函数`,让客户端逻辑层也做到偏移量不可见 28 | -------------------------------------------------------------------------------- /doc/规范-脚本规范.md: -------------------------------------------------------------------------------- 1 | ## go-xserver 开发模式介绍 2 | 3 | [go-xserver 在 Windows 10 中开发,并在 Linux 环境中调试运行](编译-在Win10中Linux环境搭建.md) 4 | 5 | ## go-xserver 脚本规范 6 | 7 | go-xserver 下脚本将统一使用 Linux Shell 脚本 8 | -------------------------------------------------------------------------------- /doc/规范-配置文件_框架层.md: -------------------------------------------------------------------------------- 1 | ## 框架层配置文件规范 2 | 3 | go-xserver 中框架层配置可以表现为下面的形式: 4 | 5 | - 配置结构体 6 | 7 | - [go-xserver/common/config/framework.go](go-xserver/common/config/framework.go) 8 | 9 | - 配置文件 10 | 11 | - [go-xserver/common/config/framework.toml](go-xserver/common/config/framework.toml) 12 | 13 | - 可提供工具,根据 framework.go 自动生成 framework.toml 模板文件(优先级低) 14 | 15 | - 命令行参数 16 | 17 | - 自动生成 18 | 19 | - 环境变量 20 | 21 | - 自动生成 22 | 23 | 以上均存在一一对应关系 24 | 25 | 26 | ## 缺省值 27 | 28 | - 配置文件中可以删除字段(即不配置某字段),而使用缺省值 29 | 30 | - 可以在`配置结构体`的 Tag 中,填写 default ,定义缺省值 31 | 32 | ## 优先级 33 | 34 | 优先级从高到低如下: 35 | 36 | ```shell 37 | 命令行参数 > 环境变量 > 配置文件 > 缺省值 38 | ``` 39 | 40 | **高优先级值覆盖低优先级的值** 41 | 42 | 43 | ## 查看命令行参数、环境变量 44 | 45 | 命令行参数、环境变量自动生成,以下命令查看: 46 | 47 | ```shell 48 | go-xserver -h 49 | ``` 50 | 51 | 目前有: 52 | 53 | ```shell 54 | Usage: 55 | go-xserver [flags] 56 | 57 | Flags: 58 | -a, --app string 应用名(插件,必填) 59 | --common-debug Debug 版本标志 (default true) 60 | --common-intranettoken string 内部服务器验证 TOKEN (default "6d8f1f3a-739f-47fe-9ed1-ea39276cd10d") 61 | --common-logdir string Log 路径 (default "./logs") 62 | --common-logflushinterval int Log 写入到文件的时间间隔,单位:毫秒 (default 1000) 63 | --common-loglevel int Log 等级, 1 infoLog; 2 warningLog; 3 errorLog; 4 fatalLog (default 1) 64 | --common-msgcmdoffset int 消息号 = 服务类型 * MsgCmdOffset + 数字 (default 1000) 65 | --common-pprof string Http pprof 地址 66 | --common-version string 版本号 (default "0.0.1") 67 | -c, --config string 配置目录路径 (default "../config") 68 | --dbaccount-addrs strings Redis 数据库地址 (default [127.0.0.1:6379]) 69 | --dbaccount-dbindex int Redis DB 索引 (default 1) 70 | --dbaccount-name string Redis 数据库名称 71 | --dbaccount-password string Redis 数据库密码 (default "123456") 72 | --dbmgr-addrs strings Redis 数据库地址 (default [127.0.0.1:6379]) 73 | --dbmgr-dbindex int Redis DB 索引 (default 1) 74 | --dbmgr-name string Redis 数据库名称 75 | --dbmgr-password string Redis 数据库密码 (default "123456") 76 | --dbrolename-addrs strings Redis 数据库地址 (default [127.0.0.1:6379]) 77 | --dbrolename-dbindex int Redis DB 索引 (default 1) 78 | --dbrolename-name string Redis 数据库名称 79 | --dbrolename-password string Redis 数据库密码 (default "123456") 80 | --dbserver-addrs strings Redis 数据库地址 (default [127.0.0.1:6379]) 81 | --dbserver-dbindex int Redis DB 索引 (default 1) 82 | --dbserver-name string Redis 数据库名称 83 | --dbserver-password string Redis 数据库密码 (default "123456") 84 | --dbtoken-addrs strings Redis 数据库地址 (default [127.0.0.1:6379]) 85 | --dbtoken-dbindex int Redis DB 索引 (default 1) 86 | --dbtoken-name string Redis 数据库名称 87 | --dbtoken-password string Redis 数据库密码 (default "123456") 88 | -h, --help help for go-xserver 89 | --network-ipinner string 内网 IP (default "127.0.0.1") 90 | --network-ipouter string 外网 IP (default "127.0.0.1") 91 | --network-iptype int 类型: 1 表示使用 IP;类型: 0 表示使用网卡名 (default 1) 92 | --network-port strings 第一个端口为对外提供服务的端口;第二个端口为服务器组内提供服务的端口;若有其他继续填充,自定义 (default [7500,30000]) 93 | --role-idletime int 角色闲置该时间间隔后账号、角色对象内存中清除。单位:秒 (default 300) 94 | --role-sessionaffinityinterval int 账号登出后,分配的服务器资源信息保留时间。单位:秒 (default 300) 95 | -s, --suffix string Log 文件名后缀,多开时可以通过它,避免多个进程共用 1 个 Log 文件 96 | 97 | Environment variables: 98 | GOXSERVER_APP 99 | GOXSERVER_COMMON_DEBUG 100 | GOXSERVER_COMMON_INTRANETTOKEN 101 | GOXSERVER_COMMON_LOGDIR 102 | GOXSERVER_COMMON_LOGFLUSHINTERVAL 103 | GOXSERVER_COMMON_LOGLEVEL 104 | GOXSERVER_COMMON_MSGCMDOFFSET 105 | GOXSERVER_COMMON_PPROF 106 | GOXSERVER_COMMON_VERSION 107 | GOXSERVER_CONFIG 108 | GOXSERVER_DBACCOUNT_ADDRS 109 | GOXSERVER_DBACCOUNT_DBINDEX 110 | GOXSERVER_DBACCOUNT_NAME 111 | GOXSERVER_DBACCOUNT_PASSWORD 112 | GOXSERVER_DBMGR_ADDRS 113 | GOXSERVER_DBMGR_DBINDEX 114 | GOXSERVER_DBMGR_NAME 115 | GOXSERVER_DBMGR_PASSWORD 116 | GOXSERVER_DBROLENAME_ADDRS 117 | GOXSERVER_DBROLENAME_DBINDEX 118 | GOXSERVER_DBROLENAME_NAME 119 | GOXSERVER_DBROLENAME_PASSWORD 120 | GOXSERVER_DBSERVER_ADDRS 121 | GOXSERVER_DBSERVER_DBINDEX 122 | GOXSERVER_DBSERVER_NAME 123 | GOXSERVER_DBSERVER_PASSWORD 124 | GOXSERVER_DBTOKEN_ADDRS 125 | GOXSERVER_DBTOKEN_DBINDEX 126 | GOXSERVER_DBTOKEN_NAME 127 | GOXSERVER_DBTOKEN_PASSWORD 128 | GOXSERVER_NETWORK_IPINNER 129 | GOXSERVER_NETWORK_IPOUTER 130 | GOXSERVER_NETWORK_IPTYPE 131 | GOXSERVER_NETWORK_PORT 132 | GOXSERVER_ROLE_IDLETIME 133 | GOXSERVER_ROLE_SESSIONAFFINITYINTERVAL 134 | GOXSERVER_SUFFIX 135 | ``` 136 | -------------------------------------------------------------------------------- /doc/规范-配置文件_逻辑层.md: -------------------------------------------------------------------------------- 1 | ## 自由加载配置 2 | 3 | go-xserver 不会限制逻辑层使用哪种方法加载配置文件、配置文件格式 4 | 5 | go-xserver 框架层提供了一个默认配置文件加载接口,推荐使用 6 | 7 | 8 | ## 默认配置文件加载接口 ( IConfig.LoadConfig ) 9 | 10 | 接口在 [common/context/config.go](../common/context/config.go) 文件中定义: 11 | 12 | ```go 13 | // IConfig : 配置类接口 14 | type IConfig interface { 15 | LoadConfig(cfgfile string, cfgobj interface{}) bool // 逻辑层可以用该接口加载配置文件到 cfgobj 结构体对象, cfgobj 为指针类型 16 | Config() *config.FrameworkConfig // 获取框架层配置 17 | PrintUsage() // 打印命令行参数 18 | } 19 | ``` 20 | 21 | #### 使用例子 22 | 23 | 可以参考 Login 服务 24 | 25 | - [services/login/login.go](../services/login/login.go) : 26 | 27 | ```go 28 | // LoginConfig : 登录服务器配置 29 | type LoginConfig struct { 30 | AllocServers []config.NodeType `default:"[3]" desc:"给账号分配哪些类型服务器"` 31 | } 32 | func (login *Login) Start() bool { 33 | if ok := Ctx.LoadConfig("login.toml", login.cfg); !ok { 34 | Ctx.Errorln("Load config fail, config: login.toml") 35 | return false 36 | } 37 | // ... (省略代码) ... 38 | Ctx.RegisterAllocationNodeType(login.cfg.AllocServers) 39 | // ... (省略代码) ... 40 | return true 41 | } 42 | ``` 43 | 44 | - [services/login/login.toml](../services/login/login.toml) : 45 | 46 | ```toml 47 | AllocServers = [3, 4] 48 | ``` 49 | 50 | ## 其他 51 | 52 | 由于插件初始化与配置初始化的顺序问题,暂时无法把逻辑层配置合并进`环境变量`与`命令行参数`上 53 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fananchong/go-xserver 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.1 // indirect 7 | github.com/fananchong/glog v0.0.0-20190224032502-d814c6adef29 8 | github.com/fananchong/go-redis-orm.v2 v0.0.0-20190224024744-b3158985225c 9 | github.com/fananchong/gotcp v0.0.0-20190515204732-9d69e65d7c79 10 | github.com/fsnotify/fsnotify v1.4.7 11 | github.com/gogo/protobuf v1.2.1 12 | github.com/gomodule/redigo v2.0.0+incompatible 13 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 14 | github.com/satori/go.uuid v1.2.0 15 | github.com/spf13/cobra v0.0.3 16 | github.com/spf13/pflag v1.0.3 17 | github.com/spf13/viper v1.3.2 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/FZambia/sentinel v1.0.0 h1:KJ0ryjKTZk5WMp0dXvSdNqp3lFaW1fNFuEYfrkLOYIc= 4 | github.com/FZambia/sentinel v1.0.0/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2FT26vP5gI= 5 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 6 | github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= 7 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 8 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 9 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/fananchong/glog v0.0.0-20190224032502-d814c6adef29 h1:a9L/BvSUx3g3HDWWz0sOnHWIFrDxfjn2v92PNFnxRSE= 13 | github.com/fananchong/glog v0.0.0-20190224032502-d814c6adef29/go.mod h1:GEGZvOvgIeNNhzjxczz39fvYeYwhrRVyfYB+n3GcFeo= 14 | github.com/fananchong/go-redis-orm.v2 v0.0.0-20190224024744-b3158985225c h1:SZZc7SX1mxGXYO9bvTTy0aL1QJbbhQp2X+gb0mEp2bU= 15 | github.com/fananchong/go-redis-orm.v2 v0.0.0-20190224024744-b3158985225c/go.mod h1:HgSap/1Tq/6LazUQ/g4R25n0GgArz3eDdu3NM8ddEU8= 16 | github.com/fananchong/goredis v0.0.0-20181224141957-8c4a4601c4c9 h1:8TZIKl6eywKmlNw3JGUSnvhNwuUdYiavh4Mp/DJbSXo= 17 | github.com/fananchong/goredis v0.0.0-20181224141957-8c4a4601c4c9/go.mod h1:5LLIOz1tkD4ZKNwGVP2Ns9DlF4bCPsi3a5J7GkwuJQo= 18 | github.com/fananchong/gotcp v0.0.0-20190515204732-9d69e65d7c79 h1:lvJMtK4wE1feK/QIiWEsF9hIxu3A9xAxUbghxR0+nbY= 19 | github.com/fananchong/gotcp v0.0.0-20190515204732-9d69e65d7c79/go.mod h1:NwqSPUhE3lftRXXTbIYO3aSdkdQSs2gCGIZA4Mi1B6c= 20 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 21 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 22 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 23 | github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= 24 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 25 | github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= 26 | github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= 27 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 28 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 29 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 30 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 31 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 32 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 33 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= 34 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 35 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 36 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 37 | github.com/mna/redisc v1.1.3 h1:jPCXHNsZXQV9BtNNZWZlL8h29SRjSRUeqIdBQKPlykw= 38 | github.com/mna/redisc v1.1.3/go.mod h1:GXeOb7zyYKiT+K8MKdIiJvuv7MfhDoQGcuzfiJQmqQI= 39 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 40 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 41 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 42 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 43 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 44 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 45 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 46 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 47 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 48 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 49 | github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= 50 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 51 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 52 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 53 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 54 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 55 | github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= 56 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 57 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 58 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 59 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 60 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 61 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 62 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A= 63 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 64 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 65 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 66 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 67 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 68 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 69 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 70 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 71 | -------------------------------------------------------------------------------- /internal/app.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/fananchong/go-xserver/common" 7 | "github.com/fananchong/go-xserver/internal/components" 8 | "github.com/fananchong/go-xserver/internal/components/misc" 9 | nodegateway "github.com/fananchong/go-xserver/internal/components/node/gateway" 10 | nodelogin "github.com/fananchong/go-xserver/internal/components/node/login" 11 | nodemgr "github.com/fananchong/go-xserver/internal/components/node/mgr" 12 | nodenormal "github.com/fananchong/go-xserver/internal/components/node/normal" 13 | "github.com/fananchong/go-xserver/internal/utils" 14 | ) 15 | 16 | // App : 应用程序类 17 | type App struct { 18 | ctx *common.Context 19 | components []utils.IComponent 20 | } 21 | 22 | // NewApp : 应用程序类的构造函数 23 | func NewApp() *App { 24 | app := &App{ 25 | ctx: &common.Context{Context: misc.CreateContext()}, 26 | } 27 | return app 28 | } 29 | 30 | // Run : 启动应用程序 31 | func (app *App) Run() { 32 | // 注册组件 33 | app.components = []utils.IComponent{ 34 | components.NewTime(app.ctx), 35 | components.NewRand(app.ctx), 36 | components.NewConfig(app.ctx), 37 | components.NewLog(app.ctx), 38 | components.NewPprof(app.ctx), 39 | components.NewPlugin(app.ctx), 40 | components.NewRedis(app.ctx), 41 | components.NewRole2Account(app.ctx), 42 | components.NewUID(app.ctx), 43 | components.NewTCPServer(app.ctx), 44 | nodenormal.NewNormal(app.ctx), 45 | nodemgr.NewMgr(app.ctx), 46 | nodelogin.NewLogin(app.ctx), 47 | nodegateway.NewGateway(app.ctx), 48 | components.NewSignal(app.ctx), 49 | } 50 | 51 | // 应用程序正式运行 52 | defer app.onAppShutDown() 53 | app.onAppReady() 54 | } 55 | 56 | func (app *App) onAppReady() { 57 | misc.SetComponentCount(app.ctx, len(app.components)) 58 | for i := 0; i < len(app.components); i++ { 59 | misc.OneComponentOK(app.ctx) 60 | c := app.components[i] 61 | if !c.Start() { 62 | os.Exit(1) 63 | } 64 | } 65 | } 66 | 67 | func (app *App) onAppShutDown() { 68 | for i := len(app.components) - 1; i >= 0; i-- { 69 | c := app.components[i] 70 | c.Close() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/components/config.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "reflect" 7 | "sort" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/fananchong/go-xserver/common" 12 | "github.com/fananchong/go-xserver/common/config" 13 | "github.com/fsnotify/fsnotify" 14 | "github.com/spf13/cobra" 15 | "github.com/spf13/pflag" 16 | "github.com/spf13/viper" 17 | ) 18 | 19 | const frameworkCfgName = "framework.toml" 20 | 21 | // Config : 配置组件 22 | type Config struct { 23 | ctx *common.Context 24 | configPath string 25 | app string 26 | suffix string 27 | rootCmd *cobra.Command 28 | viperObjs map[string]*viper.Viper 29 | configObj *config.FrameworkConfig 30 | } 31 | 32 | // NewConfig : 实例化 33 | func NewConfig(ctx *common.Context) *Config { 34 | cfg := &Config{ 35 | ctx: ctx, 36 | viperObjs: make(map[string]*viper.Viper), 37 | configObj: &config.FrameworkConfig{}, 38 | } 39 | cfg.viperObjs[frameworkCfgName] = viper.New() 40 | if err := cfg.init(); err != nil { 41 | fmt.Println(err) 42 | os.Exit(1) 43 | } 44 | ctx.IConfig = cfg 45 | return cfg 46 | } 47 | 48 | // Start : 实例化组件 49 | func (*Config) Start() bool { 50 | return true 51 | } 52 | 53 | // Close : 关闭组件 54 | func (*Config) Close() { 55 | // No need to do anything 56 | } 57 | 58 | const constEnvPrefix string = "GOXSERVER" 59 | 60 | func (cfg *Config) init() error { 61 | viperObj := cfg.viperObjs[frameworkCfgName] 62 | cfg.rootCmd = &cobra.Command{ 63 | Use: "go-xserver", 64 | Run: func(c *cobra.Command, args []string) { 65 | if appName := viperObj.GetString("app"); appName == "" { 66 | cfg.PrintUsage() 67 | os.Exit(1) 68 | } 69 | }, 70 | } 71 | cfg.rootCmd.SetHelpFunc(func(c *cobra.Command, args []string) { 72 | cfg.PrintUsage() 73 | os.Exit(1) 74 | }) 75 | flags := cfg.rootCmd.PersistentFlags() 76 | flags.StringVarP(&cfg.configPath, "config", "c", "../config", "配置目录路径") 77 | flags.StringVarP(&cfg.app, "app", "a", "", "应用名(插件,必填)") 78 | flags.StringVarP(&cfg.suffix, "suffix", "s", "", "Log 文件名后缀,多开时可以通过它,避免多个进程共用 1 个 Log 文件") 79 | viperObj.BindPFlags(cfg.rootCmd.PersistentFlags()) 80 | cfg.bindConfig(cfg.rootCmd, *cfg.configObj) 81 | cobra.OnInitialize(func() { 82 | viperObj.SetConfigFile(cfg.configPath + "/" + frameworkCfgName) 83 | viperObj.AutomaticEnv() 84 | if err := viperObj.ReadInConfig(); err != nil { 85 | fmt.Println("Failed to read configuration file, err =", err) 86 | os.Exit(1) 87 | } 88 | if err := viperObj.Unmarshal(cfg.configObj); err != nil { 89 | fmt.Println("Parsing the configuration file failed, err =", err) 90 | os.Exit(1) 91 | } 92 | viperObj.WatchConfig() 93 | viperObj.OnConfigChange(func(e fsnotify.Event) { 94 | c := &config.FrameworkConfig{} 95 | if err := viperObj.Unmarshal(c); err != nil { 96 | cfg.ctx.Errorln("Parsing the configuration file failed, err =", err) 97 | } else { 98 | if c.Common.Version != "" { 99 | cfg.configObj = c 100 | cfg.ctx.Printf("Configuration information is: %#v\n", cfg.configObj) 101 | } 102 | } 103 | }) 104 | }) 105 | return cfg.rootCmd.Execute() 106 | } 107 | 108 | func (cfg *Config) bindConfig(c *cobra.Command, s interface{}) { 109 | flags := c.Flags() 110 | cfg.parseStruct(flags, reflect.TypeOf(s), "") 111 | } 112 | 113 | func (cfg *Config) parseStruct(flags *pflag.FlagSet, rt reflect.Type, prefix string) { 114 | for i := 0; i < rt.NumField(); i++ { 115 | sf := rt.Field(i) 116 | rawName := strings.ToLower(sf.Name) 117 | if prefix != "" { 118 | rawName = prefix + "." + rawName 119 | } 120 | name := strings.Replace(rawName, ".", "-", -1) 121 | desc := sf.Tag.Get("desc") 122 | defaultValue := sf.Tag.Get("default") 123 | switch sf.Type.Kind() { 124 | case reflect.Struct: 125 | cfg.parseStruct(flags, sf.Type, rawName) 126 | continue 127 | case reflect.Bool: 128 | v, _ := strconv.ParseBool(defaultValue) 129 | flags.Bool(name, v, desc) 130 | case reflect.String: 131 | flags.String(name, defaultValue, desc) 132 | case reflect.Int, reflect.Int32: 133 | v, _ := strconv.ParseInt(defaultValue, 10, 32) 134 | flags.Int(name, int(v), desc) 135 | case reflect.Uint, reflect.Uint32: 136 | v, _ := strconv.ParseUint(defaultValue, 10, 32) 137 | flags.Uint(name, uint(v), desc) 138 | case reflect.Int64: 139 | v, _ := strconv.ParseInt(defaultValue, 10, 64) 140 | flags.Int64(name, int64(v), desc) 141 | case reflect.Uint64: 142 | v, _ := strconv.ParseUint(defaultValue, 10, 64) 143 | flags.Uint64(name, uint64(v), desc) 144 | case reflect.Slice: 145 | var v []string 146 | if defaultValue != "" { 147 | for _, tmp := range strings.Split(strings.Trim(defaultValue, "[]"), ",") { 148 | v = append(v, strings.Trim(tmp, " ")) 149 | } 150 | } 151 | flags.StringSlice(name, v, desc) 152 | default: 153 | fmt.Println("bindConfig fail, err = unsupported field: ", rawName) 154 | os.Exit(1) 155 | } 156 | viperObj := cfg.viperObjs[frameworkCfgName] 157 | viperObj.BindPFlag(rawName, flags.Lookup(name)) 158 | viperObj.BindEnv(rawName, strings.Replace(fmt.Sprintf("%s_%s", constEnvPrefix, strings.ToUpper(name)), "-", "_", -1)) 159 | } 160 | } 161 | 162 | // LoadConfig : 逻辑层可以用该接口加载配置文件 163 | func (cfg *Config) LoadConfig(cfgfile string, cfgobj interface{}) bool { 164 | if _, ok := cfg.viperObjs[cfgfile]; ok { 165 | return true 166 | } 167 | cfg.viperObjs[cfgfile] = viper.New() 168 | viperObj := cfg.viperObjs[cfgfile] 169 | viperObj.SetConfigFile(cfg.configPath + "/" + cfgfile) 170 | viperObj.AutomaticEnv() 171 | if err := viperObj.ReadInConfig(); err != nil { 172 | cfg.ctx.Errorf("Failed to read configuration file, err: %s, cfg: %s, path: %s", err.Error(), cfgfile, cfg.configPath) 173 | return false 174 | } 175 | if err := viperObj.Unmarshal(cfgobj); err != nil { 176 | cfg.ctx.Errorf("Parsing the configuration file failed, err: %s, cfg: %s, path: %s", err.Error(), cfgfile, cfg.configPath) 177 | return false 178 | } 179 | viperObj.WatchConfig() 180 | viperObj.OnConfigChange(func(e fsnotify.Event) { 181 | if err := viperObj.Unmarshal(cfgobj); err != nil { 182 | cfg.ctx.Errorf("Parsing the configuration file failed, err: %s, cfg: %s, path: %s", err.Error(), cfgfile, cfg.configPath) 183 | } else { 184 | cfg.ctx.Printf("Configuration information is: %#v\n", cfgobj) 185 | } 186 | }) 187 | return true 188 | } 189 | 190 | // Config : 获取框架层配置 191 | func (cfg *Config) Config() *config.FrameworkConfig { 192 | return cfg.configObj 193 | } 194 | 195 | // PrintUsage : 打印命令行参数 196 | func (cfg *Config) PrintUsage() { 197 | cfg.rootCmd.Usage() 198 | fmt.Println("") 199 | fmt.Println("Environment variables:") 200 | viperObj := cfg.viperObjs[frameworkCfgName] 201 | keys := viperObj.AllKeys() 202 | sort.Sort(sort.StringSlice(keys)) 203 | for _, k := range keys { 204 | env := strings.ToUpper(strings.Replace(constEnvPrefix+"_"+k, ".", "_", -1)) 205 | fmt.Printf(" %s\n", env) 206 | } 207 | } 208 | 209 | // GetViperObj : 获取 viper 对象 210 | func (cfg *Config) GetViperObj() *viper.Viper { 211 | return cfg.viperObjs[frameworkCfgName] 212 | } 213 | -------------------------------------------------------------------------------- /internal/components/log.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "time" 7 | 8 | "github.com/fananchong/glog" 9 | "github.com/fananchong/go-xserver/common" 10 | "github.com/fananchong/gotcp" 11 | ) 12 | 13 | // Log : 日志组件 14 | type Log struct { 15 | ctx *common.Context 16 | } 17 | 18 | // NewLog : 实例化 19 | func NewLog(ctx *common.Context) *Log { 20 | log := &Log{ctx: ctx} 21 | log.init() 22 | return log 23 | } 24 | 25 | // Start : 实例化组件 26 | func (log *Log) Start() bool { 27 | return true 28 | } 29 | 30 | func (log *Log) init() { 31 | v := log.ctx.IConfig.(*Config).GetViperObj() 32 | tmpLog := glog.GetLogger() 33 | tmpLog.SetAppName(filepath.Base(os.Args[0]) + "_" + v.GetString("app") + v.GetString("suffix")) 34 | logDir := log.ctx.Config().Common.LogDir 35 | if logDir != "" { 36 | os.MkdirAll(logDir, os.ModePerm) 37 | } 38 | tmpLog.SetLogDir(logDir) 39 | tmpLog.SetLogLevel(log.ctx.Config().Common.LogLevel - 1) 40 | tmpLog.SetFlushInterval(time.Duration(log.ctx.Config().Common.LogFlushInterval) * time.Millisecond) 41 | log.ctx.ILogger = tmpLog 42 | 43 | // TODO : gotcp 需要支持非全局LOG类实例 44 | gotcp.SetLogger(log.ctx) 45 | } 46 | 47 | // Close : 关闭组件 48 | func (log *Log) Close() { 49 | if log.ctx != nil && log.ctx.ILogger != nil { 50 | log.ctx.Flush() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /internal/components/misc/context.go: -------------------------------------------------------------------------------- 1 | package misc 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/fananchong/go-xserver/common/config" 8 | ) 9 | 10 | // 框架层的一些全局变量,可以按以下方式 Set/Get 11 | 12 | // ContextValueType : context value 的 key 类型 13 | type ContextValueType int 14 | 15 | const ( 16 | _contextValueKey ContextValueType = iota 17 | _waitGroup 18 | _pluginType 19 | _pluginID 20 | ) 21 | 22 | // CreateContext : 获取 context.Context 对象 23 | func CreateContext() context.Context { 24 | contextValue := make(map[ContextValueType]interface{}) 25 | contextValue[_waitGroup] = &sync.WaitGroup{} 26 | contextValue[_pluginType] = 0 27 | contextValue[_pluginID] = 0 28 | return context.WithValue(context.Background(), _contextValueKey, contextValue) 29 | } 30 | 31 | // SetComponentCount : 设置组件数量 32 | func SetComponentCount(ctx context.Context, count int) { 33 | values := ctx.Value(_contextValueKey).(map[ContextValueType]interface{}) 34 | values[_waitGroup].(*sync.WaitGroup).Add(count) 35 | } 36 | 37 | // OneComponentOK : 某组件初始化完毕 38 | func OneComponentOK(ctx context.Context) { 39 | values := ctx.Value(_contextValueKey).(map[ContextValueType]interface{}) 40 | values[_waitGroup].(*sync.WaitGroup).Done() 41 | } 42 | 43 | // WaitComponent : 等待所有组件初始化完毕 44 | func WaitComponent(ctx context.Context) { 45 | values := ctx.Value(_contextValueKey).(map[ContextValueType]interface{}) 46 | values[_waitGroup].(*sync.WaitGroup).Wait() 47 | } 48 | 49 | // SetPluginType : 设置插件类型 50 | func SetPluginType(ctx context.Context, t config.NodeType) { 51 | values := ctx.Value(_contextValueKey).(map[ContextValueType]interface{}) 52 | values[_pluginType] = t 53 | } 54 | 55 | // GetPluginType : 获取插件类型 56 | func GetPluginType(ctx context.Context) config.NodeType { 57 | values := ctx.Value(_contextValueKey).(map[ContextValueType]interface{}) 58 | return values[_pluginType].(config.NodeType) 59 | } 60 | 61 | // SetPluginID : 设置插件ID 62 | func SetPluginID(ctx context.Context, id uint32) { 63 | values := ctx.Value(_contextValueKey).(map[ContextValueType]interface{}) 64 | values[_pluginID] = id 65 | } 66 | 67 | // GetPluginID : 获取插件ID 68 | func GetPluginID(ctx context.Context) uint32 { 69 | values := ctx.Value(_contextValueKey).(map[ContextValueType]interface{}) 70 | return values[_pluginID].(uint32) 71 | } 72 | -------------------------------------------------------------------------------- /internal/components/node/common/node.go: -------------------------------------------------------------------------------- 1 | package nodecommon 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/fananchong/go-xserver/common" 8 | "github.com/fananchong/go-xserver/common/config" 9 | "github.com/fananchong/go-xserver/common/context" 10 | "github.com/fananchong/go-xserver/internal/protocol" 11 | "github.com/fananchong/go-xserver/internal/utils" 12 | "github.com/fananchong/gotcp" 13 | ) 14 | 15 | // Node : 管理节点基类 16 | type Node struct { 17 | Ctx *common.Context 18 | DefaultNodeInterfaceImpl 19 | components []utils.IComponent 20 | mtx sync.Mutex 21 | } 22 | 23 | // NewNode : 管理节点基类实现类的构造函数 24 | func NewNode(ctx *common.Context, nodeType config.NodeType) *Node { 25 | node := &Node{Ctx: ctx} 26 | node.SessMgr = NewSessionMgr(ctx) 27 | node.Info = &protocol.SERVER_INFO{} 28 | node.Info.Type = uint32(nodeType) 29 | node.Info.Addrs = []string{utils.GetIPInner(ctx), utils.GetIPOuter(ctx)} 30 | node.Info.Ports = ctx.Config().Network.Port 31 | // TODO: 后续支持 32 | // node.Info.Overload 33 | // node.Info.Version 34 | return node 35 | } 36 | 37 | // Init : 初始化节点 38 | func (node *Node) Init(sessType interface{}, components []utils.IComponent) bool { 39 | // 分配 NODE ID 40 | node.Info.Id = NodeID2ServerID(NewNID(node.Ctx, config.NodeType(node.Info.Type))) 41 | 42 | // tcp server 43 | server := &gotcp.Server{} 44 | server.RegisterSessType(sessType) 45 | server.SetAddress(utils.GetIPInner(node.Ctx), utils.GetIntranetListenPort(node.Ctx)) 46 | server.SetUnfixedPort(true) 47 | server.SetUserData(&UserData{Ctx: node.Ctx, SessMgr: node.SessMgr}) 48 | 49 | // ping ticker 50 | pingTicker := utils.NewTickerHelper("PING", node.Ctx, 5*time.Second, node.ping) 51 | 52 | // bind components 53 | node.components = []utils.IComponent{ 54 | server, 55 | pingTicker, 56 | } 57 | node.components = append(node.components, components...) 58 | return true 59 | } 60 | 61 | // Start : 节点开始工作 62 | func (node *Node) Start() bool { 63 | node.mtx.Lock() 64 | defer node.mtx.Unlock() 65 | for _, v := range node.components { 66 | if v != nil && v.Start() == false { 67 | panic("") 68 | } 69 | switch v.(type) { 70 | case *gotcp.Server: 71 | node.Ctx.Config().Network.Port[utils.PORTFORINTRANET] = v.(*gotcp.Server).GetRealPort() 72 | } 73 | } 74 | return true 75 | } 76 | 77 | // Close : 关闭节点 78 | func (node *Node) Close() { 79 | node.mtx.Lock() 80 | defer node.mtx.Unlock() 81 | for _, v := range node.components { 82 | v.Close() 83 | } 84 | } 85 | 86 | func (node *Node) ping() { 87 | node.SessMgr.ForAll(func(sess *SessionBase) { 88 | msg := &protocol.MSG_MGR_PING{} 89 | sess.SendMsg(uint64(protocol.CMD_MGR_PING), msg) 90 | }) 91 | } 92 | 93 | // SendMsgToClient : 发送消息给客户端,通过 Gateway 中继 94 | func (node *Node) SendMsgToClient(account string, cmd uint64, data []byte, flag uint8) bool { 95 | // Gateway 、 MgrServer 调用该接口会 panic 96 | // - Gateway 不需要这个接口,没有意义 97 | // - MgrServer 如果有需求需要通过 Gateway 给客户端消息,则可以实现之。优先级太低了! 98 | panic("") 99 | } 100 | 101 | // BroadcastMsgToClient : 广播消息给客户端,通过 Gateway 中继 102 | func (node *Node) BroadcastMsgToClient(cmd uint64, data []byte, flag uint8) bool { 103 | // Gateway 、 MgrServer 调用该接口会 panic 104 | // - Gateway 不需要这个接口,没有意义 105 | // - MgrServer 如果有需求需要通过 Gateway 给客户端消息,则可以实现之。优先级太低了! 106 | panic("") 107 | } 108 | 109 | // SendMsgToServer : 发送消息给某类型服务(随机一个) 110 | func (node *Node) SendMsgToServer(t config.NodeType, cmd uint64, data []byte, flag uint8) bool { 111 | // Gateway 、 MgrServer 调用该接口会 panic 112 | // - Gateway 不需要这个接口,没有意义 113 | // - MgrServer 如果有需求需要通过 Gateway 给客户端消息,则可以实现之。优先级太低了! 114 | panic("") 115 | } 116 | 117 | // ReplyMsgToServer : 回发消息给请求服务器 118 | func (node *Node) ReplyMsgToServer(targetID context.NodeID, cmd uint64, data []byte, flag uint8) bool { 119 | // Gateway 、 MgrServer 调用该接口会 panic 120 | // - Gateway 不需要这个接口,没有意义 121 | // - MgrServer 如果有需求需要通过 Gateway 给客户端消息,则可以实现之。优先级太低了! 122 | panic("") 123 | } 124 | 125 | // BroadcastMsgToServer : 广播消息给某类型服务 126 | func (node *Node) BroadcastMsgToServer(t config.NodeType, cmd uint64, data []byte, flag uint8) bool { 127 | // Gateway 、 MgrServer 调用该接口会 panic 128 | // - Gateway 不需要这个接口,没有意义 129 | // - MgrServer 如果有需求需要通过 Gateway 给客户端消息,则可以实现之。优先级太低了! 130 | panic("") 131 | } 132 | -------------------------------------------------------------------------------- /internal/components/node/common/session.go: -------------------------------------------------------------------------------- 1 | package nodecommon 2 | 3 | import ( 4 | "github.com/fananchong/go-xserver/common" 5 | "github.com/fananchong/go-xserver/common/config" 6 | "github.com/fananchong/go-xserver/common/context" 7 | "github.com/fananchong/go-xserver/internal/components/misc" 8 | "github.com/fananchong/go-xserver/internal/protocol" 9 | "github.com/fananchong/go-xserver/internal/utils" 10 | "github.com/fananchong/gotcp" 11 | ) 12 | 13 | // ISessionDerived : SessionBase 派生类接口定义 14 | type ISessionDerived interface { 15 | DoVerify(msg *protocol.MSG_MGR_REGISTER_SERVER) 16 | DoRegister(msg *protocol.MSG_MGR_REGISTER_SERVER) 17 | DoLose(msg *protocol.MSG_MGR_LOSE_SERVER) 18 | DoClose(sessbase *SessionBase) 19 | DoRecv(cmd uint64, data []byte, flag byte) (done bool) 20 | } 21 | 22 | // SessionBase : 网络会话类 23 | type SessionBase struct { 24 | DefaultNodeInterfaceImpl 25 | *gotcp.Session 26 | Ctx *common.Context 27 | CacheRegisterMsg *protocol.MSG_MGR_REGISTER_SERVER 28 | derived ISessionDerived 29 | } 30 | 31 | // NewSessionBase : 网络会话类的构造函数 32 | func NewSessionBase(ctx *common.Context, derived ISessionDerived) *SessionBase { 33 | return &SessionBase{ 34 | Ctx: ctx, 35 | Session: &gotcp.Session{}, 36 | derived: derived, 37 | } 38 | } 39 | 40 | // OnRecv : 接收到网络数据包,被触发 41 | func (sessbase *SessionBase) OnRecv(data []byte, flag byte) { 42 | cmd := gotcp.GetCmd(data) 43 | data = gotcp.GetData(data) 44 | if sessbase.IsVerified() == false && sessbase.doVerify(protocol.CMD_MGR_ENUM(cmd), data, flag) == false { 45 | return 46 | } 47 | switch protocol.CMD_MGR_ENUM(cmd) { 48 | case protocol.CMD_MGR_REGISTER_SERVER: 49 | sessbase.doRegister(data, flag) 50 | case protocol.CMD_MGR_LOSE_SERVER: 51 | sessbase.doLose(data, flag) 52 | case protocol.CMD_MGR_PING: 53 | // No need to do anything 54 | default: 55 | if sessbase.derived.DoRecv(cmd, data, flag) == false { 56 | sessbase.Ctx.Errorln("Unknown message number, message number is", cmd) 57 | } 58 | } 59 | } 60 | 61 | // OnClose : 断开连接,被触发 62 | func (sessbase *SessionBase) OnClose() { 63 | sessbase.derived.DoClose(sessbase) 64 | } 65 | 66 | func (sessbase *SessionBase) doVerify(cmd protocol.CMD_MGR_ENUM, data []byte, flag byte) bool { 67 | if cmd == protocol.CMD_MGR_REGISTER_SERVER { 68 | msg := &protocol.MSG_MGR_REGISTER_SERVER{} 69 | if gotcp.Decode(data, flag, msg) == nil { 70 | sessbase.Ctx.Errorln("Message parsing failed, message number is`protocol.CMD_MGR_REGISTER_SERVER`(", int(protocol.CMD_MGR_REGISTER_SERVER), ")") 71 | sessbase.Close() 72 | return false 73 | } 74 | if msg.GetToken() != sessbase.Ctx.Config().Common.IntranetToken { 75 | sessbase.Ctx.Errorln("Token verification failed.", 76 | "Msg token:", msg.GetToken(), 77 | "Expect token:", sessbase.Ctx.Config().Common.IntranetToken) 78 | sessbase.Close() 79 | return false 80 | } 81 | if msg.GetTargetServerType() != uint32(misc.GetPluginType(sessbase.Ctx)) { 82 | sessbase.Ctx.Errorln("Target server type verification failed.", 83 | "Msg target server type:", msg.GetTargetServerType(), 84 | "Expect target server type:", sessbase.GetType()) 85 | sessbase.Close() 86 | return false 87 | } 88 | if msg.GetTargetServerID().GetID() != 0 && msg.GetTargetServerID().GetID() != misc.GetPluginID(sessbase.Ctx) { 89 | sessbase.Ctx.Errorln("Target server id verification failed.", 90 | "Msg target server id:", msg.GetTargetServerID().GetID(), 91 | "Expect target server id:", sessbase.GetID()) 92 | sessbase.Close() 93 | return false 94 | } 95 | sessbase.derived.DoVerify(msg) 96 | sessbase.Verify() 97 | return true 98 | } 99 | sessbase.Ctx.Errorln("The expected message number is `protocol.CMD_MGR_REGISTER_SERVER`(", int(protocol.CMD_MGR_REGISTER_SERVER), "), but", cmd, "(", int(cmd), ")") 100 | sessbase.Close() 101 | return false 102 | } 103 | 104 | func (sessbase *SessionBase) doRegister(data []byte, flag byte) { 105 | msg := &protocol.MSG_MGR_REGISTER_SERVER{} 106 | if gotcp.Decode(data, flag, msg) == nil { 107 | sessbase.Ctx.Errorln("Message parsing failed, message number is`protocol.CMD_MGR_REGISTER_SERVER`(", int(protocol.CMD_MGR_REGISTER_SERVER), ")") 108 | sessbase.Close() 109 | return 110 | } 111 | sessbase.derived.DoRegister(msg) 112 | } 113 | 114 | func (sessbase *SessionBase) doLose(data []byte, flag byte) { 115 | msg := &protocol.MSG_MGR_LOSE_SERVER{} 116 | if gotcp.Decode(data, flag, msg) == nil { 117 | sessbase.Ctx.Errorln("Message parsing failed, message number is`protocol.CMD_MGR_LOSE_SERVER`(", int(protocol.CMD_MGR_LOSE_SERVER), ")") 118 | return 119 | } 120 | sessbase.derived.DoLose(msg) 121 | } 122 | 123 | // RegisterSelf : 注册自己 124 | func (sessbase *SessionBase) RegisterSelf(id context.NodeID, selfType config.NodeType, targetServerType config.NodeType, targetServerID *protocol.SERVER_ID) { 125 | msg := &protocol.MSG_MGR_REGISTER_SERVER{} 126 | msg.Data = &protocol.SERVER_INFO{} 127 | msg.Data.Id = NodeID2ServerID(id) 128 | msg.Data.Type = uint32(selfType) 129 | msg.Data.Addrs = []string{utils.GetIPInner(sessbase.Ctx), utils.GetIPOuter(sessbase.Ctx)} 130 | msg.Data.Ports = sessbase.Ctx.Config().Network.Port 131 | 132 | // TODO: 后续支持 133 | // msg.Data.Overload 134 | // msg.Data.Version 135 | 136 | msg.Token = sessbase.Ctx.Config().Common.IntranetToken 137 | msg.TargetServerType = uint32(targetServerType) 138 | msg.TargetServerID = targetServerID 139 | sessbase.Info = msg.GetData() 140 | sessbase.SendMsg(uint64(protocol.CMD_MGR_REGISTER_SERVER), msg) 141 | 142 | if targetServerType == config.Mgr { 143 | sessbase.Ctx.Infoln("Register your information with the management server, info:", msg.GetData()) 144 | } else if targetServerType == config.Gateway { 145 | sessbase.Ctx.Infoln("Register your information with the gateway server, info:", msg.GetData()) 146 | } else { 147 | sessbase.Ctx.Errorln("Register your information with the server(", targetServerType, "), info:", msg.GetData()) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /internal/components/node/common/session_mgr.go: -------------------------------------------------------------------------------- 1 | package nodecommon 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/fananchong/go-xserver/common" 7 | "github.com/fananchong/go-xserver/common/config" 8 | "github.com/fananchong/go-xserver/common/context" 9 | "github.com/fananchong/go-xserver/internal/protocol" 10 | ) 11 | 12 | // SessionMgr : 网络会话对象管理类 13 | type SessionMgr struct { 14 | ctx *common.Context 15 | m sync.RWMutex 16 | ss map[config.NodeType][]*SessionBase 17 | ssByID map[context.NodeID]*SessionBase 18 | counter map[int]uint32 19 | } 20 | 21 | // NewSessionMgr : 实例化网络会话对象管理类 22 | func NewSessionMgr(ctx *common.Context) *SessionMgr { 23 | sessMgr := &SessionMgr{ 24 | ctx: ctx, 25 | ss: make(map[config.NodeType][]*SessionBase), 26 | ssByID: make(map[context.NodeID]*SessionBase), 27 | counter: make(map[int]uint32), 28 | } 29 | return sessMgr 30 | } 31 | 32 | // Register : 有网络会话节点加入 33 | func (sessmgr *SessionMgr) Register(sess *SessionBase) { 34 | t := sess.GetType() 35 | sid := sess.GetSID() 36 | sessmgr.m.Lock() 37 | defer sessmgr.m.Unlock() 38 | for sessmgr.deleteSessInSS(sid, t) { 39 | // No need to do anything 40 | } 41 | sessmgr.ss[t] = append(sessmgr.ss[t], sess) 42 | sessmgr.ssByID[sess.GetID()] = sess 43 | } 44 | 45 | // Lose1 : 丢失网络会话节点 46 | func (sessmgr *SessionMgr) Lose1(sess *SessionBase) { 47 | t := sess.GetType() 48 | sid := sess.GetSID() 49 | sessmgr.Lose2(sid, t) 50 | } 51 | 52 | // Lose2 : 丢失网络会话节点 53 | func (sessmgr *SessionMgr) Lose2(sid *protocol.SERVER_ID, t config.NodeType) { 54 | sessmgr.m.Lock() 55 | defer sessmgr.m.Unlock() 56 | for sessmgr.deleteSessInSS(sid, t) { 57 | // No need to do anything 58 | } 59 | delete(sessmgr.ssByID, ServerID2NodeID(sid)) 60 | } 61 | 62 | func (sessmgr *SessionMgr) deleteSessInSS(sid *protocol.SERVER_ID, t config.NodeType) bool { 63 | // 不用加锁,调用它的函数会加锁 64 | if lst, ok := sessmgr.ss[t]; ok { 65 | findindex := -1 66 | for i, v := range lst { 67 | if EqualSID(v.GetSID(), sid) { 68 | findindex = i 69 | break 70 | } 71 | } 72 | if findindex >= 0 { 73 | sessmgr.ss[t] = append(lst[:findindex], lst[findindex+1:]...) 74 | return true 75 | } 76 | } 77 | return false 78 | } 79 | 80 | // GetByID : 根据 NID 获取网络会话节点 81 | func (sessmgr *SessionMgr) GetByID(nid context.NodeID) *SessionBase { 82 | sessmgr.m.RLock() 83 | defer sessmgr.m.RUnlock() 84 | if v, ok := sessmgr.ssByID[nid]; ok { 85 | return v 86 | } 87 | return nil 88 | } 89 | 90 | // GetByType : 根据节点类型,获取某类网络会话节点列表 91 | func (sessmgr *SessionMgr) GetByType(t config.NodeType) []*SessionBase { 92 | sessmgr.m.RLock() 93 | defer sessmgr.m.RUnlock() 94 | var ret []*SessionBase 95 | if lst, ok := sessmgr.ss[t]; ok { 96 | ret = append(ret, lst...) 97 | } 98 | return ret 99 | } 100 | 101 | // GetAll : 获取所有网络会话节点列表 102 | func (sessmgr *SessionMgr) GetAll() []*SessionBase { 103 | sessmgr.m.RLock() 104 | defer sessmgr.m.RUnlock() 105 | var ret []*SessionBase 106 | for _, lst := range sessmgr.ss { 107 | ret = append(ret, lst...) 108 | } 109 | return ret 110 | } 111 | 112 | // SelectOne : 选择 1 个某类型的网络会话节点 113 | func (sessmgr *SessionMgr) SelectOne(t config.NodeType) *SessionBase { 114 | lst := sessmgr.GetByType(t) 115 | n := len(lst) 116 | if n > 0 { 117 | if _, ok := sessmgr.counter[int(t)]; !ok { 118 | sessmgr.counter[int(t)] = 0 119 | } 120 | index := int32(sessmgr.counter[int(t)] % uint32(n)) 121 | sessmgr.counter[int(t)]++ 122 | return lst[index] 123 | } 124 | return nil 125 | } 126 | 127 | // ForByType : 根据类型遍历网络会话节点 128 | func (sessmgr *SessionMgr) ForByType(t config.NodeType, f func(*SessionBase)) { 129 | lst := sessmgr.GetByType(t) 130 | for _, v := range lst { 131 | f(v) 132 | } 133 | } 134 | 135 | // ForAll : 遍历所有网络会话节点 136 | func (sessmgr *SessionMgr) ForAll(f func(*SessionBase)) { 137 | lst := sessmgr.GetAll() 138 | for _, v := range lst { 139 | f(v) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /internal/components/node/common/userdata.go: -------------------------------------------------------------------------------- 1 | package nodecommon 2 | 3 | import ( 4 | "github.com/fananchong/go-xserver/common" 5 | ) 6 | 7 | // UserData : 传递给 Session 的数据 8 | type UserData struct { 9 | Ctx *common.Context 10 | SessMgr *SessionMgr 11 | } 12 | -------------------------------------------------------------------------------- /internal/components/node/common/uuid_helper.go: -------------------------------------------------------------------------------- 1 | package nodecommon 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/fananchong/go-xserver/common" 8 | "github.com/fananchong/go-xserver/common/config" 9 | "github.com/fananchong/go-xserver/common/context" 10 | "github.com/fananchong/go-xserver/internal/protocol" 11 | ) 12 | 13 | // NewNID : 生成一个NID 14 | func NewNID(ctx *common.Context, t config.NodeType) context.NodeID { 15 | key := fmt.Sprintf("NODEID%d", t) 16 | id, err := ctx.GetUID(key) 17 | if err != nil { 18 | ctx.Errorln(err) 19 | os.Exit(1) 20 | } 21 | return context.NodeID(uint32(t)*10000 + uint32(id)%10000) 22 | } 23 | 24 | // NodeID2ServerID : context.NodeID 转化为 protocol.SERVER_ID 25 | func NodeID2ServerID(nid context.NodeID) *protocol.SERVER_ID { 26 | sid := &protocol.SERVER_ID{ 27 | ID: uint32(nid), 28 | } 29 | return sid 30 | } 31 | 32 | // ServerID2NodeID : protocol.SERVER_ID 转化为 context.NodeID 33 | func ServerID2NodeID(sid *protocol.SERVER_ID) context.NodeID { 34 | return context.NodeID(sid.ID) 35 | } 36 | 37 | // EqualSID : 2 个 SID 是否相等 38 | func EqualSID(sid1 *protocol.SERVER_ID, sid2 *protocol.SERVER_ID) bool { 39 | return sid1.GetID() == sid2.GetID() 40 | } 41 | 42 | // EqualNID : 2 个 NID 是否相等 43 | func EqualNID(nid1 context.NodeID, nid2 context.NodeID) bool { 44 | return uint32(nid1) == uint32(nid2) 45 | } 46 | -------------------------------------------------------------------------------- /internal/components/node/gateway/gateway.go: -------------------------------------------------------------------------------- 1 | package nodegateway 2 | 3 | import ( 4 | go_redis_orm "github.com/fananchong/go-redis-orm.v2" 5 | "github.com/fananchong/go-xserver/common" 6 | "github.com/fananchong/go-xserver/common/config" 7 | "github.com/fananchong/go-xserver/common/context" 8 | "github.com/fananchong/go-xserver/internal/components/misc" 9 | nodecommon "github.com/fananchong/go-xserver/internal/components/node/common" 10 | "github.com/fananchong/go-xserver/internal/db" 11 | "github.com/fananchong/go-xserver/internal/protocol" 12 | "github.com/fananchong/go-xserver/internal/utils" 13 | ) 14 | 15 | // Gateway : 网关节点 16 | type Gateway struct { 17 | *nodecommon.Node 18 | ctx *common.Context 19 | funcSendToClient context.FuncTypeSendToClient 20 | funcSendToAllClient context.FuncTypeSendToAllClient 21 | funcEncodeFunc context.FuncTypeEncode 22 | funcDecodeFunc context.FuncTypeDecode 23 | users *UserMgr 24 | } 25 | 26 | // NewGateway : 网关节点实现类的构造函数 27 | func NewGateway(ctx *common.Context) *Gateway { 28 | gw := &Gateway{ 29 | ctx: ctx, 30 | Node: nodecommon.NewNode(ctx, config.Gateway), 31 | } 32 | gw.ctx.IGateway = gw 33 | gw.users = NewUserMgr(ctx, gw) 34 | return gw 35 | } 36 | 37 | // Start : 启动 38 | func (gw *Gateway) Start() bool { 39 | if misc.GetPluginType(gw.ctx) == config.Gateway { 40 | if gw.initRedis() == false { 41 | return false 42 | } 43 | if gw.Node.Init(Session{}, []utils.IComponent{}) == false { 44 | return false 45 | } 46 | if gw.Node.Start() == false { 47 | return false 48 | } 49 | gw.users.Start() 50 | } 51 | return true 52 | } 53 | 54 | // Close : 关闭 55 | func (gw *Gateway) Close() { 56 | if gw.Node != nil { 57 | gw.Node.Close() 58 | gw.users.Close() 59 | gw.Node = nil 60 | } 61 | } 62 | 63 | // VerifyToken : 令牌验证。返回值: 0 成功;1 令牌错误; 2 系统错误 64 | func (gw *Gateway) VerifyToken(account, token string, clientSession context.IClientSesion) uint32 { 65 | tokenObj := db.NewToken(gw.ctx.Config().DbToken.Name, account) 66 | if err := tokenObj.Load(); err != nil { 67 | gw.ctx.Errorln(err, "account:", account) 68 | return 2 69 | } 70 | tmpTokenObj := tokenObj.GetToken(false) 71 | if token != tmpTokenObj.Token { 72 | gw.ctx.Errorf("Token verification failed, expecting token to be %s, but %s. account: %s\n", tmpTokenObj.Token, token, account) 73 | return 1 74 | } 75 | gw.users.AddUser(account, tmpTokenObj.GetAllocServers(), clientSession) 76 | return 0 77 | } 78 | 79 | // OnRecvFromClient : 可自定义客户端交互协议。data 格式需转化为框架层可理解的格式。done 为 true ,表示框架层接管处理该消息 80 | func (gw *Gateway) OnRecvFromClient(account string, cmd uint32, data []byte, flag uint8) (done bool) { 81 | nodeType := config.NodeType(cmd / uint32(gw.ctx.Config().Common.MsgCmdOffset)) 82 | if nodeType <= config.Gateway { 83 | gw.ctx.Errorln("Wrong message number. cmd:", cmd, "account:", account) 84 | return 85 | } 86 | 87 | // 是否需要状态中继 88 | nodeID, err := gw.users.GetServerAndActive(account, nodeType) 89 | if err != nil { 90 | gw.ctx.Errorln(err, "account:", account, "cmd:", cmd) 91 | return 92 | } 93 | var target *nodecommon.SessionBase 94 | if nodeID != nil { 95 | target = gw.GetNode(*nodeID) 96 | } else { 97 | target = gw.GetNodeOne(nodeType) 98 | } 99 | if target == nil { 100 | gw.ctx.Errorln("Target server not found. cmd:", cmd, "account:", account, "nodeType", nodeType) 101 | return 102 | } 103 | 104 | // Gateway 接管该消息,并开始中继 105 | done = true 106 | 107 | msg := &protocol.MSG_GW_RELAY_CLIENT_MSG{} 108 | msg.Account = account 109 | msg.CMD = cmd % uint32(gw.ctx.Config().Common.MsgCmdOffset) 110 | msg.Data = append(msg.Data, data...) 111 | msg.Flag = uint32(flag) 112 | if target.SendMsg(uint64(protocol.CMD_GW_RELAY_CLIENT_MSG), msg) == false { 113 | gw.ctx.Errorln("Sending a message to the target server failed. cmd:", cmd, "account:", account, "nodeType", nodeType) 114 | return 115 | } 116 | return 117 | } 118 | 119 | // RegisterSendToClient : 可自定义客户端交互协议 120 | func (gw *Gateway) RegisterSendToClient(f context.FuncTypeSendToClient) { 121 | gw.funcSendToClient = f 122 | } 123 | 124 | // GetSendToClient : 可自定义客户端交互协议 125 | func (gw *Gateway) GetSendToClient() context.FuncTypeSendToClient { 126 | return gw.funcSendToClient 127 | } 128 | 129 | // RegisterSendToAllClient : 可自定义客户端交互协议 130 | func (gw *Gateway) RegisterSendToAllClient(f context.FuncTypeSendToAllClient) { 131 | gw.funcSendToAllClient = f 132 | } 133 | 134 | // GetSendToAllClient : 可自定义客户端交互协议 135 | func (gw *Gateway) GetSendToAllClient() context.FuncTypeSendToAllClient { 136 | return gw.funcSendToAllClient 137 | } 138 | 139 | // RegisterEncodeFunc : 可自定义加解密算法 140 | func (gw *Gateway) RegisterEncodeFunc(f context.FuncTypeEncode) { 141 | gw.funcEncodeFunc = f 142 | } 143 | 144 | // RegisterDecodeFunc : 可自定义加解密算法 145 | func (gw *Gateway) RegisterDecodeFunc(f context.FuncTypeDecode) { 146 | gw.funcDecodeFunc = f 147 | } 148 | 149 | func (gw *Gateway) initRedis() bool { 150 | // db token 151 | err := go_redis_orm.CreateDB( 152 | gw.ctx.Config().DbToken.Name, 153 | gw.ctx.Config().DbToken.Addrs, 154 | gw.ctx.Config().DbToken.Password, 155 | gw.ctx.Config().DbToken.DBIndex) 156 | if err != nil { 157 | gw.ctx.Errorln(err) 158 | return false 159 | } 160 | 161 | // db server 162 | err = go_redis_orm.CreateDB( 163 | gw.ctx.Config().DbServer.Name, 164 | gw.ctx.Config().DbServer.Addrs, 165 | gw.ctx.Config().DbServer.Password, 166 | gw.ctx.Config().DbServer.DBIndex) 167 | if err != nil { 168 | gw.ctx.Errorln(err) 169 | return false 170 | } 171 | gw.users.ServerRedisCli = go_redis_orm.GetDB(gw.ctx.Config().DbServer.Name) 172 | return true 173 | } 174 | -------------------------------------------------------------------------------- /internal/components/node/gateway/session.go: -------------------------------------------------------------------------------- 1 | package nodegateway 2 | 3 | import ( 4 | gocontext "context" 5 | "net" 6 | 7 | "github.com/fananchong/go-xserver/common/config" 8 | "github.com/fananchong/go-xserver/common/context" 9 | nodecommon "github.com/fananchong/go-xserver/internal/components/node/common" 10 | "github.com/fananchong/go-xserver/internal/protocol" 11 | "github.com/fananchong/gotcp" 12 | ) 13 | 14 | // Session : 网络会话类 15 | type Session struct { 16 | *nodecommon.SessionBase 17 | funcSendToClient context.FuncTypeSendToClient 18 | funcSendToAllClient context.FuncTypeSendToAllClient 19 | } 20 | 21 | // Init : 初始化网络会话节点 22 | func (sess *Session) Init(root gocontext.Context, conn net.Conn, derived gotcp.ISession, userdata interface{}) { 23 | ud := userdata.(*nodecommon.UserData) 24 | sess.SessionBase = nodecommon.NewSessionBase(ud.Ctx, sess) 25 | sess.SessionBase.Init(root, conn, derived) 26 | sess.SessMgr = ud.SessMgr 27 | sess.funcSendToClient = ud.Ctx.IGateway.(*Gateway).GetSendToClient() 28 | sess.funcSendToAllClient = ud.Ctx.IGateway.(*Gateway).GetSendToAllClient() 29 | } 30 | 31 | // DoVerify : 验证时保存自己的注册消息 32 | func (sess *Session) DoVerify(msg *protocol.MSG_MGR_REGISTER_SERVER) { 33 | sess.Info = msg.GetData() 34 | } 35 | 36 | // DoRegister : 某节点注册时处理 37 | func (sess *Session) DoRegister(msg *protocol.MSG_MGR_REGISTER_SERVER) { 38 | if nodecommon.EqualSID(sess.Info.GetId(), msg.GetData().GetId()) == false { 39 | sess.Ctx.Errorln("Service ID is different.") 40 | sess.Ctx.Errorln("sess.Info.GetId() :", sess.Info.GetId().GetID()) 41 | sess.Ctx.Errorln("msg.GetData().GetId() :", msg.GetData().GetId().GetID()) 42 | sess.Close() 43 | return 44 | } 45 | if msg.GetTargetServerType() != uint32(config.Gateway) { 46 | sess.Ctx.Errorln("Target server type different. Expectation is config.Gateway, but it is", msg.GetTargetServerType()) 47 | sess.Close() 48 | return 49 | } 50 | sess.Info = msg.GetData() 51 | sess.SessMgr.Register(sess.SessionBase) 52 | sess.Ctx.Infoln("The service node registers with me, the node ID is", msg.GetData().GetId().GetID()) 53 | sess.Ctx.Infoln(sess.Info) 54 | } 55 | 56 | // DoLose : 节点丢失时处理 57 | func (sess *Session) DoLose(msg *protocol.MSG_MGR_LOSE_SERVER) { 58 | } 59 | 60 | // DoClose : 节点关闭时处理 61 | func (sess *Session) DoClose(sessbase *nodecommon.SessionBase) { 62 | if sess.SessionBase == sessbase && sessbase.Info != nil { 63 | sess.SessMgr.Lose1(sessbase) 64 | sess.Ctx.Infoln("Service node loses connection, type:", sess.Info.GetType(), "id:", sess.Info.GetId().GetID()) 65 | } 66 | } 67 | 68 | // DoRecv : 节点收到消息处理 69 | func (sess *Session) DoRecv(cmd uint64, data []byte, flag byte) (done bool) { 70 | done = true 71 | switch protocol.CMD_GW_ENUM(cmd) { 72 | case protocol.CMD_GW_RELAY_CLIENT_MSG: 73 | msg := &protocol.MSG_GW_RELAY_CLIENT_MSG{} 74 | if gotcp.Decode(data, flag, msg) == nil { 75 | sess.Ctx.Errorln("Message parsing failed, message number is`protocol.CMD_GW_RELAY_CLIENT_MSG`(", int(protocol.CMD_GW_RELAY_CLIENT_MSG), ")") 76 | done = false 77 | return 78 | } 79 | targetAccount := msg.GetAccount() 80 | if targetAccount != "" { 81 | sess.funcSendToClient(targetAccount, 82 | uint64(msg.GetCMD())+uint64(sess.Info.GetType())*uint64(sess.Ctx.Config().Common.MsgCmdOffset), 83 | msg.GetData(), uint8(msg.GetFlag())) 84 | } else { 85 | sess.funcSendToAllClient( 86 | uint64(msg.GetCMD())+uint64(sess.Info.GetType())*uint64(sess.Ctx.Config().Common.MsgCmdOffset), 87 | msg.GetData(), uint8(msg.GetFlag())) 88 | } 89 | case protocol.CMD_GW_RELAY_SERVER_MSG1: 90 | msg := &protocol.MSG_GW_RELAY_SERVER_MSG1{} 91 | if gotcp.Decode(data, flag, msg) == nil { 92 | sess.Ctx.Errorln("Message parsing failed, message number is`protocol.CMD_GW_RELAY_SERVER_MSG1`(", int(protocol.CMD_GW_RELAY_SERVER_MSG1), ")") 93 | done = false 94 | return 95 | } 96 | if msg.GetSendType() == protocol.RELAY_SERVER_MSG_TYPE_BROADCAST { 97 | sess.SessMgr.ForByType(config.NodeType(msg.GetTargetType()), func(targetSess *nodecommon.SessionBase) { 98 | targetSess.SendEx(int(cmd), data, flag) 99 | }) 100 | } else if msg.GetSendType() == protocol.RELAY_SERVER_MSG_TYPE_RANDOM { 101 | targetSess := sess.SessMgr.SelectOne(config.NodeType(msg.GetTargetType())) 102 | if targetSess != nil { 103 | targetSess.SendEx(int(cmd), data, flag) 104 | } else { 105 | sess.Ctx.Errorln("No find server.", "cmd:", msg.GetCMD(), "targetType:", msg.GetTargetType()) 106 | done = false 107 | return 108 | } 109 | } else { 110 | sess.Ctx.Errorln("Field 'send type' error, value:", msg.GetSendType(), "cmd:", msg.GetCMD(), "targetType:", msg.GetTargetType()) 111 | done = false 112 | return 113 | } 114 | case protocol.CMD_GW_RELAY_SERVER_MSG2: 115 | msg := &protocol.MSG_GW_RELAY_SERVER_MSG2{} 116 | if gotcp.Decode(data, flag, msg) == nil { 117 | sess.Ctx.Errorln("Message parsing failed, message number is`protocol.CMD_GW_RELAY_SERVER_MSG2`(", int(protocol.CMD_GW_RELAY_SERVER_MSG2), ")") 118 | done = false 119 | return 120 | } 121 | id := nodecommon.ServerID2NodeID(msg.GetTargetID()) 122 | targetSess := sess.SessMgr.GetByID(id) 123 | if targetSess != nil { 124 | targetSess.SendEx(int(cmd), data, flag) 125 | } else { 126 | sess.Ctx.Errorln("No find server.", "cmd:", msg.GetCMD(), "targetServerID:", id) 127 | done = false 128 | return 129 | } 130 | default: 131 | done = false 132 | } 133 | return 134 | } 135 | -------------------------------------------------------------------------------- /internal/components/node/gateway/user_mgr.go: -------------------------------------------------------------------------------- 1 | package nodegateway 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "time" 7 | 8 | go_redis_orm "github.com/fananchong/go-redis-orm.v2" 9 | "github.com/fananchong/go-xserver/common" 10 | "github.com/fananchong/go-xserver/common/config" 11 | "github.com/fananchong/go-xserver/common/context" 12 | nodecommon "github.com/fananchong/go-xserver/internal/components/node/common" 13 | "github.com/fananchong/go-xserver/internal/db" 14 | "github.com/fananchong/go-xserver/internal/protocol" 15 | "github.com/fananchong/go-xserver/internal/utils" 16 | ) 17 | 18 | // User : 表示 1 个客户端对象 19 | type User struct { 20 | Account string 21 | Servers map[uint32]context.NodeID 22 | ActiveTimestamp int64 23 | ClientSession context.IClientSesion 24 | } 25 | 26 | // NewUser : 客户端对象构造函数 27 | func NewUser(ctx *common.Context, account string, clientSession context.IClientSesion) *User { 28 | user := &User{ 29 | Account: account, 30 | Servers: make(map[uint32]context.NodeID), 31 | ActiveTimestamp: ctx.GetTickCount(), 32 | ClientSession: clientSession, 33 | } 34 | return user 35 | } 36 | 37 | // UserMgr : 客户端对象管理类 38 | type UserMgr struct { 39 | ctx *common.Context 40 | users map[string]*User 41 | mutex sync.RWMutex 42 | ServerRedisCli go_redis_orm.IClient 43 | checkActiveTicker *utils.Ticker 44 | myGateway *Gateway 45 | } 46 | 47 | // NewUserMgr : 客户端对象管理类构造函数 48 | func NewUserMgr(ctx *common.Context, myGateway *Gateway) *UserMgr { 49 | userMgr := &UserMgr{ 50 | ctx: ctx, 51 | users: make(map[string]*User), 52 | myGateway: myGateway, 53 | } 54 | userMgr.checkActiveTicker = utils.NewTickerHelper("CHECK_ACTIVE", ctx, 1*time.Second, userMgr.checkActive) 55 | return userMgr 56 | } 57 | 58 | // AddUser : 加入一个玩家 59 | func (userMgr *UserMgr) AddUser(account string, servers map[uint32]*protocol.SERVER_ID, clientSession context.IClientSesion) error { 60 | userMgr.mutex.Lock() 61 | defer userMgr.mutex.Unlock() 62 | user := NewUser(userMgr.ctx, account, clientSession) 63 | for nodeType, serverID := range servers { 64 | user.Servers[nodeType] = nodecommon.ServerID2NodeID(serverID) 65 | key := db.GetKeyAllocServer(nodeType, account) 66 | if _, err := userMgr.ServerRedisCli.Do("EXPIRE", key, 365*86400); err != nil { // 设置账号分配的服务器资源信息,过期时间 1 年 67 | return err 68 | } 69 | } 70 | msg := &protocol.MSG_GW_REGISTER_ACCOUNT{} 71 | msg.Account = account 72 | for nodeType, serverID := range user.Servers { 73 | if nodeType != uint32(config.Gateway) { 74 | if userMgr.myGateway.SendByID(serverID, uint64(protocol.CMD_GW_REGISTER_ACCOUNT), msg) == false { 75 | userMgr.ctx.Errorln("Sending a 'register account' message failed. account:", user.Account) 76 | } 77 | } 78 | } 79 | userMgr.users[account] = user 80 | return nil 81 | } 82 | 83 | // GetServerAndActive : 获取玩家对应服务器类型的服务器资源信息 84 | func (userMgr *UserMgr) GetServerAndActive(account string, nodeType config.NodeType) (*context.NodeID, error) { 85 | userMgr.mutex.RLock() 86 | defer userMgr.mutex.RUnlock() 87 | if user, ok := userMgr.users[account]; ok { 88 | user.ActiveTimestamp = userMgr.ctx.GetTickCount() 89 | if id, ok := user.Servers[uint32(nodeType)]; ok { 90 | return &id, nil 91 | } 92 | return nil, nil 93 | } 94 | return nil, errors.New("No server information corresponding to the account was found") 95 | } 96 | 97 | func (userMgr *UserMgr) checkActive() { 98 | userMgr.mutex.Lock() 99 | defer userMgr.mutex.Unlock() 100 | 101 | // 检查闲置连接 102 | now := userMgr.ctx.GetTickCount() 103 | var dels []*User 104 | for _, user := range userMgr.users { 105 | if now-user.ActiveTimestamp >= userMgr.ctx.Config().Role.IdleTime*1000 { 106 | dels = append(dels, user) 107 | } 108 | } 109 | 110 | // TODO: 删除操作现在是 1 条 1 条执行,会很慢,极端情况下,是卡玩家登录的。 111 | // 待优化为 REDIS PIPELINING 模式 112 | // 参考 : https://godoc.org/github.com/gomodule/redigo/redis#hdr-Pipelining ,类似: 113 | // c.Send("SET", "foo", "bar") 114 | // c.Send("GET", "foo") 115 | // c.Flush() 116 | // c.Receive() // reply from SET 117 | // v, err = c.Receive() // reply from GET 118 | 119 | // 删除闲置连接 120 | for _, user := range dels { 121 | msg := &protocol.MSG_GW_LOSE_ACCOUNT{} 122 | msg.Account = user.Account 123 | for nodeType, serverID := range user.Servers { 124 | key := db.GetKeyAllocServer(nodeType, user.Account) 125 | ttl := userMgr.ctx.Config().Role.SessionAffinityInterval 126 | if _, err := userMgr.ServerRedisCli.Do("EXPIRE", key, ttl); err != nil { // 设置账号分配的服务器资源信息,过期时间 5 分钟 127 | userMgr.ctx.Errorln(err, "account:", user.Account) 128 | } 129 | if nodeType != uint32(config.Gateway) { 130 | if userMgr.myGateway.SendByID(serverID, uint64(protocol.CMD_GW_LOSE_ACCOUNT), msg) == false { 131 | userMgr.ctx.Errorln("Sending a 'lost account' message failed. account:", user.Account) 132 | } 133 | } 134 | } 135 | delete(userMgr.users, user.Account) 136 | user.ClientSession.Close() 137 | userMgr.ctx.Infoln("Delete user information, account:", user.Account) 138 | } 139 | } 140 | 141 | // Start : 开始 142 | func (userMgr *UserMgr) Start() { 143 | userMgr.checkActiveTicker.Start() 144 | } 145 | 146 | // Close : 结束 147 | func (userMgr *UserMgr) Close() { 148 | userMgr.checkActiveTicker.Close() 149 | } 150 | -------------------------------------------------------------------------------- /internal/components/node/login/login.go: -------------------------------------------------------------------------------- 1 | package nodelogin 2 | 3 | import ( 4 | go_redis_orm "github.com/fananchong/go-redis-orm.v2" 5 | "github.com/fananchong/go-xserver/common" 6 | "github.com/fananchong/go-xserver/common/config" 7 | "github.com/fananchong/go-xserver/common/context" 8 | "github.com/fananchong/go-xserver/internal/components/misc" 9 | nodecommon "github.com/fananchong/go-xserver/internal/components/node/common" 10 | nodenormal "github.com/fananchong/go-xserver/internal/components/node/normal" 11 | "github.com/fananchong/go-xserver/internal/db" 12 | "github.com/fananchong/go-xserver/internal/protocol" 13 | "github.com/fananchong/go-xserver/internal/utils" 14 | "github.com/gomodule/redigo/redis" 15 | uuid "github.com/satori/go.uuid" 16 | ) 17 | 18 | // Login : 登陆模块 19 | type Login struct { 20 | *nodenormal.Normal 21 | ctx *common.Context 22 | verificationFunc context.FuncTypeAccountVerification 23 | allocServerType []config.NodeType 24 | serverRedis db.RedisAtomic 25 | } 26 | 27 | // NewLogin : 实例化登陆模块 28 | func NewLogin(ctx *common.Context) *Login { 29 | login := &Login{ 30 | ctx: ctx, 31 | } 32 | login.ctx.ILogin = login 33 | return login 34 | } 35 | 36 | // Start : 启动 37 | func (login *Login) Start() bool { 38 | pluginType := misc.GetPluginType(login.ctx) 39 | if pluginType == config.Login { 40 | login.Normal = login.ctx.INode.(*nodenormal.Normal) 41 | if !login.initRedis() { 42 | return false 43 | } 44 | } 45 | return true 46 | } 47 | 48 | // RegisterCustomAccountVerification : 注册自定义账号验证处理 49 | func (login *Login) RegisterCustomAccountVerification(f context.FuncTypeAccountVerification) { 50 | login.verificationFunc = f 51 | } 52 | 53 | // RegisterAllocationNodeType : 注册要分配的服务器资源类型 54 | func (login *Login) RegisterAllocationNodeType(types []config.NodeType) { 55 | login.allocServerType = append(login.allocServerType, types...) 56 | } 57 | 58 | // Login : 登陆处理 59 | func (login *Login) Login(account, password string, defaultMode bool, userdata []byte) (string, 60 | []string, []int32, []config.NodeType, []context.NodeID, context.LoginErrCode) { 61 | 62 | //账号验证 63 | var err context.LoginErrCode 64 | if defaultMode { 65 | err = login.loginByDefault(account, password) 66 | } else { 67 | err = login.verificationFunc(account, password, userdata) 68 | } 69 | if err != context.LoginSuccess { 70 | return "", nil, nil, nil, nil, err 71 | } 72 | 73 | // 分配服务资源列表 74 | addressList, portList, ids, ok := login.selectServerList(account, login.allocServerType) 75 | if !ok { 76 | return "", nil, nil, nil, nil, context.LoginSystemError 77 | } 78 | 79 | //生成 Token 80 | tempID := uuid.NewV4().String() 81 | tokenObj := db.NewToken(login.ctx.Config().DbToken.Name, account) 82 | tokenObj.Expire(7 * 86400) // 令牌过期时间 7 天 83 | to := tokenObj.GetToken(true) 84 | to.Token = tempID 85 | to.AllocServers = make(map[uint32]*protocol.SERVER_ID) 86 | for i := 0; i < len(login.allocServerType); i++ { 87 | to.AllocServers[uint32(login.allocServerType[i])] = ids[i] 88 | } 89 | if err := tokenObj.Save(); err != nil { 90 | login.ctx.Errorln(err, "account:", account) 91 | return "", nil, nil, nil, nil, context.LoginSystemError 92 | } 93 | var retIDs []context.NodeID 94 | for i := 0; i < len(login.allocServerType); i++ { 95 | retIDs = append(retIDs, context.NodeID(ids[i].GetID())) 96 | } 97 | return tempID, addressList, portList, login.allocServerType, retIDs, context.LoginSuccess 98 | } 99 | 100 | // Close : 关闭 101 | func (login *Login) Close() { 102 | if login.serverRedis.Cli != nil { 103 | login.serverRedis.Cli.Close() 104 | login.serverRedis.Cli = nil 105 | } 106 | } 107 | 108 | func (login *Login) loginByDefault(account, password string) context.LoginErrCode { 109 | accountObj := db.NewAccount(login.ctx.Config().DbAccount.Name, account) 110 | if err := accountObj.Load(); err != nil { 111 | // 新建账号 112 | if err != go_redis_orm.ERR_ISNOT_EXIST_KEY { 113 | login.ctx.Errorln(err, "account:", account) 114 | return context.LoginSystemError 115 | } 116 | accountObj.SetPasswd(password) 117 | if err = accountObj.Save(); err != nil { 118 | login.ctx.Errorln(err, "account:", account) 119 | return context.LoginSystemError 120 | } 121 | } else { 122 | // 验证密码 123 | if accountObj.GetPasswd() != password { 124 | return context.LoginVerifyFail 125 | } 126 | } 127 | return context.LoginSuccess 128 | } 129 | 130 | func (login *Login) initRedis() bool { 131 | // db token 132 | err := go_redis_orm.CreateDB( 133 | login.ctx.Config().DbToken.Name, 134 | login.ctx.Config().DbToken.Addrs, 135 | login.ctx.Config().DbToken.Password, 136 | login.ctx.Config().DbToken.DBIndex) 137 | if err != nil { 138 | login.ctx.Errorln(err) 139 | return false 140 | } 141 | 142 | // db server 143 | c, err := redis.Dial("tcp", login.ctx.Config().DbServer.Addrs[0]) 144 | if err != nil { 145 | login.ctx.Errorln(err) 146 | return false 147 | } 148 | if login.ctx.Config().DbServer.Password != "" { 149 | if _, err := c.Do("AUTH", login.ctx.Config().DbServer.Password); err != nil { 150 | login.ctx.Errorln(err) 151 | c.Close() 152 | return false 153 | } 154 | } 155 | if login.ctx.Config().DbServer.DBIndex != 0 { 156 | if _, err := c.Do("SELECT", login.ctx.Config().DbServer.DBIndex); err != nil { 157 | login.ctx.Errorln(err) 158 | c.Close() 159 | return false 160 | } 161 | } 162 | login.serverRedis.Cli = c 163 | return true 164 | } 165 | 166 | func (login *Login) selectServerList(account string, nodeType []config.NodeType) (addressList []string, portList []int32, serverIDs []*protocol.SERVER_ID, ok bool) { 167 | for i := 0; i < len(nodeType); i++ { 168 | dbObj, have := login.selectServer(account, nodeType[i]) 169 | if !have { 170 | return 171 | } 172 | addressList = append(addressList, dbObj.Address) 173 | portList = append(portList, dbObj.Port) 174 | serverIDs = append(serverIDs, dbObj.ServerID) 175 | } 176 | ok = true 177 | return 178 | } 179 | 180 | func (login *Login) selectServer(account string, nodeType config.NodeType) (dbObj *db.AccountServer, ok bool) { 181 | LOOP: 182 | dbObj = &db.AccountServer{} 183 | login.PrintNodeInfo(login.ctx, nodeType) 184 | node := login.GetNodeOne(nodeType) 185 | if node == nil { 186 | login.ctx.Errorln("Did not find the server. type:", nodeType, "account:", account) 187 | return 188 | } 189 | nodeID := node.GetID() 190 | dbObj.ServerID = nodecommon.NodeID2ServerID(nodeID) 191 | dbObj.Type = nodeType 192 | dbObj.Address = node.GetIP(utils.IPOUTER) 193 | dbObj.Port = node.GetPort(int(utils.PORTFORCLIENT)) 194 | 195 | var data string 196 | var err error 197 | data, err = dbObj.Marshal() 198 | if err != nil { 199 | login.ctx.Errorln(err, "account:", account) 200 | return 201 | } 202 | login.ctx.Infoln("account:", account, "server:", data) 203 | var ret string 204 | key := db.GetKeyAllocServer(uint32(nodeType), account) 205 | ret, err = login.serverRedis.SetGetX(key, data, 365*86400) // 设置账号分配的服务器资源信息,过期时间 1 年 206 | if err != nil { 207 | login.ctx.Errorln(err, "account:", account) 208 | return 209 | } 210 | if ret != "" { 211 | dbObj.Unmarshal(ret) 212 | if login.HaveNode(nodecommon.ServerID2NodeID(dbObj.ServerID)) == false { 213 | if _, err = login.serverRedis.DelX(key, ret); err != nil { 214 | login.ctx.Errorln(err, "account:", account) 215 | return 216 | } 217 | login.ctx.Infoln("Try again to get one server, type:", nodeType, "account:", account) 218 | goto LOOP 219 | } 220 | } 221 | ok = true 222 | return 223 | } 224 | -------------------------------------------------------------------------------- /internal/components/node/mgr/mgr.go: -------------------------------------------------------------------------------- 1 | package nodemgr 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/fananchong/go-xserver/common" 8 | "github.com/fananchong/go-xserver/common/config" 9 | "github.com/fananchong/go-xserver/internal/components/misc" 10 | nodecommon "github.com/fananchong/go-xserver/internal/components/node/common" 11 | "github.com/fananchong/go-xserver/internal/db" 12 | "github.com/fananchong/go-xserver/internal/utils" 13 | ) 14 | 15 | // Mgr : 管理节点 16 | type Mgr struct { 17 | *nodecommon.Node 18 | ctx *common.Context 19 | } 20 | 21 | // NewMgr : 管理节点实现类的构造函数 22 | func NewMgr(ctx *common.Context) *Mgr { 23 | mgr := &Mgr{ 24 | ctx: ctx, 25 | Node: nodecommon.NewNode(ctx, config.Mgr), 26 | } 27 | pluginType := misc.GetPluginType(mgr.ctx) 28 | if pluginType == config.Mgr { 29 | mgr.init() 30 | mgr.ctx.INode = mgr 31 | } 32 | return mgr 33 | } 34 | 35 | // Start : 实例化组件 36 | func (mgr *Mgr) Start() bool { 37 | pluginType := misc.GetPluginType(mgr.ctx) 38 | if pluginType == config.Mgr { 39 | if mgr.Node.Start() == false { 40 | mgr.ctx.Errorln("Mgr Server node failed to start") 41 | os.Exit(1) 42 | } 43 | } 44 | return true 45 | } 46 | 47 | // Close : 关闭组件 48 | func (mgr *Mgr) Close() { 49 | if mgr.Node != nil { 50 | mgr.Node.Close() 51 | mgr.Node = nil 52 | } 53 | } 54 | 55 | func (mgr *Mgr) init() bool { 56 | // register ticker 57 | registerTicker := utils.NewTickerHelper("REGISTER", mgr.ctx, 1*time.Second, mgr.register) 58 | return mgr.Node.Init(Session{}, []utils.IComponent{registerTicker}) 59 | } 60 | 61 | func (mgr *Mgr) register() { 62 | data := db.NewMgrServer(mgr.ctx.Config().DbMgr.Name, 0) 63 | data.SetAddr(utils.GetIPInner(mgr.ctx)) 64 | data.SetPort(utils.GetIntranetListenPort(mgr.ctx)) 65 | if err := data.Save(); err != nil { 66 | mgr.ctx.Errorln(err) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/components/node/mgr/session.go: -------------------------------------------------------------------------------- 1 | package nodemgr 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | "github.com/fananchong/go-xserver/common/config" 8 | nodecommon "github.com/fananchong/go-xserver/internal/components/node/common" 9 | "github.com/fananchong/go-xserver/internal/protocol" 10 | "github.com/fananchong/gotcp" 11 | ) 12 | 13 | // Session : 网络会话类 14 | type Session struct { 15 | *nodecommon.SessionBase 16 | } 17 | 18 | // Init : 初始化网络会话节点 19 | func (sess *Session) Init(root context.Context, conn net.Conn, derived gotcp.ISession, userdata interface{}) { 20 | ud := userdata.(*nodecommon.UserData) 21 | sess.SessionBase = nodecommon.NewSessionBase(ud.Ctx, sess) 22 | sess.SessionBase.Init(root, conn, derived) 23 | sess.SessMgr = ud.SessMgr 24 | } 25 | 26 | // DoVerify : 验证时保存自己的注册消息 27 | func (sess *Session) DoVerify(msg *protocol.MSG_MGR_REGISTER_SERVER) { 28 | sess.Info = msg.GetData() 29 | sess.CacheRegisterMsg = msg 30 | } 31 | 32 | // DoRegister : 某节点注册时处理 33 | func (sess *Session) DoRegister(msg *protocol.MSG_MGR_REGISTER_SERVER) { 34 | if nodecommon.EqualSID(sess.Info.GetId(), msg.GetData().GetId()) == false { 35 | sess.Close() 36 | return 37 | } 38 | if msg.GetTargetServerType() != uint32(config.Mgr) { 39 | sess.Close() 40 | return 41 | } 42 | sess.Info = msg.GetData() 43 | sess.Ctx.Infoln("The service node registers with me, the node ID is ", msg.GetData().GetId().GetID()) 44 | sess.Ctx.Infoln(sess.Info) 45 | 46 | sess.SessMgr.Register(sess.SessionBase) 47 | sess.SessMgr.ForAll(func(elem *nodecommon.SessionBase) { 48 | if elem != sess.SessionBase { 49 | sess.CacheRegisterMsg.TargetServerType = uint32(elem.GetType()) 50 | sess.CacheRegisterMsg.TargetServerID = nodecommon.NodeID2ServerID(elem.GetID()) 51 | elem.SendMsg(uint64(protocol.CMD_MGR_REGISTER_SERVER), sess.CacheRegisterMsg) 52 | } 53 | }) 54 | sess.SessMgr.ForAll(func(elem *nodecommon.SessionBase) { 55 | if elem != sess.SessionBase { 56 | elem.CacheRegisterMsg.TargetServerType = uint32(sess.GetType()) 57 | elem.CacheRegisterMsg.TargetServerID = nodecommon.NodeID2ServerID(sess.GetID()) 58 | sess.SendMsg(uint64(protocol.CMD_MGR_REGISTER_SERVER), elem.CacheRegisterMsg) 59 | } 60 | }) 61 | } 62 | 63 | // DoLose : 节点丢失时处理 64 | func (sess *Session) DoLose(msg *protocol.MSG_MGR_LOSE_SERVER) { 65 | } 66 | 67 | // DoClose : 节点关闭时处理 68 | func (sess *Session) DoClose(sessbase *nodecommon.SessionBase) { 69 | if sess.SessionBase == sessbase && sessbase.Info != nil { 70 | sess.SessMgr.Lose1(sessbase) 71 | msg := &protocol.MSG_MGR_LOSE_SERVER{} 72 | msg.Id = sess.Info.GetId() 73 | msg.Type = sess.Info.GetType() 74 | sess.SessMgr.ForAll(func(elem *nodecommon.SessionBase) { 75 | elem.SendMsg(uint64(protocol.CMD_MGR_LOSE_SERVER), msg) 76 | }) 77 | sess.Ctx.Infoln("Service node loses connection, type:", msg.Type, "id:", msg.Id.GetID()) 78 | } 79 | } 80 | 81 | // DoRecv : 节点收到消息处理 82 | func (sess *Session) DoRecv(cmd uint64, data []byte, flag byte) (done bool) { 83 | return 84 | } 85 | -------------------------------------------------------------------------------- /internal/components/node/normal/intranet_session.go: -------------------------------------------------------------------------------- 1 | package nodenormal 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/fananchong/go-xserver/common" 8 | "github.com/fananchong/go-xserver/common/config" 9 | "github.com/fananchong/go-xserver/common/context" 10 | nodecommon "github.com/fananchong/go-xserver/internal/components/node/common" 11 | "github.com/fananchong/go-xserver/internal/protocol" 12 | "github.com/fananchong/go-xserver/internal/utils" 13 | "github.com/fananchong/gotcp" 14 | ) 15 | 16 | // IntranetSession : 网络会话类( 服务器组内 Gateway 客户端会话类 ) 17 | type IntranetSession struct { 18 | *nodecommon.SessionBase 19 | sourceSess *Session 20 | } 21 | 22 | // NewIntranetSession : 网络会话类的构造函数 23 | func NewIntranetSession(ctx *common.Context, sessMgr *nodecommon.SessionMgr, sourceSess *Session) *IntranetSession { 24 | sess := &IntranetSession{} 25 | sess.SessionBase = nodecommon.NewSessionBase(ctx, sess) 26 | sess.SessMgr = sessMgr 27 | sess.sourceSess = sourceSess 28 | return sess 29 | } 30 | 31 | // Start : 启动 32 | func (sess *IntranetSession) Start() { 33 | go func() { 34 | for { 35 | node := sess.SessMgr.GetByID(nodecommon.ServerID2NodeID(sess.Info.GetId())) 36 | if node == nil { 37 | // 目标节点已丢失,不用试图去连接啦 38 | break 39 | } 40 | address := fmt.Sprintf("%s:%d", sess.Info.GetAddrs()[utils.IPINNER], sess.Info.GetPorts()[utils.PORTFORINTRANET]) 41 | sess.Ctx.Infoln("Try to connect to the gateway server, address:", address, "node:", sess.Info.GetId().GetID()) 42 | if sess.Connect(address, sess) == false { 43 | time.Sleep(1 * time.Second) 44 | continue 45 | } 46 | sess.Verify() 47 | sess.RegisterSelf(sess.sourceSess.GetID(), sess.sourceSess.GetType(), config.Gateway, sess.Info.GetId()) 48 | sess.Ctx.Infoln("Successfully connected to the gateway server, address:", address, "node:", sess.Info.GetId().GetID()) 49 | break 50 | } 51 | }() 52 | } 53 | 54 | // DoRegister : 某节点注册时处理 55 | func (sess *IntranetSession) DoRegister(msg *protocol.MSG_MGR_REGISTER_SERVER) { 56 | } 57 | 58 | // DoVerify : 验证时保存自己的注册消息 59 | func (sess *IntranetSession) DoVerify(msg *protocol.MSG_MGR_REGISTER_SERVER) { 60 | } 61 | 62 | // DoLose : 节点丢失时处理 63 | func (sess *IntranetSession) DoLose(msg *protocol.MSG_MGR_LOSE_SERVER) { 64 | 65 | } 66 | 67 | // DoClose : 节点关闭时处理 68 | func (sess *IntranetSession) DoClose(sessbase *nodecommon.SessionBase) { 69 | } 70 | 71 | // DoRecv : 节点收到消息处理 72 | func (sess *IntranetSession) DoRecv(cmd uint64, data []byte, flag byte) (done bool) { 73 | done = true 74 | switch protocol.CMD_GW_ENUM(cmd) { 75 | case protocol.CMD_GW_RELAY_CLIENT_MSG: 76 | f := sess.FuncOnRelayMsg() 77 | if f == nil { 78 | sess.Ctx.Errorln("There is no handler function, the handler function is `FuncOnRelayMsg`") 79 | return 80 | } 81 | msg := &protocol.MSG_GW_RELAY_CLIENT_MSG{} 82 | if gotcp.Decode(data, flag, msg) == nil { 83 | sess.Ctx.Errorln("Message parsing failed, message number is `protocol.CMD_GW_RELAY_CLIENT_MSG`(", int(protocol.CMD_GW_RELAY_CLIENT_MSG), ")") 84 | return 85 | } 86 | f(config.Client, context.NodeID(0), msg.GetAccount(), uint64(msg.GetCMD()), msg.GetData(), uint8(msg.GetFlag())) 87 | case protocol.CMD_GW_RELAY_SERVER_MSG1: 88 | f := sess.FuncOnRelayMsg() 89 | if f == nil { 90 | sess.Ctx.Errorln("There is no handler function, the handler function is `FuncOnRelayMsg`") 91 | return 92 | } 93 | msg := &protocol.MSG_GW_RELAY_SERVER_MSG1{} 94 | if gotcp.Decode(data, flag, msg) == nil { 95 | sess.Ctx.Errorln("Message parsing failed, message number is `protocol.CMD_GW_RELAY_SERVER_MSG1`(", int(protocol.CMD_GW_RELAY_SERVER_MSG1), ")") 96 | return 97 | } 98 | if msg.GetTargetType() != uint32(sess.sourceSess.GetType()) { 99 | sess.Ctx.Errorln("Field 'TargetType' error. TargetType:", msg.GetTargetType(), "SessType:", sess.sourceSess.GetType()) 100 | return 101 | } 102 | f(config.NodeType(msg.GetSourceType()), nodecommon.ServerID2NodeID(msg.GetSourceID()), "", uint64(msg.GetCMD()), msg.GetData(), uint8(msg.GetFlag())) 103 | case protocol.CMD_GW_RELAY_SERVER_MSG2: 104 | f := sess.FuncOnRelayMsg() 105 | if f == nil { 106 | sess.Ctx.Errorln("There is no handler function, the handler function is `FuncOnRelayMsg`") 107 | return 108 | } 109 | msg := &protocol.MSG_GW_RELAY_SERVER_MSG2{} 110 | if gotcp.Decode(data, flag, msg) == nil { 111 | sess.Ctx.Errorln("Message parsing failed, message number is `protocol.CMD_GW_RELAY_SERVER_MSG2`(", int(protocol.CMD_GW_RELAY_SERVER_MSG2), ")") 112 | return 113 | } 114 | if !nodecommon.EqualSID(msg.GetTargetID(), nodecommon.NodeID2ServerID(sess.sourceSess.GetID())) { 115 | sess.Ctx.Errorln("Field 'TargetID' error. TargetID:", msg.GetTargetID(), "SessID:", sess.sourceSess.GetID()) 116 | return 117 | } 118 | f(config.NodeType(msg.GetSourceType()), nodecommon.ServerID2NodeID(msg.GetSourceID()), "", uint64(msg.GetCMD()), msg.GetData(), uint8(msg.GetFlag())) 119 | case protocol.CMD_GW_REGISTER_ACCOUNT: 120 | msg := &protocol.MSG_GW_REGISTER_ACCOUNT{} 121 | if gotcp.Decode(data, flag, msg) == nil { 122 | sess.Ctx.Errorln("Message parsing failed, message number is `protocol.CMD_GW_REGISTER_ACCOUNT`(", int(protocol.CMD_GW_REGISTER_ACCOUNT), ")") 123 | return 124 | } 125 | sess.sourceSess.GWMgr.AddUser(msg.Account, sess.SessionBase) 126 | case protocol.CMD_GW_LOSE_ACCOUNT: 127 | f := sess.FuncOnLoseAccount() 128 | if f == nil { 129 | sess.Ctx.Errorln("There is no handler function, the handler function is `FuncOnLoseAccount`") 130 | return 131 | } 132 | msg := &protocol.MSG_GW_LOSE_ACCOUNT{} 133 | if gotcp.Decode(data, flag, msg) == nil { 134 | sess.Ctx.Errorln("Message parsing failed, message number is `protocol.CMD_GW_LOSE_ACCOUNT`(", int(protocol.CMD_GW_LOSE_ACCOUNT), ")") 135 | return 136 | } 137 | f(msg.Account) 138 | sess.sourceSess.GWMgr.DelUser(msg.Account) 139 | default: 140 | done = false 141 | } 142 | return 143 | } 144 | -------------------------------------------------------------------------------- /internal/components/node/normal/intranet_session_mgr.go: -------------------------------------------------------------------------------- 1 | package nodenormal 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/fananchong/go-xserver/common" 8 | nodecommon "github.com/fananchong/go-xserver/internal/components/node/common" 9 | "github.com/fananchong/go-xserver/internal/utils" 10 | ) 11 | 12 | // User : 13 | type User struct { 14 | Account string 15 | Sess *nodecommon.SessionBase 16 | ActiveTimestamp int64 17 | } 18 | 19 | // NewUser : 20 | func NewUser(account string, sess *nodecommon.SessionBase) *User { 21 | user := &User{ 22 | Account: account, 23 | Sess: sess, 24 | ActiveTimestamp: sess.Ctx.GetTickCount(), 25 | } 26 | return user 27 | } 28 | 29 | // IntranetSessionMgr : IntranetSession 对象管理类 30 | type IntranetSessionMgr struct { 31 | ctx *common.Context 32 | users map[string]*User 33 | mutex sync.RWMutex 34 | checkActiveTicker *utils.Ticker 35 | } 36 | 37 | // NewIntranetSessionMgr : 38 | func NewIntranetSessionMgr(ctx *common.Context) *IntranetSessionMgr { 39 | mgr := &IntranetSessionMgr{ 40 | ctx: ctx, 41 | users: make(map[string]*User), 42 | } 43 | mgr.checkActiveTicker = utils.NewTickerHelper("CHECK_ACTIVE", ctx, 1*time.Second, mgr.checkActive) 44 | return mgr 45 | } 46 | 47 | // AddUser : 48 | func (mgr *IntranetSessionMgr) AddUser(account string, sess *nodecommon.SessionBase) { 49 | mgr.mutex.Lock() 50 | defer mgr.mutex.Unlock() 51 | mgr.users[account] = NewUser(account, sess) 52 | } 53 | 54 | // DelUser : 55 | func (mgr *IntranetSessionMgr) DelUser(account string) { 56 | mgr.mutex.Lock() 57 | defer mgr.mutex.Unlock() 58 | delete(mgr.users, account) 59 | } 60 | 61 | // GetAndActive : 62 | func (mgr *IntranetSessionMgr) GetAndActive(account string) *nodecommon.SessionBase { 63 | mgr.mutex.RLock() 64 | defer mgr.mutex.RUnlock() 65 | if user, ok := mgr.users[account]; ok { 66 | user.ActiveTimestamp = mgr.ctx.GetTickCount() 67 | return user.Sess 68 | } 69 | return nil 70 | } 71 | 72 | func (mgr *IntranetSessionMgr) checkActive() { 73 | mgr.mutex.Lock() 74 | defer mgr.mutex.Unlock() 75 | now := mgr.ctx.GetTickCount() 76 | var dels []*User 77 | for _, user := range mgr.users { 78 | if now-user.ActiveTimestamp >= mgr.ctx.Config().Role.IdleTime*1000 { 79 | dels = append(dels, user) 80 | } 81 | } 82 | for _, user := range dels { 83 | delete(mgr.users, user.Account) 84 | } 85 | } 86 | 87 | // Start : 开始 88 | func (mgr *IntranetSessionMgr) Start() { 89 | mgr.checkActiveTicker.Start() 90 | } 91 | 92 | // Close : 结束 93 | func (mgr *IntranetSessionMgr) Close() { 94 | mgr.checkActiveTicker.Close() 95 | } 96 | -------------------------------------------------------------------------------- /internal/components/node/normal/normal.go: -------------------------------------------------------------------------------- 1 | package nodenormal 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | "time" 7 | 8 | "github.com/fananchong/go-xserver/common" 9 | "github.com/fananchong/go-xserver/common/config" 10 | "github.com/fananchong/go-xserver/internal/components/misc" 11 | nodecommon "github.com/fananchong/go-xserver/internal/components/node/common" 12 | nodegateway "github.com/fananchong/go-xserver/internal/components/node/gateway" 13 | "github.com/fananchong/go-xserver/internal/protocol" 14 | "github.com/fananchong/go-xserver/internal/utils" 15 | ) 16 | 17 | // 通过该类接入服务器组,该类主要处理与 Mgr Server 的交互 18 | 19 | // Normal : 普通节点 20 | type Normal struct { 21 | *Session 22 | ctx *common.Context 23 | components []utils.IComponent 24 | mtx sync.Mutex 25 | } 26 | 27 | // NewNormal : 普通节点实现类的构造函数 28 | func NewNormal(ctx *common.Context) *Normal { 29 | normal := &Normal{ 30 | ctx: ctx, 31 | Session: NewSession(ctx), 32 | } 33 | pluginType := misc.GetPluginType(ctx) 34 | if pluginType != config.Mgr { 35 | normal.Info = &protocol.SERVER_INFO{} 36 | if pluginType != config.Gateway { 37 | normal.Info.Id = nodecommon.NodeID2ServerID(nodecommon.NewNID(ctx, pluginType)) 38 | } 39 | normal.Info.Type = uint32(pluginType) 40 | normal.Info.Addrs = []string{utils.GetIPInner(ctx), utils.GetIPOuter(ctx)} 41 | normal.Info.Ports = ctx.Config().Network.Port 42 | // TODO: 后续支持 43 | // normal.Info.Overload 44 | // normal.Info.Version 45 | normal.init() 46 | normal.ctx.INode = normal 47 | if pluginType != config.Gateway { 48 | ctx.Infoln("NODE ID:", normal.GetID(), ", NODE TYPE:", pluginType) 49 | misc.SetPluginID(normal.ctx, uint32(normal.GetID())) 50 | } 51 | } 52 | return normal 53 | } 54 | 55 | func (normal *Normal) init() bool { 56 | // ping ticker 57 | pingTicker := utils.NewTickerHelper("PING", normal.Ctx, 5*time.Second, normal.Ping) 58 | 59 | // bind components 60 | normal.components = []utils.IComponent{ 61 | normal.Session, 62 | pingTicker, 63 | } 64 | return true 65 | } 66 | 67 | // Start : 节点开始工作 68 | func (normal *Normal) Start() bool { 69 | pluginType := misc.GetPluginType(normal.ctx) 70 | if pluginType != config.Mgr { 71 | go func() { 72 | misc.WaitComponent(normal.ctx) 73 | if pluginType == config.Gateway { 74 | normal.Info.Id = nodecommon.NodeID2ServerID(normal.ctx.IGateway.(*nodegateway.Gateway).GetID()) 75 | normal.ctx.Infoln("NODE ID:", normal.GetID(), ", NODE TYPE:", pluginType) 76 | misc.SetPluginID(normal.ctx, uint32(normal.GetID())) 77 | } 78 | normal.ctx.Infoln("Service node start ...") 79 | if normal.start() == false { 80 | normal.ctx.Errorln("Service node failed to start") 81 | os.Exit(1) 82 | } 83 | }() 84 | } 85 | return true 86 | } 87 | 88 | func (normal *Normal) start() bool { 89 | normal.mtx.Lock() 90 | defer normal.mtx.Unlock() 91 | for _, v := range normal.components { 92 | if v != nil && v.Start() == false { 93 | panic("") 94 | } 95 | } 96 | return true 97 | } 98 | 99 | // Close : 关闭节点 100 | func (normal *Normal) Close() { 101 | normal.mtx.Lock() 102 | defer normal.mtx.Unlock() 103 | for _, v := range normal.components { 104 | v.Close() 105 | } 106 | normal.Session.Shutdown() 107 | } 108 | -------------------------------------------------------------------------------- /internal/components/plugin.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "os" 5 | "plugin" 6 | 7 | "github.com/fananchong/go-xserver/common" 8 | "github.com/fananchong/go-xserver/common/config" 9 | "github.com/fananchong/go-xserver/internal/components/misc" 10 | ) 11 | 12 | // Plugin : 插件组件 13 | type Plugin struct { 14 | ctx *common.Context 15 | pluginObj common.IPlugin 16 | } 17 | 18 | // NewPlugin : 实例化 19 | func NewPlugin(ctx *common.Context) *Plugin { 20 | p := &Plugin{ctx: ctx} 21 | p.loadPlugin(ctx) 22 | return p 23 | } 24 | 25 | // Start : 实例化组件 26 | func (p *Plugin) Start() bool { 27 | var ret bool 28 | if p.pluginObj != nil { 29 | ret = p.pluginObj.Start() 30 | } 31 | return ret 32 | } 33 | 34 | // Close : 关闭组件 35 | func (p *Plugin) Close() { 36 | if p.pluginObj != nil { 37 | p.pluginObj.Close() 38 | p.pluginObj = nil 39 | } 40 | } 41 | 42 | func (p *Plugin) loadPlugin(ctx *common.Context) { 43 | v := p.ctx.IConfig.(*Config).GetViperObj() 44 | appName := v.GetString("app") 45 | if appName == "" { 46 | p.ctx.PrintUsage() 47 | os.Exit(1) 48 | } 49 | so, err := plugin.Open(appName + ".so") 50 | if err != nil { 51 | ctx.Errorln(err) 52 | os.Exit(1) 53 | } 54 | obj, err := so.Lookup("PluginObj") 55 | if err != nil { 56 | ctx.Errorln(err) 57 | os.Exit(1) 58 | } 59 | t, err := so.Lookup("PluginType") 60 | if err != nil { 61 | ctx.Errorln(err) 62 | os.Exit(1) 63 | } 64 | c, err := so.Lookup("Ctx") 65 | if err != nil { 66 | ctx.Errorln(err) 67 | os.Exit(1) 68 | } 69 | p.pluginObj = *obj.(*common.IPlugin) 70 | pluginType := *t.(*config.NodeType) 71 | *c.(**common.Context) = ctx 72 | misc.SetPluginType(ctx, pluginType) 73 | } 74 | -------------------------------------------------------------------------------- /internal/components/pprof.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "net/http" 5 | _ "net/http/pprof" // 只使用 pprof 包的 init 函数 6 | 7 | "github.com/fananchong/go-xserver/common" 8 | ) 9 | 10 | // Pprof : http pprof组件 11 | type Pprof struct { 12 | ctx *common.Context 13 | web *http.Server 14 | } 15 | 16 | // NewPprof : 实例化 17 | func NewPprof(ctx *common.Context) *Pprof { 18 | return &Pprof{ctx: ctx} 19 | } 20 | 21 | // Start : 实例化组件 22 | func (pprof *Pprof) Start() bool { 23 | addr := pprof.ctx.Config().Common.Pprof 24 | if addr != "" { 25 | go func() { 26 | pprof.ctx.Println("pprof listen :", addr) 27 | pprof.web = &http.Server{Addr: addr, Handler: nil} 28 | pprof.web.ListenAndServe() 29 | }() 30 | } 31 | return true 32 | } 33 | 34 | // Close : 关闭组件 35 | func (pprof *Pprof) Close() { 36 | if pprof.web != nil { 37 | pprof.web.Close() 38 | pprof.web = nil 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/components/rand.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/fananchong/go-xserver/common" 7 | ) 8 | 9 | // Rand : 随机函数组件 10 | type Rand struct { 11 | ctx *common.Context 12 | } 13 | 14 | // NewRand : 实例化 15 | func NewRand(ctx *common.Context) *Rand { 16 | r := &Rand{ctx: ctx} 17 | r.init() 18 | return r 19 | } 20 | 21 | // Start : 实例化组件 22 | func (r *Rand) Start() bool { 23 | return true 24 | } 25 | 26 | func (r *Rand) init() { 27 | r.ctx.IRand = rand.New(rand.NewSource(r.ctx.GetTickCount())) 28 | } 29 | 30 | // Close : 关闭组件 31 | func (*Rand) Close() { 32 | // No need to do anything 33 | } 34 | -------------------------------------------------------------------------------- /internal/components/redis.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "time" 5 | 6 | go_redis_orm "github.com/fananchong/go-redis-orm.v2" 7 | "github.com/fananchong/go-xserver/common" 8 | "github.com/fananchong/go-xserver/common/config" 9 | ) 10 | 11 | // Redis : Redis 组件 12 | type Redis struct { 13 | ctx *common.Context 14 | } 15 | 16 | // NewRedis : 实例化 17 | func NewRedis(ctx *common.Context) *Redis { 18 | redis := &Redis{ctx: ctx} 19 | 20 | // TODO: go_redis_orm 可以实例化,而非全局的 21 | go_redis_orm.SetNewRedisHandler(go_redis_orm.NewDefaultRedisClient) 22 | 23 | cfgs := []config.FrameworkConfigRedis{ 24 | redis.ctx.Config().DbMgr, 25 | redis.ctx.Config().DbRoleName, 26 | redis.ctx.Config().DbServer, 27 | redis.ctx.Config().DbAccount, 28 | } 29 | for _, cfg := range cfgs { 30 | LOOP: 31 | if err := go_redis_orm.CreateDB(cfg.Name, cfg.Addrs, cfg.Password, cfg.DBIndex); err != nil { 32 | redis.ctx.Errorln(err) 33 | time.Sleep(5 * time.Second) 34 | goto LOOP 35 | } 36 | } 37 | return redis 38 | } 39 | 40 | // Start : 实例化组件 41 | func (redis *Redis) Start() bool { 42 | return true 43 | } 44 | 45 | // Close : 关闭组件 46 | func (*Redis) Close() { 47 | // No need to do anything 48 | } 49 | -------------------------------------------------------------------------------- /internal/components/role2account.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | go_redis_orm "github.com/fananchong/go-redis-orm.v2" 8 | "github.com/fananchong/go-xserver/common" 9 | "github.com/fananchong/go-xserver/internal/db" 10 | "github.com/fananchong/go-xserver/internal/utils" 11 | ) 12 | 13 | // AccountInfo : 14 | type AccountInfo struct { 15 | Role string 16 | Account string 17 | ActiveTimestamp int64 18 | } 19 | 20 | // NewAccountInfo : 21 | func NewAccountInfo(role, account string, t int64) *AccountInfo { 22 | info := &AccountInfo{ 23 | Role: role, 24 | Account: account, 25 | ActiveTimestamp: t, 26 | } 27 | return info 28 | } 29 | 30 | // Role2Account : 本帮助类,提供`根据角色名查找账号`的功能 31 | type Role2Account struct { 32 | ctx *common.Context 33 | cache map[string]*AccountInfo 34 | mutex sync.RWMutex 35 | checkActiveTicker *utils.Ticker 36 | } 37 | 38 | // NewRole2Account : 构造函数 39 | func NewRole2Account(ctx *common.Context) *Role2Account { 40 | role2account := &Role2Account{ 41 | ctx: ctx, 42 | cache: make(map[string]*AccountInfo), 43 | } 44 | role2account.checkActiveTicker = utils.NewTickerHelper("CHECK_ACTIVE", ctx, 1*time.Second, role2account.checkActive) 45 | ctx.IRole2Account = role2account 46 | return role2account 47 | } 48 | 49 | // Add : 加入本地缓存 50 | func (role2account *Role2Account) Add(role, account string) { 51 | role2account.mutex.Lock() 52 | defer role2account.mutex.Unlock() 53 | role2account.cache[role] = NewAccountInfo(role, account, role2account.ctx.GetTickCount()) 54 | } 55 | 56 | // AddAndInsertDB : 加入本地缓存并保存数据库 57 | func (role2account *Role2Account) AddAndInsertDB(role, account string) bool { 58 | dbObj := db.NewRoleName(role2account.ctx.Config().DbRoleName.Name, role) 59 | dbObj.SetAccount(account) 60 | if err := dbObj.Save(); err != nil { 61 | role2account.ctx.Errorln(err, "role:", role, "account:", account) 62 | return false 63 | } 64 | role2account.mutex.Lock() 65 | defer role2account.mutex.Unlock() 66 | role2account.cache[role] = NewAccountInfo(role, account, role2account.ctx.GetTickCount()) 67 | return true 68 | } 69 | 70 | // GetAndActive : 根据角色名,查找账号。先从本地缓存中找,没有则数据库中找 71 | func (role2account *Role2Account) GetAndActive(role string) string { 72 | role2account.mutex.RLock() 73 | if info, ok := role2account.cache[role]; ok { 74 | info.ActiveTimestamp = role2account.ctx.GetTickCount() 75 | role2account.mutex.RUnlock() 76 | return info.Account 77 | } 78 | role2account.mutex.RUnlock() 79 | dbObj := db.NewRoleName(role2account.ctx.Config().DbRoleName.Name, role) 80 | if err := dbObj.Load(); err != nil { 81 | if err != go_redis_orm.ERR_ISNOT_EXIST_KEY { 82 | role2account.ctx.Errorln(err, "role:", role) 83 | } 84 | return "" 85 | } 86 | role2account.Add(role, dbObj.GetAccount()) 87 | return dbObj.GetAccount() 88 | } 89 | 90 | func (role2account *Role2Account) checkActive() { 91 | // TODO: 需要新增最小堆排序容器字段,并维护。使该函数计算复杂度最高为 O(logn) 92 | role2account.mutex.Lock() 93 | defer role2account.mutex.Unlock() 94 | now := role2account.ctx.GetTickCount() 95 | var dels []*AccountInfo 96 | for _, info := range role2account.cache { 97 | if now-info.ActiveTimestamp >= 24*60*60*1000 { // 1 天 98 | dels = append(dels, info) 99 | } 100 | } 101 | for _, info := range dels { 102 | delete(role2account.cache, info.Role) 103 | } 104 | } 105 | 106 | // Start : 开始 107 | func (role2account *Role2Account) Start() bool { 108 | role2account.checkActiveTicker.Start() 109 | return true 110 | } 111 | 112 | // Close : 结束 113 | func (role2account *Role2Account) Close() { 114 | role2account.checkActiveTicker.Close() 115 | } 116 | -------------------------------------------------------------------------------- /internal/components/signal.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | 8 | "github.com/fananchong/go-xserver/common" 9 | ) 10 | 11 | // Signal : 信号处理组件 12 | type Signal struct { 13 | ctx *common.Context 14 | sig chan os.Signal 15 | } 16 | 17 | // NewSignal : 实例化 18 | func NewSignal(ctx *common.Context) *Signal { 19 | return &Signal{ctx: ctx} 20 | } 21 | 22 | // Start : 实例化组件 23 | func (s *Signal) Start() bool { 24 | s.ctx.Infoln("The program started successfully") 25 | s.sig = make(chan os.Signal) 26 | signal.Notify(s.sig, 27 | syscall.SIGINT, 28 | syscall.SIGQUIT, 29 | syscall.SIGABRT, 30 | syscall.SIGTERM, 31 | syscall.SIGPIPE) 32 | 33 | for { 34 | select { 35 | case sig := <-s.sig: 36 | switch sig { 37 | case syscall.SIGPIPE: 38 | default: 39 | s.ctx.Infoln("Received signal:", sig) 40 | return true 41 | } 42 | } 43 | } 44 | } 45 | 46 | // Close : 关闭组件 47 | func (s *Signal) Close() { 48 | // No need to do anything 49 | } 50 | -------------------------------------------------------------------------------- /internal/components/tcpserver.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "github.com/fananchong/go-xserver/common" 5 | "github.com/fananchong/go-xserver/internal/utils" 6 | "github.com/fananchong/gotcp" 7 | ) 8 | 9 | // TCPServer : TCP Server 组件 10 | type TCPServer struct { 11 | ctx *common.Context 12 | } 13 | 14 | // NewTCPServer : 实例化 15 | func NewTCPServer(ctx *common.Context) *TCPServer { 16 | server := &TCPServer{ctx: ctx} 17 | server.ctx.ITCPServer = &gotcp.Server{} 18 | return server 19 | } 20 | 21 | // Start : 实例化组件 22 | func (server *TCPServer) Start() bool { 23 | s := server.ctx.ITCPServer.(*gotcp.Server) 24 | if s.GetSessionType() != nil { 25 | // 填写 `0.0.0.0` , 而不是 `utils.GetIPOuter(server.ctx)` 具体外网 IP ,是为了能支持阿里云 ECS 服务器 26 | if !startTCPServer(s, "0.0.0.0", utils.GetDefaultServicePort(server.ctx)) { 27 | return false 28 | } 29 | server.ctx.Config().Network.Port[utils.PORTFORCLIENT] = s.GetRealPort() 30 | } 31 | return true 32 | } 33 | 34 | // Close : 关闭组件 35 | func (server *TCPServer) Close() { 36 | if server.ctx.ITCPServer != nil { 37 | server.ctx.ITCPServer.(*gotcp.Server).Close() 38 | server.ctx.ITCPServer = nil 39 | } 40 | } 41 | 42 | func startTCPServer(s *gotcp.Server, addr string, port int32) bool { 43 | s.SetAddress(addr, port) 44 | s.SetUnfixedPort(true) 45 | return s.Start() 46 | } 47 | -------------------------------------------------------------------------------- /internal/components/time.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/fananchong/go-xserver/common" 7 | ) 8 | 9 | // Time : 随机函数组件 10 | type Time struct { 11 | ctx *common.Context 12 | delta int64 13 | } 14 | 15 | // NewTime : 实例化 16 | func NewTime(ctx *common.Context) *Time { 17 | t := &Time{ctx: ctx} 18 | t.init() 19 | return t 20 | } 21 | 22 | // Start : 实例化组件 23 | func (t *Time) Start() bool { 24 | return true 25 | } 26 | 27 | func (t *Time) init() { 28 | t.ctx.ITime = t 29 | } 30 | 31 | // Close : 关闭组件 32 | func (*Time) Close() { 33 | // No need to do anything 34 | } 35 | 36 | // GetTickCount : 毫秒数 37 | func (t *Time) GetTickCount() int64 { 38 | now := time.Now().UnixNano() / 1e6 39 | return now + t.delta 40 | } 41 | 42 | // SetDelta : 设置时间差(单位毫秒),用来快进或后退当前时间(调试时间相关功能时,会用到) 43 | func (t *Time) SetDelta(delta int64) { 44 | t.delta = delta 45 | } 46 | -------------------------------------------------------------------------------- /internal/components/uid.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | go_redis_orm "github.com/fananchong/go-redis-orm.v2" 5 | "github.com/fananchong/go-xserver/common" 6 | "github.com/gomodule/redigo/redis" 7 | ) 8 | 9 | // UID : UID 组件 10 | type UID struct { 11 | ctx *common.Context 12 | cli go_redis_orm.IClient 13 | } 14 | 15 | // NewUID : 实例化 16 | func NewUID(ctx *common.Context) *UID { 17 | uid := &UID{ 18 | ctx: ctx, 19 | cli: go_redis_orm.GetDB(ctx.Config().DbAccount.Name), 20 | } 21 | ctx.IUID = uid 22 | return uid 23 | } 24 | 25 | // Start : 实例化组件 26 | func (uid *UID) Start() bool { 27 | return true 28 | } 29 | 30 | // Close : 关闭组件 31 | func (*UID) Close() { 32 | // No need to do anything 33 | } 34 | 35 | // GetUID : 根据 KEY , 获取 UID 36 | func (uid *UID) GetUID(key string) (uint64, error) { 37 | return redis.Uint64(uid.cli.Do("HINCRBY", "idgen", key, 1)) 38 | } 39 | -------------------------------------------------------------------------------- /internal/db/account.go: -------------------------------------------------------------------------------- 1 | /// ------------------------------------------------------------------------------- 2 | /// THIS FILE IS ORIGINALLY GENERATED BY redis2go.exe. 3 | /// PLEASE DO NOT MODIFY THIS FILE. 4 | /// ------------------------------------------------------------------------------- 5 | 6 | package db 7 | 8 | import ( 9 | "errors" 10 | 11 | go_redis_orm "github.com/fananchong/go-redis-orm.v2" 12 | "github.com/gomodule/redigo/redis" 13 | ) 14 | 15 | // Account : 代表 1 个 redis 对象 16 | type Account struct { 17 | Key string 18 | passwd string 19 | 20 | dirtyDataInAccount map[string]interface{} 21 | dirtyDataForStructFiledInAccount map[string]interface{} 22 | isLoadInAccount bool 23 | dbKeyInAccount string 24 | ddbNameInAccount string 25 | expireInAccount uint 26 | } 27 | 28 | // NewAccount : NewAccount 的构造函数 29 | func NewAccount(dbName string, key string) *Account { 30 | return &Account{ 31 | Key: key, 32 | ddbNameInAccount: dbName, 33 | dbKeyInAccount: "Account:" + key, 34 | dirtyDataInAccount: make(map[string]interface{}), 35 | dirtyDataForStructFiledInAccount: make(map[string]interface{}), 36 | } 37 | } 38 | 39 | // HasKey : 是否存在 KEY 40 | // 返回值,若访问数据库失败返回-1;若 key 存在返回 1 ,否则返回 0 。 41 | func (objAccount *Account) HasKey() (int, error) { 42 | db := go_redis_orm.GetDB(objAccount.ddbNameInAccount) 43 | val, err := redis.Int(db.Do("EXISTS", objAccount.dbKeyInAccount)) 44 | if err != nil { 45 | return -1, err 46 | } 47 | return val, nil 48 | } 49 | 50 | // Load : 从 redis 加载数据 51 | func (objAccount *Account) Load() error { 52 | if objAccount.isLoadInAccount == true { 53 | return errors.New("alreay load") 54 | } 55 | db := go_redis_orm.GetDB(objAccount.ddbNameInAccount) 56 | val, err := redis.Values(db.Do("HGETALL", objAccount.dbKeyInAccount)) 57 | if err != nil { 58 | return err 59 | } 60 | if len(val) == 0 { 61 | return go_redis_orm.ERR_ISNOT_EXIST_KEY 62 | } 63 | var data struct { 64 | Passwd string `redis:"passwd"` 65 | } 66 | if err := redis.ScanStruct(val, &data); err != nil { 67 | return err 68 | } 69 | objAccount.passwd = data.Passwd 70 | objAccount.isLoadInAccount = true 71 | return nil 72 | } 73 | 74 | // Save : 保存数据到 redis 75 | func (objAccount *Account) Save() error { 76 | if len(objAccount.dirtyDataInAccount) == 0 && len(objAccount.dirtyDataForStructFiledInAccount) == 0 { 77 | return nil 78 | } 79 | for k := range objAccount.dirtyDataForStructFiledInAccount { 80 | _ = k 81 | 82 | } 83 | db := go_redis_orm.GetDB(objAccount.ddbNameInAccount) 84 | if _, err := db.Do("HMSET", redis.Args{}.Add(objAccount.dbKeyInAccount).AddFlat(objAccount.dirtyDataInAccount)...); err != nil { 85 | return err 86 | } 87 | if objAccount.expireInAccount != 0 { 88 | if _, err := db.Do("EXPIRE", objAccount.dbKeyInAccount, objAccount.expireInAccount); err != nil { 89 | return err 90 | } 91 | } 92 | objAccount.dirtyDataInAccount = make(map[string]interface{}) 93 | objAccount.dirtyDataForStructFiledInAccount = make(map[string]interface{}) 94 | return nil 95 | } 96 | 97 | // Delete : 从 redis 删除数据 98 | func (objAccount *Account) Delete() error { 99 | db := go_redis_orm.GetDB(objAccount.ddbNameInAccount) 100 | _, err := db.Do("DEL", objAccount.dbKeyInAccount) 101 | if err == nil { 102 | objAccount.isLoadInAccount = false 103 | objAccount.dirtyDataInAccount = make(map[string]interface{}) 104 | objAccount.dirtyDataForStructFiledInAccount = make(map[string]interface{}) 105 | } 106 | return err 107 | } 108 | 109 | // IsLoad : 是否已经从 redis 导入数据 110 | func (objAccount *Account) IsLoad() bool { 111 | return objAccount.isLoadInAccount 112 | } 113 | 114 | // Expire : 向 redis 设置该对象过期时间 115 | func (objAccount *Account) Expire(v uint) { 116 | objAccount.expireInAccount = v 117 | } 118 | 119 | // DirtyData : 获取该对象目前已脏的数据 120 | func (objAccount *Account) DirtyData() (map[string]interface{}, error) { 121 | for k := range objAccount.dirtyDataForStructFiledInAccount { 122 | _ = k 123 | 124 | } 125 | data := make(map[string]interface{}) 126 | for k, v := range objAccount.dirtyDataInAccount { 127 | data[k] = v 128 | } 129 | objAccount.dirtyDataInAccount = make(map[string]interface{}) 130 | objAccount.dirtyDataForStructFiledInAccount = make(map[string]interface{}) 131 | return data, nil 132 | } 133 | 134 | // Save2 : 保存数据到 redis 的第 2 种方法 135 | func (objAccount *Account) Save2(dirtyData map[string]interface{}) error { 136 | if len(dirtyData) == 0 { 137 | return nil 138 | } 139 | db := go_redis_orm.GetDB(objAccount.ddbNameInAccount) 140 | if _, err := db.Do("HMSET", redis.Args{}.Add(objAccount.dbKeyInAccount).AddFlat(dirtyData)...); err != nil { 141 | return err 142 | } 143 | if objAccount.expireInAccount != 0 { 144 | if _, err := db.Do("EXPIRE", objAccount.dbKeyInAccount, objAccount.expireInAccount); err != nil { 145 | return err 146 | } 147 | } 148 | return nil 149 | } 150 | 151 | // GetPasswd : 获取字段值 152 | func (objAccount *Account) GetPasswd() string { 153 | return objAccount.passwd 154 | } 155 | 156 | // SetPasswd : 设置字段值 157 | func (objAccount *Account) SetPasswd(value string) { 158 | objAccount.passwd = value 159 | objAccount.dirtyDataInAccount["passwd"] = string([]byte(value)) 160 | } 161 | -------------------------------------------------------------------------------- /internal/db/account_servers.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/fananchong/go-xserver/common/config" 8 | "github.com/fananchong/go-xserver/internal/protocol" 9 | ) 10 | 11 | // 账号对应分配的服务资源 12 | // Login Server 会负责分配 13 | // 可以分配多个服务器资源 14 | 15 | // AccountServer : 分配的服务资源信息(单个) 16 | type AccountServer struct { 17 | ServerID *protocol.SERVER_ID 18 | Address string 19 | Port int32 20 | Type config.NodeType 21 | } 22 | 23 | // Marshal : 序列化 24 | func (accountserver *AccountServer) Marshal() (string, error) { 25 | data, err := json.Marshal(accountserver) 26 | if err != nil { 27 | return "", err 28 | } 29 | return string(data), nil 30 | } 31 | 32 | // Unmarshal : 反序列化 33 | func (accountserver *AccountServer) Unmarshal(data string) error { 34 | return json.Unmarshal([]byte(data), accountserver) 35 | } 36 | 37 | // GetKeyAllocServer : 账号对应的服务器资源的 KEY 38 | func GetKeyAllocServer(nodeType uint32, account string) string { 39 | return fmt.Sprintf("srv%d_%s", nodeType, account) 40 | } 41 | -------------------------------------------------------------------------------- /internal/db/g.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | docker run --rm -v $PWD/../:/out -w /out/db znly/protoc --gogofaster_out=. -I=. -I=../protocol *.proto 6 | sed -i 's#import protocol "."#import protocol "github.com/fananchong/go-xserver/internal/protocol"#g' ./token.pb.go 7 | docker run --rm -v $PWD/redis_def:/app/input -v $PWD:/app/output fananchong/redis2go --package=db 8 | -------------------------------------------------------------------------------- /internal/db/init.go: -------------------------------------------------------------------------------- 1 | //go:generate ./g.sh 2 | 3 | package db 4 | 5 | func init() {} 6 | -------------------------------------------------------------------------------- /internal/db/mgrserver.go: -------------------------------------------------------------------------------- 1 | /// ------------------------------------------------------------------------------- 2 | /// THIS FILE IS ORIGINALLY GENERATED BY redis2go.exe. 3 | /// PLEASE DO NOT MODIFY THIS FILE. 4 | /// ------------------------------------------------------------------------------- 5 | 6 | package db 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | 12 | go_redis_orm "github.com/fananchong/go-redis-orm.v2" 13 | "github.com/gomodule/redigo/redis" 14 | ) 15 | 16 | // MgrServer : 代表 1 个 redis 对象 17 | type MgrServer struct { 18 | Key uint32 19 | addr string 20 | port int32 21 | 22 | dirtyDataInMgrServer map[string]interface{} 23 | dirtyDataForStructFiledInMgrServer map[string]interface{} 24 | isLoadInMgrServer bool 25 | dbKeyInMgrServer string 26 | ddbNameInMgrServer string 27 | expireInMgrServer uint 28 | } 29 | 30 | // NewMgrServer : NewMgrServer 的构造函数 31 | func NewMgrServer(dbName string, key uint32) *MgrServer { 32 | return &MgrServer{ 33 | Key: key, 34 | ddbNameInMgrServer: dbName, 35 | dbKeyInMgrServer: "MgrServer:" + fmt.Sprintf("%d", key), 36 | dirtyDataInMgrServer: make(map[string]interface{}), 37 | dirtyDataForStructFiledInMgrServer: make(map[string]interface{}), 38 | } 39 | } 40 | 41 | // HasKey : 是否存在 KEY 42 | // 返回值,若访问数据库失败返回-1;若 key 存在返回 1 ,否则返回 0 。 43 | func (objMgrServer *MgrServer) HasKey() (int, error) { 44 | db := go_redis_orm.GetDB(objMgrServer.ddbNameInMgrServer) 45 | val, err := redis.Int(db.Do("EXISTS", objMgrServer.dbKeyInMgrServer)) 46 | if err != nil { 47 | return -1, err 48 | } 49 | return val, nil 50 | } 51 | 52 | // Load : 从 redis 加载数据 53 | func (objMgrServer *MgrServer) Load() error { 54 | if objMgrServer.isLoadInMgrServer == true { 55 | return errors.New("alreay load") 56 | } 57 | db := go_redis_orm.GetDB(objMgrServer.ddbNameInMgrServer) 58 | val, err := redis.Values(db.Do("HGETALL", objMgrServer.dbKeyInMgrServer)) 59 | if err != nil { 60 | return err 61 | } 62 | if len(val) == 0 { 63 | return go_redis_orm.ERR_ISNOT_EXIST_KEY 64 | } 65 | var data struct { 66 | Addr string `redis:"addr"` 67 | Port int32 `redis:"port"` 68 | } 69 | if err := redis.ScanStruct(val, &data); err != nil { 70 | return err 71 | } 72 | objMgrServer.addr = data.Addr 73 | objMgrServer.port = data.Port 74 | objMgrServer.isLoadInMgrServer = true 75 | return nil 76 | } 77 | 78 | // Save : 保存数据到 redis 79 | func (objMgrServer *MgrServer) Save() error { 80 | if len(objMgrServer.dirtyDataInMgrServer) == 0 && len(objMgrServer.dirtyDataForStructFiledInMgrServer) == 0 { 81 | return nil 82 | } 83 | for k := range objMgrServer.dirtyDataForStructFiledInMgrServer { 84 | _ = k 85 | 86 | } 87 | db := go_redis_orm.GetDB(objMgrServer.ddbNameInMgrServer) 88 | if _, err := db.Do("HMSET", redis.Args{}.Add(objMgrServer.dbKeyInMgrServer).AddFlat(objMgrServer.dirtyDataInMgrServer)...); err != nil { 89 | return err 90 | } 91 | if objMgrServer.expireInMgrServer != 0 { 92 | if _, err := db.Do("EXPIRE", objMgrServer.dbKeyInMgrServer, objMgrServer.expireInMgrServer); err != nil { 93 | return err 94 | } 95 | } 96 | objMgrServer.dirtyDataInMgrServer = make(map[string]interface{}) 97 | objMgrServer.dirtyDataForStructFiledInMgrServer = make(map[string]interface{}) 98 | return nil 99 | } 100 | 101 | // Delete : 从 redis 删除数据 102 | func (objMgrServer *MgrServer) Delete() error { 103 | db := go_redis_orm.GetDB(objMgrServer.ddbNameInMgrServer) 104 | _, err := db.Do("DEL", objMgrServer.dbKeyInMgrServer) 105 | if err == nil { 106 | objMgrServer.isLoadInMgrServer = false 107 | objMgrServer.dirtyDataInMgrServer = make(map[string]interface{}) 108 | objMgrServer.dirtyDataForStructFiledInMgrServer = make(map[string]interface{}) 109 | } 110 | return err 111 | } 112 | 113 | // IsLoad : 是否已经从 redis 导入数据 114 | func (objMgrServer *MgrServer) IsLoad() bool { 115 | return objMgrServer.isLoadInMgrServer 116 | } 117 | 118 | // Expire : 向 redis 设置该对象过期时间 119 | func (objMgrServer *MgrServer) Expire(v uint) { 120 | objMgrServer.expireInMgrServer = v 121 | } 122 | 123 | // DirtyData : 获取该对象目前已脏的数据 124 | func (objMgrServer *MgrServer) DirtyData() (map[string]interface{}, error) { 125 | for k := range objMgrServer.dirtyDataForStructFiledInMgrServer { 126 | _ = k 127 | 128 | } 129 | data := make(map[string]interface{}) 130 | for k, v := range objMgrServer.dirtyDataInMgrServer { 131 | data[k] = v 132 | } 133 | objMgrServer.dirtyDataInMgrServer = make(map[string]interface{}) 134 | objMgrServer.dirtyDataForStructFiledInMgrServer = make(map[string]interface{}) 135 | return data, nil 136 | } 137 | 138 | // Save2 : 保存数据到 redis 的第 2 种方法 139 | func (objMgrServer *MgrServer) Save2(dirtyData map[string]interface{}) error { 140 | if len(dirtyData) == 0 { 141 | return nil 142 | } 143 | db := go_redis_orm.GetDB(objMgrServer.ddbNameInMgrServer) 144 | if _, err := db.Do("HMSET", redis.Args{}.Add(objMgrServer.dbKeyInMgrServer).AddFlat(dirtyData)...); err != nil { 145 | return err 146 | } 147 | if objMgrServer.expireInMgrServer != 0 { 148 | if _, err := db.Do("EXPIRE", objMgrServer.dbKeyInMgrServer, objMgrServer.expireInMgrServer); err != nil { 149 | return err 150 | } 151 | } 152 | return nil 153 | } 154 | 155 | // GetAddr : 获取字段值 156 | func (objMgrServer *MgrServer) GetAddr() string { 157 | return objMgrServer.addr 158 | } 159 | 160 | // GetPort : 获取字段值 161 | func (objMgrServer *MgrServer) GetPort() int32 { 162 | return objMgrServer.port 163 | } 164 | 165 | // SetAddr : 设置字段值 166 | func (objMgrServer *MgrServer) SetAddr(value string) { 167 | objMgrServer.addr = value 168 | objMgrServer.dirtyDataInMgrServer["addr"] = string([]byte(value)) 169 | } 170 | 171 | // SetPort : 设置字段值 172 | func (objMgrServer *MgrServer) SetPort(value int32) { 173 | objMgrServer.port = value 174 | objMgrServer.dirtyDataInMgrServer["port"] = value 175 | } 176 | -------------------------------------------------------------------------------- /internal/db/redis_atomic.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gomodule/redigo/redis" 7 | ) 8 | 9 | // RedisAtomic : 自定义 Redis 原子操作 10 | type RedisAtomic struct { 11 | Cli redis.Conn 12 | } 13 | 14 | // SetGetX : 原子操作,封装 `不管 SetGetX 有没有设置成功,都重置过期时间为 n` 15 | // 返回值, 空 表示设置成功; 非空 表示设置失败,并返回原先已设置的值 16 | func (redisatomic *RedisAtomic) SetGetX(key, value string, expire int) (string, error) { 17 | lua := `local r=redis.call('SET',KEYS[1],ARGV[1],'NX'); 18 | redis.call('EXPIRE',KEYS[1],%d); 19 | if not not r then return "" else local r=redis.call('GET',KEYS[1]); return r end` 20 | cmd := fmt.Sprintf(lua, expire) 21 | getScript := redis.NewScript(1, cmd) 22 | reply, err := getScript.Do(redisatomic.Cli, key, value) 23 | if err != nil { 24 | return "", err 25 | } 26 | return redis.String(reply, nil) 27 | } 28 | 29 | // DelX : 原子操作,封装 `条件删除, if value = "xxx" then del key` 30 | // 返回值, 1 有删除操作; 0 不需要删除 31 | func (redisatomic *RedisAtomic) DelX(key, value string) (int, error) { 32 | cmd := `local v=redis.call('GET',KEYS[1]); 33 | if ARGV[1]==v then return redis.call('DEL',KEYS[1]) else return 0 end` 34 | getScript := redis.NewScript(1, cmd) 35 | reply, err := getScript.Do(redisatomic.Cli, key, value) 36 | if err != nil { 37 | return -1, err 38 | } 39 | return redis.Int(reply, nil) 40 | } 41 | -------------------------------------------------------------------------------- /internal/db/redis_def/account.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Account", 3 | "type": "1-1", 4 | "key": "string", 5 | "struct_format": "gogo", 6 | "fields": { 7 | "passwd": "string" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /internal/db/redis_def/mgrserver.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MgrServer", 3 | "type": "1-1", 4 | "key": "uint32", 5 | "struct_format": "gogo", 6 | "fields": { 7 | "addr": "string", 8 | "port": "int32" 9 | } 10 | } -------------------------------------------------------------------------------- /internal/db/redis_def/rolename.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RoleName", 3 | "type": "1-1", 4 | "key": "string", 5 | "struct_format": "gogo", 6 | "fields": { 7 | "account": "string" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /internal/db/redis_def/token.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Token", 3 | "type": "1-1", 4 | "key": "string", 5 | "struct_format": "gogo", 6 | "fields": { 7 | "token": "DB_TOKEN" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /internal/db/rolename.go: -------------------------------------------------------------------------------- 1 | /// ------------------------------------------------------------------------------- 2 | /// THIS FILE IS ORIGINALLY GENERATED BY redis2go.exe. 3 | /// PLEASE DO NOT MODIFY THIS FILE. 4 | /// ------------------------------------------------------------------------------- 5 | 6 | package db 7 | 8 | import ( 9 | "errors" 10 | 11 | go_redis_orm "github.com/fananchong/go-redis-orm.v2" 12 | "github.com/gomodule/redigo/redis" 13 | ) 14 | 15 | // RoleName : 代表 1 个 redis 对象 16 | type RoleName struct { 17 | Key string 18 | account string 19 | 20 | dirtyDataInRoleName map[string]interface{} 21 | dirtyDataForStructFiledInRoleName map[string]interface{} 22 | isLoadInRoleName bool 23 | dbKeyInRoleName string 24 | ddbNameInRoleName string 25 | expireInRoleName uint 26 | } 27 | 28 | // NewRoleName : NewRoleName 的构造函数 29 | func NewRoleName(dbName string, key string) *RoleName { 30 | return &RoleName{ 31 | Key: key, 32 | ddbNameInRoleName: dbName, 33 | dbKeyInRoleName: "RoleName:" + key, 34 | dirtyDataInRoleName: make(map[string]interface{}), 35 | dirtyDataForStructFiledInRoleName: make(map[string]interface{}), 36 | } 37 | } 38 | 39 | // HasKey : 是否存在 KEY 40 | // 返回值,若访问数据库失败返回-1;若 key 存在返回 1 ,否则返回 0 。 41 | func (objRoleName *RoleName) HasKey() (int, error) { 42 | db := go_redis_orm.GetDB(objRoleName.ddbNameInRoleName) 43 | val, err := redis.Int(db.Do("EXISTS", objRoleName.dbKeyInRoleName)) 44 | if err != nil { 45 | return -1, err 46 | } 47 | return val, nil 48 | } 49 | 50 | // Load : 从 redis 加载数据 51 | func (objRoleName *RoleName) Load() error { 52 | if objRoleName.isLoadInRoleName == true { 53 | return errors.New("alreay load") 54 | } 55 | db := go_redis_orm.GetDB(objRoleName.ddbNameInRoleName) 56 | val, err := redis.Values(db.Do("HGETALL", objRoleName.dbKeyInRoleName)) 57 | if err != nil { 58 | return err 59 | } 60 | if len(val) == 0 { 61 | return go_redis_orm.ERR_ISNOT_EXIST_KEY 62 | } 63 | var data struct { 64 | Account string `redis:"account"` 65 | } 66 | if err := redis.ScanStruct(val, &data); err != nil { 67 | return err 68 | } 69 | objRoleName.account = data.Account 70 | objRoleName.isLoadInRoleName = true 71 | return nil 72 | } 73 | 74 | // Save : 保存数据到 redis 75 | func (objRoleName *RoleName) Save() error { 76 | if len(objRoleName.dirtyDataInRoleName) == 0 && len(objRoleName.dirtyDataForStructFiledInRoleName) == 0 { 77 | return nil 78 | } 79 | for k := range objRoleName.dirtyDataForStructFiledInRoleName { 80 | _ = k 81 | 82 | } 83 | db := go_redis_orm.GetDB(objRoleName.ddbNameInRoleName) 84 | if _, err := db.Do("HMSET", redis.Args{}.Add(objRoleName.dbKeyInRoleName).AddFlat(objRoleName.dirtyDataInRoleName)...); err != nil { 85 | return err 86 | } 87 | if objRoleName.expireInRoleName != 0 { 88 | if _, err := db.Do("EXPIRE", objRoleName.dbKeyInRoleName, objRoleName.expireInRoleName); err != nil { 89 | return err 90 | } 91 | } 92 | objRoleName.dirtyDataInRoleName = make(map[string]interface{}) 93 | objRoleName.dirtyDataForStructFiledInRoleName = make(map[string]interface{}) 94 | return nil 95 | } 96 | 97 | // Delete : 从 redis 删除数据 98 | func (objRoleName *RoleName) Delete() error { 99 | db := go_redis_orm.GetDB(objRoleName.ddbNameInRoleName) 100 | _, err := db.Do("DEL", objRoleName.dbKeyInRoleName) 101 | if err == nil { 102 | objRoleName.isLoadInRoleName = false 103 | objRoleName.dirtyDataInRoleName = make(map[string]interface{}) 104 | objRoleName.dirtyDataForStructFiledInRoleName = make(map[string]interface{}) 105 | } 106 | return err 107 | } 108 | 109 | // IsLoad : 是否已经从 redis 导入数据 110 | func (objRoleName *RoleName) IsLoad() bool { 111 | return objRoleName.isLoadInRoleName 112 | } 113 | 114 | // Expire : 向 redis 设置该对象过期时间 115 | func (objRoleName *RoleName) Expire(v uint) { 116 | objRoleName.expireInRoleName = v 117 | } 118 | 119 | // DirtyData : 获取该对象目前已脏的数据 120 | func (objRoleName *RoleName) DirtyData() (map[string]interface{}, error) { 121 | for k := range objRoleName.dirtyDataForStructFiledInRoleName { 122 | _ = k 123 | 124 | } 125 | data := make(map[string]interface{}) 126 | for k, v := range objRoleName.dirtyDataInRoleName { 127 | data[k] = v 128 | } 129 | objRoleName.dirtyDataInRoleName = make(map[string]interface{}) 130 | objRoleName.dirtyDataForStructFiledInRoleName = make(map[string]interface{}) 131 | return data, nil 132 | } 133 | 134 | // Save2 : 保存数据到 redis 的第 2 种方法 135 | func (objRoleName *RoleName) Save2(dirtyData map[string]interface{}) error { 136 | if len(dirtyData) == 0 { 137 | return nil 138 | } 139 | db := go_redis_orm.GetDB(objRoleName.ddbNameInRoleName) 140 | if _, err := db.Do("HMSET", redis.Args{}.Add(objRoleName.dbKeyInRoleName).AddFlat(dirtyData)...); err != nil { 141 | return err 142 | } 143 | if objRoleName.expireInRoleName != 0 { 144 | if _, err := db.Do("EXPIRE", objRoleName.dbKeyInRoleName, objRoleName.expireInRoleName); err != nil { 145 | return err 146 | } 147 | } 148 | return nil 149 | } 150 | 151 | // GetAccount : 获取字段值 152 | func (objRoleName *RoleName) GetAccount() string { 153 | return objRoleName.account 154 | } 155 | 156 | // SetAccount : 设置字段值 157 | func (objRoleName *RoleName) SetAccount(value string) { 158 | objRoleName.account = value 159 | objRoleName.dirtyDataInRoleName["account"] = string([]byte(value)) 160 | } 161 | -------------------------------------------------------------------------------- /internal/db/token.go: -------------------------------------------------------------------------------- 1 | /// ------------------------------------------------------------------------------- 2 | /// THIS FILE IS ORIGINALLY GENERATED BY redis2go.exe. 3 | /// PLEASE DO NOT MODIFY THIS FILE. 4 | /// ------------------------------------------------------------------------------- 5 | 6 | package db 7 | 8 | import ( 9 | "errors" 10 | 11 | go_redis_orm "github.com/fananchong/go-redis-orm.v2" 12 | "github.com/gogo/protobuf/proto" 13 | "github.com/gomodule/redigo/redis" 14 | ) 15 | 16 | // Token : 代表 1 个 redis 对象 17 | type Token struct { 18 | Key string 19 | token DB_TOKEN 20 | 21 | dirtyDataInToken map[string]interface{} 22 | dirtyDataForStructFiledInToken map[string]interface{} 23 | isLoadInToken bool 24 | dbKeyInToken string 25 | ddbNameInToken string 26 | expireInToken uint 27 | } 28 | 29 | // NewToken : NewToken 的构造函数 30 | func NewToken(dbName string, key string) *Token { 31 | return &Token{ 32 | Key: key, 33 | ddbNameInToken: dbName, 34 | dbKeyInToken: "Token:" + key, 35 | dirtyDataInToken: make(map[string]interface{}), 36 | dirtyDataForStructFiledInToken: make(map[string]interface{}), 37 | } 38 | } 39 | 40 | // HasKey : 是否存在 KEY 41 | // 返回值,若访问数据库失败返回-1;若 key 存在返回 1 ,否则返回 0 。 42 | func (objToken *Token) HasKey() (int, error) { 43 | db := go_redis_orm.GetDB(objToken.ddbNameInToken) 44 | val, err := redis.Int(db.Do("EXISTS", objToken.dbKeyInToken)) 45 | if err != nil { 46 | return -1, err 47 | } 48 | return val, nil 49 | } 50 | 51 | // Load : 从 redis 加载数据 52 | func (objToken *Token) Load() error { 53 | if objToken.isLoadInToken == true { 54 | return errors.New("alreay load") 55 | } 56 | db := go_redis_orm.GetDB(objToken.ddbNameInToken) 57 | val, err := redis.Values(db.Do("HGETALL", objToken.dbKeyInToken)) 58 | if err != nil { 59 | return err 60 | } 61 | if len(val) == 0 { 62 | return go_redis_orm.ERR_ISNOT_EXIST_KEY 63 | } 64 | var data struct { 65 | Token []byte `redis:"token"` 66 | } 67 | if err := redis.ScanStruct(val, &data); err != nil { 68 | return err 69 | } 70 | if err := proto.Unmarshal(data.Token, &objToken.token); err != nil { 71 | return err 72 | } 73 | 74 | objToken.isLoadInToken = true 75 | return nil 76 | } 77 | 78 | // Save : 保存数据到 redis 79 | func (objToken *Token) Save() error { 80 | if len(objToken.dirtyDataInToken) == 0 && len(objToken.dirtyDataForStructFiledInToken) == 0 { 81 | return nil 82 | } 83 | for k := range objToken.dirtyDataForStructFiledInToken { 84 | _ = k 85 | if k == "token" { 86 | data, err := proto.Marshal(&objToken.token) 87 | if err != nil { 88 | return err 89 | } 90 | objToken.dirtyDataInToken["token"] = data 91 | } 92 | } 93 | db := go_redis_orm.GetDB(objToken.ddbNameInToken) 94 | if _, err := db.Do("HMSET", redis.Args{}.Add(objToken.dbKeyInToken).AddFlat(objToken.dirtyDataInToken)...); err != nil { 95 | return err 96 | } 97 | if objToken.expireInToken != 0 { 98 | if _, err := db.Do("EXPIRE", objToken.dbKeyInToken, objToken.expireInToken); err != nil { 99 | return err 100 | } 101 | } 102 | objToken.dirtyDataInToken = make(map[string]interface{}) 103 | objToken.dirtyDataForStructFiledInToken = make(map[string]interface{}) 104 | return nil 105 | } 106 | 107 | // Delete : 从 redis 删除数据 108 | func (objToken *Token) Delete() error { 109 | db := go_redis_orm.GetDB(objToken.ddbNameInToken) 110 | _, err := db.Do("DEL", objToken.dbKeyInToken) 111 | if err == nil { 112 | objToken.isLoadInToken = false 113 | objToken.dirtyDataInToken = make(map[string]interface{}) 114 | objToken.dirtyDataForStructFiledInToken = make(map[string]interface{}) 115 | } 116 | return err 117 | } 118 | 119 | // IsLoad : 是否已经从 redis 导入数据 120 | func (objToken *Token) IsLoad() bool { 121 | return objToken.isLoadInToken 122 | } 123 | 124 | // Expire : 向 redis 设置该对象过期时间 125 | func (objToken *Token) Expire(v uint) { 126 | objToken.expireInToken = v 127 | } 128 | 129 | // DirtyData : 获取该对象目前已脏的数据 130 | func (objToken *Token) DirtyData() (map[string]interface{}, error) { 131 | for k := range objToken.dirtyDataForStructFiledInToken { 132 | _ = k 133 | if k == "token" { 134 | data, err := proto.Marshal(&objToken.token) 135 | if err != nil { 136 | return nil, err 137 | } 138 | objToken.dirtyDataInToken["token"] = data 139 | } 140 | } 141 | data := make(map[string]interface{}) 142 | for k, v := range objToken.dirtyDataInToken { 143 | data[k] = v 144 | } 145 | objToken.dirtyDataInToken = make(map[string]interface{}) 146 | objToken.dirtyDataForStructFiledInToken = make(map[string]interface{}) 147 | return data, nil 148 | } 149 | 150 | // Save2 : 保存数据到 redis 的第 2 种方法 151 | func (objToken *Token) Save2(dirtyData map[string]interface{}) error { 152 | if len(dirtyData) == 0 { 153 | return nil 154 | } 155 | db := go_redis_orm.GetDB(objToken.ddbNameInToken) 156 | if _, err := db.Do("HMSET", redis.Args{}.Add(objToken.dbKeyInToken).AddFlat(dirtyData)...); err != nil { 157 | return err 158 | } 159 | if objToken.expireInToken != 0 { 160 | if _, err := db.Do("EXPIRE", objToken.dbKeyInToken, objToken.expireInToken); err != nil { 161 | return err 162 | } 163 | } 164 | return nil 165 | } 166 | 167 | // GetToken : 获取字段值 168 | func (objToken *Token) GetToken(mutable bool) *DB_TOKEN { 169 | if mutable { 170 | objToken.dirtyDataForStructFiledInToken["token"] = nil 171 | } 172 | return &objToken.token 173 | } 174 | -------------------------------------------------------------------------------- /internal/db/token.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package db; 4 | import "common.proto"; 5 | 6 | message DB_TOKEN { 7 | string Token = 1; // 令牌 8 | map AllocServers = 2; // 已分配的服务 9 | } -------------------------------------------------------------------------------- /internal/protocol/common.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package protocol; 4 | 5 | // 服务器ID类型 6 | message SERVER_ID { 7 | uint32 ID = 1; 8 | } 9 | -------------------------------------------------------------------------------- /internal/protocol/g.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | docker run --rm -v $PWD:/out -w /out znly/protoc --gogofaster_out=. -I=. *.proto 6 | -------------------------------------------------------------------------------- /internal/protocol/gateway.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package protocol; 4 | import "common.proto"; 5 | 6 | message CMD_GW { 7 | enum ENUM { 8 | INVALID = 0; // 未定义 9 | PING = 1; // PING ( GATEWAY -> S )。实际上用的是 mgr.proto 的协议。这里占位下,避免协议号重复 10 | REGISTER_SERVER = 2; // 注册服务器信息 ( S -> GATEWAY -> S )。实际上用的是 mgr.proto 的协议。这里占位下,避免协议号重复 11 | LOSE_SERVER = 3; // 丢失服务器信息 ( GATEWAY -> S )。实际上用的是 mgr.proto 的协议。这里占位下,避免协议号重复 12 | REGISTER_ACCOUNT = 4; // 注册账号 ( GATEWAY -> S )。只通知账号对应分配的服务 13 | RELAY_CLIENT_MSG = 5; // 中继客户端消息 ( GATEWAY -> S / S -> GATEWAY ) 14 | LOSE_ACCOUNT = 6; // 丢失账号 ( GATEWAY -> S )。只通知账号对应分配的服务 15 | RELAY_SERVER_MSG1 = 7; // 中继服务器消息 ( S -> GATEWAY -> S) 16 | RELAY_SERVER_MSG2 = 8; // 中继服务器消息 ( S -> GATEWAY -> S) 17 | } 18 | } 19 | 20 | // PING ( GATEWAY -> S ) 21 | // 参见 mgr.proto 22 | 23 | // 注册服务器信息 ( S -> GATEWAY -> S ) 24 | // 参见 mgr.proto 25 | 26 | // 丢失服务器信息( GATEWAY -> S ) 27 | // 参见 mgr.proto 28 | 29 | // 注册账号 ( GATEWAY -> S )。只通知账号对应分配的服务 30 | message MSG_GW_REGISTER_ACCOUNT { 31 | string Account = 1; // 账号 32 | } 33 | 34 | // 中继客户端消息 ( GATEWAY -> S / S -> GATEWAY ) 35 | message MSG_GW_RELAY_CLIENT_MSG { 36 | string Account = 1; // 账号。S -> GATEWAY 时,该字段为空,则表示 GATEWAY 需要把该消息发给所有客户端 37 | uint32 CMD = 2; // 消息号(真实消息号,已脱掉消息偏移量) 38 | bytes Data = 3; // 数据 39 | uint32 Flag = 4; // 数据块标志 40 | } 41 | 42 | 43 | // 丢失账号 ( GATEWAY -> S )。只通知账号对应分配的服务 44 | message MSG_GW_LOSE_ACCOUNT { 45 | string Account = 1; // 账号 46 | } 47 | 48 | // 中继服务器消息 ( S -> GATEWAY -> S) 49 | message RELAY_SERVER_MSG_TYPE { 50 | enum ENUM { 51 | BROADCAST = 0; 52 | RANDOM = 1; 53 | } 54 | } 55 | message MSG_GW_RELAY_SERVER_MSG1 { 56 | SERVER_ID SourceID = 1; // 服务ID(源) 57 | uint32 SourceType = 2; // 服务类型(源) 58 | uint32 TargetType = 3; // 服务类型(目标) 59 | RELAY_SERVER_MSG_TYPE.ENUM SendType = 4;// 发送类型。 0 广播; 1 随机一个 60 | uint32 CMD = 5; // 消息号 61 | bytes Data = 6; // 数据 62 | uint32 Flag = 7; // 数据块标志 63 | } 64 | 65 | // 中继服务器消息 ( S -> GATEWAY -> S) 66 | message MSG_GW_RELAY_SERVER_MSG2 { 67 | SERVER_ID SourceID = 1; // 服务ID(源) 68 | uint32 SourceType = 2; // 服务类型(源) 69 | SERVER_ID TargetID = 3; // 服务ID(目标) 70 | uint32 CMD = 4; // 消息号 71 | bytes Data = 5; // 数据 72 | uint32 Flag = 6; // 数据块标志 73 | } 74 | -------------------------------------------------------------------------------- /internal/protocol/init.go: -------------------------------------------------------------------------------- 1 | //go:generate ./g.sh 2 | 3 | package protocol 4 | 5 | func init() {} 6 | -------------------------------------------------------------------------------- /internal/protocol/mgr.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package protocol; 4 | import "common.proto"; 5 | 6 | message CMD_MGR { 7 | enum ENUM { 8 | INVALID = 0; // 未定义 9 | PING = 1; // PING ( MGR -> S ) 10 | REGISTER_SERVER = 2; // 注册服务器信息 ( S -> MGR -> S ) 11 | LOSE_SERVER = 3; // 丢失服务器信息 ( MGR -> S ) 12 | } 13 | } 14 | 15 | // 服务器信息 16 | message SERVER_INFO { 17 | SERVER_ID Id = 1; // 服务ID 18 | uint32 Type = 2; // 服务类型 19 | repeated string Addrs = 3; // 地址,0 内网地址 ; 1 外网地址 20 | repeated int32 Ports = 4; // 端口号列表 21 | repeated uint32 Overload = 5; // 负载值列表 22 | string Version = 6; // 版本号,用于灰度更新 23 | } 24 | 25 | // PING ( MGR -> S ) 26 | message MSG_MGR_PING { 27 | } 28 | 29 | // 注册服务器信息( S -> MGR -> S ) 30 | message MSG_MGR_REGISTER_SERVER { 31 | SERVER_INFO Data = 1; // 服务器信息 32 | string Token = 2; // 令牌,注册验证用 33 | uint32 TargetServerType = 3; // 目标服务器类型,避免连接错误 34 | SERVER_ID TargetServerID = 4; // 目标服务器ID,避免连接错误。 不为 0 时需要判断下 35 | } 36 | 37 | // 丢失服务器信息( MGR -> S ) 38 | message MSG_MGR_LOSE_SERVER { 39 | SERVER_ID Id = 1; // 服务器ID 40 | uint32 Type = 2; // 服务类型 41 | } 42 | -------------------------------------------------------------------------------- /internal/utils/component.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // IComponent : 组件接口 4 | type IComponent interface { 5 | Start() bool 6 | Close() 7 | } 8 | -------------------------------------------------------------------------------- /internal/utils/ip_helper.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "os" 7 | "regexp" 8 | "sync" 9 | 10 | "github.com/fananchong/go-xserver/common" 11 | ) 12 | 13 | var ( 14 | ipinner string 15 | ipouter string 16 | onceipinner sync.Once 17 | onceipouter sync.Once 18 | ) 19 | 20 | // IPType : IP 类型 21 | type IPType int 22 | 23 | const ( 24 | // IPINNER : 类型 0 ,内网 IP 25 | IPINNER IPType = iota 26 | 27 | // IPOUTER : 类型 1 ,外网 IP 28 | IPOUTER 29 | ) 30 | 31 | // PortType : PORT 类型 32 | type PortType int 33 | 34 | const ( 35 | // PORTFORCLIENT : 端口类型(对客户端) 36 | PORTFORCLIENT PortType = iota 37 | 38 | // PORTFORINTRANET : 端口类型(对内网) 39 | PORTFORINTRANET 40 | ) 41 | 42 | // GetIPInner : 获取内网 IP 43 | func GetIPInner(ctx *common.Context) string { 44 | onceipinner.Do(func() { 45 | switch ctx.Config().Network.IPType { 46 | case 0: 47 | ip, err := networkCard2IP(ctx.Config().Network.IPInner) 48 | if err != nil { 49 | ctx.Errorln(err) 50 | os.Exit(1) 51 | } 52 | ipinner = ip 53 | default: 54 | ipinner = ctx.Config().Network.IPInner 55 | } 56 | }) 57 | return ipinner 58 | } 59 | 60 | // GetIPOuter : 获取外网 IP 61 | func GetIPOuter(ctx *common.Context) string { 62 | onceipouter.Do(func() { 63 | switch ctx.Config().Network.IPType { 64 | case 0: 65 | ip, err := networkCard2IP(ctx.Config().Network.IPOuter) 66 | if err != nil { 67 | ctx.Errorln(err) 68 | os.Exit(1) 69 | } 70 | ipouter = ip 71 | default: 72 | ipouter = ctx.Config().Network.IPOuter 73 | } 74 | }) 75 | return ipouter 76 | } 77 | 78 | // GetIP : 根据类型获取IP 79 | func GetIP(ctx *common.Context, t IPType) string { 80 | switch t { 81 | case IPINNER: 82 | return GetIPInner(ctx) 83 | case IPOUTER: 84 | return GetIPOuter(ctx) 85 | default: 86 | panic("unknow ip type.") 87 | } 88 | } 89 | 90 | func networkCard2IP(name string) (string, error) { 91 | nic, err := net.InterfaceByName(name) 92 | if err != nil { 93 | return "", err 94 | } 95 | addresses, err := nic.Addrs() 96 | if err != nil { 97 | return "", err 98 | } 99 | r, _ := regexp.Compile(`((25[0-5]|2[0-4]\d|((1\d{2})|([1-9]?\d)))\.){3}(25[0-5]|2[0-4]\d|((1\d{2})|([1-9]?\d)))`) 100 | for _, addr := range addresses { 101 | s := r.FindAllString(addr.String(), -1) 102 | if len(s) != 0 { 103 | return s[0], nil 104 | } 105 | } 106 | return "", errors.New("no find address. nic: " + name) 107 | } 108 | 109 | // GetIntranetListenPort : 获取服务器组内监听端口 110 | func GetIntranetListenPort(ctx *common.Context) int32 { 111 | return ctx.Config().Network.Port[PORTFORINTRANET] 112 | } 113 | 114 | // GetDefaultServicePort : 获取缺省的对外端口 115 | func GetDefaultServicePort(ctx *common.Context) int32 { 116 | return ctx.Config().Network.Port[PORTFORCLIENT] 117 | } 118 | -------------------------------------------------------------------------------- /internal/utils/ticker.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "runtime/debug" 6 | "time" 7 | 8 | "github.com/fananchong/go-xserver/common" 9 | ) 10 | 11 | // Ticker : 定时器帮助类 12 | type Ticker struct { 13 | ctx *common.Context 14 | ctxSub context.Context 15 | ctxCancel context.CancelFunc 16 | interval time.Duration 17 | f func() 18 | name string 19 | } 20 | 21 | // NewTickerHelper : 定时器帮助类的构造函数 22 | func NewTickerHelper(name string, ctx *common.Context, interval time.Duration, f func()) *Ticker { 23 | return &Ticker{ 24 | ctx: ctx, 25 | interval: interval, 26 | f: f, 27 | name: name, 28 | } 29 | } 30 | 31 | // Start : 开始 32 | func (helper *Ticker) Start() bool { 33 | helper.ctxSub, helper.ctxCancel = context.WithCancel(helper.ctx) 34 | go helper.loop() 35 | return true 36 | } 37 | 38 | func (helper *Ticker) loop() { 39 | timer := time.NewTicker(helper.interval) 40 | defer timer.Stop() 41 | for { 42 | select { 43 | case <-helper.ctxSub.Done(): 44 | helper.ctx.Infoln("Ticker[", helper.name, "], off") 45 | return 46 | case <-timer.C: 47 | func() { 48 | defer func() { 49 | if err := recover(); err != nil { 50 | helper.ctx.Errorln("Ticker[", helper.name, "] except:", err, "\n", string(debug.Stack())) 51 | } 52 | }() 53 | helper.f() 54 | }() 55 | } 56 | } 57 | } 58 | 59 | // Close : 结束 60 | func (helper *Ticker) Close() { 61 | if helper.ctxCancel != nil { 62 | helper.ctxCancel() 63 | helper.ctxCancel = nil 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | //go:generate sh -c "if [ ! -d default_plugins ]; then git clone https://github.com/fananchong/go-xserver-plugins.git && mv go-xserver-plugins default_plugins; fi" 2 | //go:generate sh -c "if [ -d default_plugins ]; then cd default_plugins && git fetch --all && git reset --hard origin/master && git pull; fi" 3 | //go:generate sh -c "cd default_plugins && sed -i 's#go-xserver-plugins#go-xserver/default_plugins#g' `grep 'go-xserver-plugins' -rl . --include *.go`" 4 | //go:generate sh -c "cd default_plugins && rm -rf go.*" 5 | package main 6 | 7 | import "github.com/fananchong/go-xserver/internal" 8 | 9 | func main() { 10 | app := internal.NewApp() 11 | app.Run() 12 | } 13 | -------------------------------------------------------------------------------- /make.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | CUR_DIR=$PWD 6 | SRC_DIR=$PWD 7 | BIN_DIR=$PWD/bin 8 | CONF_DIR=$PWD/config 9 | SERVICE_DIR=$PWD/services/ 10 | FLAGS=-race 11 | 12 | case $1 in 13 | "start") 14 | mkdir -p $CONF_DIR 15 | ln -sf $SRC_DIR/common/config/framework.toml $CONF_DIR/ 16 | ln -sf $SRC_DIR/default_plugins/login/login.toml $CONF_DIR/ 17 | cd $BIN_DIR 18 | mkdir -p $BIN_DIR/logs 19 | mkdir -p $BIN_DIR/logs.back 20 | if [[ `ls -l ./logs | wc -l` > 1 ]]; then 21 | mv -f ./logs/* ./logs.back/ 22 | fi 23 | plugins=`ls | grep .so | awk -F"." '{print $1}'` 24 | for plugin_name in $plugins; do 25 | c=1 26 | if [[ $plugin_name != "mgr" && $plugin_name != "match" ]]; then 27 | c=3 28 | fi 29 | for (( i=1; i<=$c; i++ )); do 30 | nohup ./go-xserver --app $plugin_name --suffix $i > /dev/null 2>&1 & 31 | done 32 | done 33 | sleep 1s 34 | ps -ux | grep go-xserver 35 | exit 0 36 | ;; 37 | "stop") 38 | pkill go-xserver 39 | sleep 5s 40 | ps -ux | grep go-xserver 41 | exit 0 42 | ;; 43 | "") 44 | export GOPROXY=https://goproxy.io 45 | cd $SRC_DIR 46 | go generate ./... 47 | cd $SRC_DIR 48 | go vet ./... 49 | echo "start build ..." 50 | cd $SRC_DIR/default_plugins 51 | plugins=`ls -l | grep '^d' | awk '{print $9}' | grep -v 'internal\|bin'` 52 | for plugin_name in $plugins; do 53 | go build $FLAGS -buildmode=plugin -o $BIN_DIR/$plugin_name.so ./$plugin_name; 54 | done 55 | cd $SERVICE_DIR 56 | plugins=`ls -l | grep '^d' | awk '{print $9}' | grep -v 'internal'` 57 | for plugin_name in $plugins; do 58 | go build $FLAGS -buildmode=plugin -o $BIN_DIR/$plugin_name.so ./$plugin_name; 59 | done 60 | cd $SRC_DIR 61 | go build $FLAGS -o $BIN_DIR/go-xserver . 62 | echo "done" 63 | exit 0 64 | ;; 65 | esac 66 | 67 | echo "Usage:" 68 | echo " make.sh" 69 | echo " make.sh start" 70 | echo " make.sh stop" 71 | 72 | cd $CUR_DIR 73 | 74 | exit 0 75 | 76 | -------------------------------------------------------------------------------- /services/internal/db/g.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | docker run --rm -v $PWD/../:/out -w /out/db znly/protoc --gogofaster_out=. -I=. -I=../protocol *.proto 6 | docker run --rm -v $PWD/redis_def:/app/input -v $PWD:/app/output fananchong/redis2go --package=db 7 | -------------------------------------------------------------------------------- /services/internal/db/init.go: -------------------------------------------------------------------------------- 1 | //go:generate ./g.sh 2 | 3 | package db 4 | 5 | func init() {} 6 | -------------------------------------------------------------------------------- /services/internal/db/redis_def/rolebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RoleBase", 3 | "type": "1-1", 4 | "key": "uint64", 5 | "struct_format": "gogo", 6 | "fields": { 7 | "name": "string" 8 | } 9 | } -------------------------------------------------------------------------------- /services/internal/db/redis_def/rolelist.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RoleList", 3 | "type": "1-1", 4 | "key": "string", 5 | "struct_format": "gogo", 6 | "fields": { 7 | "roles": "DB_ROLELIST" 8 | } 9 | } -------------------------------------------------------------------------------- /services/internal/db/rolebase.go: -------------------------------------------------------------------------------- 1 | /// ------------------------------------------------------------------------------- 2 | /// THIS FILE IS ORIGINALLY GENERATED BY redis2go.exe. 3 | /// PLEASE DO NOT MODIFY THIS FILE. 4 | /// ------------------------------------------------------------------------------- 5 | 6 | package db 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | 12 | go_redis_orm "github.com/fananchong/go-redis-orm.v2" 13 | "github.com/gomodule/redigo/redis" 14 | ) 15 | 16 | // RoleBase : 代表 1 个 redis 对象 17 | type RoleBase struct { 18 | Key uint64 19 | name string 20 | 21 | dirtyDataInRoleBase map[string]interface{} 22 | dirtyDataForStructFiledInRoleBase map[string]interface{} 23 | isLoadInRoleBase bool 24 | dbKeyInRoleBase string 25 | ddbNameInRoleBase string 26 | expireInRoleBase uint 27 | } 28 | 29 | // NewRoleBase : NewRoleBase 的构造函数 30 | func NewRoleBase(dbName string, key uint64) *RoleBase { 31 | return &RoleBase{ 32 | Key: key, 33 | ddbNameInRoleBase: dbName, 34 | dbKeyInRoleBase: "RoleBase:" + fmt.Sprintf("%d", key), 35 | dirtyDataInRoleBase: make(map[string]interface{}), 36 | dirtyDataForStructFiledInRoleBase: make(map[string]interface{}), 37 | } 38 | } 39 | 40 | // HasKey : 是否存在 KEY 41 | // 返回值,若访问数据库失败返回-1;若 key 存在返回 1 ,否则返回 0 。 42 | func (objRoleBase *RoleBase) HasKey() (int, error) { 43 | db := go_redis_orm.GetDB(objRoleBase.ddbNameInRoleBase) 44 | val, err := redis.Int(db.Do("EXISTS", objRoleBase.dbKeyInRoleBase)) 45 | if err != nil { 46 | return -1, err 47 | } 48 | return val, nil 49 | } 50 | 51 | // Load : 从 redis 加载数据 52 | func (objRoleBase *RoleBase) Load() error { 53 | if objRoleBase.isLoadInRoleBase == true { 54 | return errors.New("alreay load") 55 | } 56 | db := go_redis_orm.GetDB(objRoleBase.ddbNameInRoleBase) 57 | val, err := redis.Values(db.Do("HGETALL", objRoleBase.dbKeyInRoleBase)) 58 | if err != nil { 59 | return err 60 | } 61 | if len(val) == 0 { 62 | return go_redis_orm.ERR_ISNOT_EXIST_KEY 63 | } 64 | var data struct { 65 | Name string `redis:"name"` 66 | } 67 | if err := redis.ScanStruct(val, &data); err != nil { 68 | return err 69 | } 70 | objRoleBase.name = data.Name 71 | objRoleBase.isLoadInRoleBase = true 72 | return nil 73 | } 74 | 75 | // Save : 保存数据到 redis 76 | func (objRoleBase *RoleBase) Save() error { 77 | if len(objRoleBase.dirtyDataInRoleBase) == 0 && len(objRoleBase.dirtyDataForStructFiledInRoleBase) == 0 { 78 | return nil 79 | } 80 | for k := range objRoleBase.dirtyDataForStructFiledInRoleBase { 81 | _ = k 82 | 83 | } 84 | db := go_redis_orm.GetDB(objRoleBase.ddbNameInRoleBase) 85 | if _, err := db.Do("HMSET", redis.Args{}.Add(objRoleBase.dbKeyInRoleBase).AddFlat(objRoleBase.dirtyDataInRoleBase)...); err != nil { 86 | return err 87 | } 88 | if objRoleBase.expireInRoleBase != 0 { 89 | if _, err := db.Do("EXPIRE", objRoleBase.dbKeyInRoleBase, objRoleBase.expireInRoleBase); err != nil { 90 | return err 91 | } 92 | } 93 | objRoleBase.dirtyDataInRoleBase = make(map[string]interface{}) 94 | objRoleBase.dirtyDataForStructFiledInRoleBase = make(map[string]interface{}) 95 | return nil 96 | } 97 | 98 | // Delete : 从 redis 删除数据 99 | func (objRoleBase *RoleBase) Delete() error { 100 | db := go_redis_orm.GetDB(objRoleBase.ddbNameInRoleBase) 101 | _, err := db.Do("DEL", objRoleBase.dbKeyInRoleBase) 102 | if err == nil { 103 | objRoleBase.isLoadInRoleBase = false 104 | objRoleBase.dirtyDataInRoleBase = make(map[string]interface{}) 105 | objRoleBase.dirtyDataForStructFiledInRoleBase = make(map[string]interface{}) 106 | } 107 | return err 108 | } 109 | 110 | // IsLoad : 是否已经从 redis 导入数据 111 | func (objRoleBase *RoleBase) IsLoad() bool { 112 | return objRoleBase.isLoadInRoleBase 113 | } 114 | 115 | // Expire : 向 redis 设置该对象过期时间 116 | func (objRoleBase *RoleBase) Expire(v uint) { 117 | objRoleBase.expireInRoleBase = v 118 | } 119 | 120 | // DirtyData : 获取该对象目前已脏的数据 121 | func (objRoleBase *RoleBase) DirtyData() (map[string]interface{}, error) { 122 | for k := range objRoleBase.dirtyDataForStructFiledInRoleBase { 123 | _ = k 124 | 125 | } 126 | data := make(map[string]interface{}) 127 | for k, v := range objRoleBase.dirtyDataInRoleBase { 128 | data[k] = v 129 | } 130 | objRoleBase.dirtyDataInRoleBase = make(map[string]interface{}) 131 | objRoleBase.dirtyDataForStructFiledInRoleBase = make(map[string]interface{}) 132 | return data, nil 133 | } 134 | 135 | // Save2 : 保存数据到 redis 的第 2 种方法 136 | func (objRoleBase *RoleBase) Save2(dirtyData map[string]interface{}) error { 137 | if len(dirtyData) == 0 { 138 | return nil 139 | } 140 | db := go_redis_orm.GetDB(objRoleBase.ddbNameInRoleBase) 141 | if _, err := db.Do("HMSET", redis.Args{}.Add(objRoleBase.dbKeyInRoleBase).AddFlat(dirtyData)...); err != nil { 142 | return err 143 | } 144 | if objRoleBase.expireInRoleBase != 0 { 145 | if _, err := db.Do("EXPIRE", objRoleBase.dbKeyInRoleBase, objRoleBase.expireInRoleBase); err != nil { 146 | return err 147 | } 148 | } 149 | return nil 150 | } 151 | 152 | // GetName : 获取字段值 153 | func (objRoleBase *RoleBase) GetName() string { 154 | return objRoleBase.name 155 | } 156 | 157 | // SetName : 设置字段值 158 | func (objRoleBase *RoleBase) SetName(value string) { 159 | objRoleBase.name = value 160 | objRoleBase.dirtyDataInRoleBase["name"] = string([]byte(value)) 161 | } 162 | -------------------------------------------------------------------------------- /services/internal/db/rolelist.go: -------------------------------------------------------------------------------- 1 | /// ------------------------------------------------------------------------------- 2 | /// THIS FILE IS ORIGINALLY GENERATED BY redis2go.exe. 3 | /// PLEASE DO NOT MODIFY THIS FILE. 4 | /// ------------------------------------------------------------------------------- 5 | 6 | package db 7 | 8 | import ( 9 | "errors" 10 | 11 | go_redis_orm "github.com/fananchong/go-redis-orm.v2" 12 | "github.com/gogo/protobuf/proto" 13 | "github.com/gomodule/redigo/redis" 14 | ) 15 | 16 | // RoleList : 代表 1 个 redis 对象 17 | type RoleList struct { 18 | Key string 19 | roles DB_ROLELIST 20 | 21 | dirtyDataInRoleList map[string]interface{} 22 | dirtyDataForStructFiledInRoleList map[string]interface{} 23 | isLoadInRoleList bool 24 | dbKeyInRoleList string 25 | ddbNameInRoleList string 26 | expireInRoleList uint 27 | } 28 | 29 | // NewRoleList : NewRoleList 的构造函数 30 | func NewRoleList(dbName string, key string) *RoleList { 31 | return &RoleList{ 32 | Key: key, 33 | ddbNameInRoleList: dbName, 34 | dbKeyInRoleList: "RoleList:" + key, 35 | dirtyDataInRoleList: make(map[string]interface{}), 36 | dirtyDataForStructFiledInRoleList: make(map[string]interface{}), 37 | } 38 | } 39 | 40 | // HasKey : 是否存在 KEY 41 | // 返回值,若访问数据库失败返回-1;若 key 存在返回 1 ,否则返回 0 。 42 | func (objRoleList *RoleList) HasKey() (int, error) { 43 | db := go_redis_orm.GetDB(objRoleList.ddbNameInRoleList) 44 | val, err := redis.Int(db.Do("EXISTS", objRoleList.dbKeyInRoleList)) 45 | if err != nil { 46 | return -1, err 47 | } 48 | return val, nil 49 | } 50 | 51 | // Load : 从 redis 加载数据 52 | func (objRoleList *RoleList) Load() error { 53 | if objRoleList.isLoadInRoleList == true { 54 | return errors.New("alreay load") 55 | } 56 | db := go_redis_orm.GetDB(objRoleList.ddbNameInRoleList) 57 | val, err := redis.Values(db.Do("HGETALL", objRoleList.dbKeyInRoleList)) 58 | if err != nil { 59 | return err 60 | } 61 | if len(val) == 0 { 62 | return go_redis_orm.ERR_ISNOT_EXIST_KEY 63 | } 64 | var data struct { 65 | Roles []byte `redis:"roles"` 66 | } 67 | if err := redis.ScanStruct(val, &data); err != nil { 68 | return err 69 | } 70 | if err := proto.Unmarshal(data.Roles, &objRoleList.roles); err != nil { 71 | return err 72 | } 73 | 74 | objRoleList.isLoadInRoleList = true 75 | return nil 76 | } 77 | 78 | // Save : 保存数据到 redis 79 | func (objRoleList *RoleList) Save() error { 80 | if len(objRoleList.dirtyDataInRoleList) == 0 && len(objRoleList.dirtyDataForStructFiledInRoleList) == 0 { 81 | return nil 82 | } 83 | for k := range objRoleList.dirtyDataForStructFiledInRoleList { 84 | _ = k 85 | if k == "roles" { 86 | data, err := proto.Marshal(&objRoleList.roles) 87 | if err != nil { 88 | return err 89 | } 90 | objRoleList.dirtyDataInRoleList["roles"] = data 91 | } 92 | } 93 | db := go_redis_orm.GetDB(objRoleList.ddbNameInRoleList) 94 | if _, err := db.Do("HMSET", redis.Args{}.Add(objRoleList.dbKeyInRoleList).AddFlat(objRoleList.dirtyDataInRoleList)...); err != nil { 95 | return err 96 | } 97 | if objRoleList.expireInRoleList != 0 { 98 | if _, err := db.Do("EXPIRE", objRoleList.dbKeyInRoleList, objRoleList.expireInRoleList); err != nil { 99 | return err 100 | } 101 | } 102 | objRoleList.dirtyDataInRoleList = make(map[string]interface{}) 103 | objRoleList.dirtyDataForStructFiledInRoleList = make(map[string]interface{}) 104 | return nil 105 | } 106 | 107 | // Delete : 从 redis 删除数据 108 | func (objRoleList *RoleList) Delete() error { 109 | db := go_redis_orm.GetDB(objRoleList.ddbNameInRoleList) 110 | _, err := db.Do("DEL", objRoleList.dbKeyInRoleList) 111 | if err == nil { 112 | objRoleList.isLoadInRoleList = false 113 | objRoleList.dirtyDataInRoleList = make(map[string]interface{}) 114 | objRoleList.dirtyDataForStructFiledInRoleList = make(map[string]interface{}) 115 | } 116 | return err 117 | } 118 | 119 | // IsLoad : 是否已经从 redis 导入数据 120 | func (objRoleList *RoleList) IsLoad() bool { 121 | return objRoleList.isLoadInRoleList 122 | } 123 | 124 | // Expire : 向 redis 设置该对象过期时间 125 | func (objRoleList *RoleList) Expire(v uint) { 126 | objRoleList.expireInRoleList = v 127 | } 128 | 129 | // DirtyData : 获取该对象目前已脏的数据 130 | func (objRoleList *RoleList) DirtyData() (map[string]interface{}, error) { 131 | for k := range objRoleList.dirtyDataForStructFiledInRoleList { 132 | _ = k 133 | if k == "roles" { 134 | data, err := proto.Marshal(&objRoleList.roles) 135 | if err != nil { 136 | return nil, err 137 | } 138 | objRoleList.dirtyDataInRoleList["roles"] = data 139 | } 140 | } 141 | data := make(map[string]interface{}) 142 | for k, v := range objRoleList.dirtyDataInRoleList { 143 | data[k] = v 144 | } 145 | objRoleList.dirtyDataInRoleList = make(map[string]interface{}) 146 | objRoleList.dirtyDataForStructFiledInRoleList = make(map[string]interface{}) 147 | return data, nil 148 | } 149 | 150 | // Save2 : 保存数据到 redis 的第 2 种方法 151 | func (objRoleList *RoleList) Save2(dirtyData map[string]interface{}) error { 152 | if len(dirtyData) == 0 { 153 | return nil 154 | } 155 | db := go_redis_orm.GetDB(objRoleList.ddbNameInRoleList) 156 | if _, err := db.Do("HMSET", redis.Args{}.Add(objRoleList.dbKeyInRoleList).AddFlat(dirtyData)...); err != nil { 157 | return err 158 | } 159 | if objRoleList.expireInRoleList != 0 { 160 | if _, err := db.Do("EXPIRE", objRoleList.dbKeyInRoleList, objRoleList.expireInRoleList); err != nil { 161 | return err 162 | } 163 | } 164 | return nil 165 | } 166 | 167 | // GetRoles : 获取字段值 168 | func (objRoleList *RoleList) GetRoles(mutable bool) *DB_ROLELIST { 169 | if mutable { 170 | objRoleList.dirtyDataForStructFiledInRoleList["roles"] = nil 171 | } 172 | return &objRoleList.roles 173 | } 174 | -------------------------------------------------------------------------------- /services/internal/db/rolelist.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package db; 4 | 5 | message DB_ROLELIST { 6 | map RoleIDs = 1; // 角色 ID 列表 7 | } -------------------------------------------------------------------------------- /services/internal/protocol/g.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | docker run --rm -v $PWD:/out -w /out znly/protoc --gogofaster_out=. -I=. *.proto 6 | docker run --rm -v $PWD:/out -w /out znly/protoc --python_out=. -I=. *.proto 7 | sed -i 's/import lobby_custom_pb2/import protocol.lobby_custom_pb2/g' ./lobby_pb2.py 8 | 9 | -------------------------------------------------------------------------------- /services/internal/protocol/init.go: -------------------------------------------------------------------------------- 1 | //go:generate ./g.sh 2 | 3 | package protocol 4 | 5 | func init() {} 6 | -------------------------------------------------------------------------------- /services/internal/protocol/lobby.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package protocol; 4 | import "lobby_custom.proto"; 5 | 6 | message CMD_LOBBY { 7 | enum ENUM { 8 | INVALID = 0; // 未定义 9 | LOGIN = 1; // 登录,获取角色信息 ( C -> LOBBY ) 10 | CREATE_ROLE = 2; // 创建角色 ( C -> LOBBY ) 11 | ENTER_GAME = 3; // 进入游戏 ( C -> LOBBY ) 12 | CHAT = 4; // 聊天 ( C -> LOBBY -> C ) 13 | MATCH = 5; // 匹配 ( C -> LOBBY ) 14 | MATCH_RESULT = 6; // 匹配 ( LOBBY -> C ) 15 | MSGCMDOFFSET = 4000; // 消息号偏移量。如发送 `MSG_LOBBY_LOGIN` 消息时,实际发送消息号为: MSGCMDOFFSET + LOGIN 16 | } 17 | } 18 | 19 | message ENUM_LOBBY_COMMON_ERROR { 20 | enum ENUM { 21 | OK = 0; // 没有错误 22 | SYSTEM_ERROR = 1; // 系统错误 23 | DUPLICATION_ROLE_NAME = 2; // 角色名重复 24 | } 25 | } 26 | 27 | // 登录,获取角色信息 ( C -> LOBBY ) 28 | message MSG_LOBBY_LOGIN { 29 | } 30 | 31 | message MSG_LOBBY_LOGIN_RESULT { 32 | ENUM_LOBBY_COMMON_ERROR.ENUM Err = 1; // 0:成功;非0错误号 33 | repeated ROLE_BASE_INFO Roles = 2; // 角色列表 34 | } 35 | 36 | // 创建角色 ( C -> LOBBY ) 37 | message MSG_LOBBY_CREATE_ROLE { 38 | uint32 Slot = 1; // 角色所在的槽位。如每个账号共有 3 个角色,则槽位值为 0 1 2 39 | ROLE_BASE_INFO Info = 2; // 创建角色所需的基本信息 40 | } 41 | 42 | message MSG_LOBBY_CREATE_ROLE_RESULT { 43 | ENUM_LOBBY_COMMON_ERROR.ENUM Err = 1; // 0:成功;非0错误号 44 | } 45 | 46 | // 进入游戏 ( C -> LOBBY ) 47 | message MSG_LOBBY_ENTER_GAME { 48 | uint32 Slot = 1; // 角色所在的槽位。如每个账号共有 3 个角色,则槽位值为 0 1 2 49 | } 50 | 51 | message MSG_LOBBY_ENTER_GAME_RESULT { 52 | ENUM_LOBBY_COMMON_ERROR.ENUM Err = 1; // 0:成功;非0错误号 53 | ROLE_DETAIL_INFO DetailInfo = 2; // 角色详细信息 54 | } 55 | 56 | // 聊天 ( C -> LOBBY -> C ) 57 | message MSG_LOBBY_CHAT { 58 | string From = 1; // 发聊天的角色角色名 59 | string To = 2; // 目标角色角色名,空则全服广播 60 | string Txt = 3; // 聊天内容 61 | } 62 | 63 | // 匹配 ( C -> LOBBY ) 64 | message MSG_LOBBY_MATCH { 65 | } 66 | 67 | // 匹配 ( LOBBY -> C ) 68 | message MSG_LOBBY_MATCH_RESULT { 69 | ENUM_LOBBY_COMMON_ERROR.ENUM Err = 1; // 0:成功;非0错误号 70 | repeated ROLE_BASE_INFO Roles = 2; // 角色列表 71 | } 72 | -------------------------------------------------------------------------------- /services/internal/protocol/lobby_custom.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package protocol; 4 | 5 | // 角色基本信息 6 | message ROLE_BASE_INFO { 7 | uint64 RoleID = 1; // 角色ID 8 | string RoleName = 2; // 角色名 9 | } 10 | 11 | 12 | // 角色详细信息(不是角色所有数据!一般主界面上的角色数据就放这里,比如游戏币、体力等等。) 13 | message ROLE_DETAIL_INFO { 14 | ROLE_BASE_INFO BaseInfo = 1; // 角色基本信息 15 | } -------------------------------------------------------------------------------- /services/internal/protocol/lobby_custom_pb2.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # source: lobby_custom.proto 3 | 4 | import sys 5 | _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import message as _message 8 | from google.protobuf import reflection as _reflection 9 | from google.protobuf import symbol_database as _symbol_database 10 | from google.protobuf import descriptor_pb2 11 | # @@protoc_insertion_point(imports) 12 | 13 | _sym_db = _symbol_database.Default() 14 | 15 | 16 | 17 | 18 | DESCRIPTOR = _descriptor.FileDescriptor( 19 | name='lobby_custom.proto', 20 | package='protocol', 21 | syntax='proto3', 22 | serialized_pb=_b('\n\x12lobby_custom.proto\x12\x08protocol\"2\n\x0eROLE_BASE_INFO\x12\x0e\n\x06RoleID\x18\x01 \x01(\x04\x12\x10\n\x08RoleName\x18\x02 \x01(\t\">\n\x10ROLE_DETAIL_INFO\x12*\n\x08\x42\x61seInfo\x18\x01 \x01(\x0b\x32\x18.protocol.ROLE_BASE_INFOb\x06proto3') 23 | ) 24 | 25 | 26 | 27 | 28 | _ROLE_BASE_INFO = _descriptor.Descriptor( 29 | name='ROLE_BASE_INFO', 30 | full_name='protocol.ROLE_BASE_INFO', 31 | filename=None, 32 | file=DESCRIPTOR, 33 | containing_type=None, 34 | fields=[ 35 | _descriptor.FieldDescriptor( 36 | name='RoleID', full_name='protocol.ROLE_BASE_INFO.RoleID', index=0, 37 | number=1, type=4, cpp_type=4, label=1, 38 | has_default_value=False, default_value=0, 39 | message_type=None, enum_type=None, containing_type=None, 40 | is_extension=False, extension_scope=None, 41 | options=None), 42 | _descriptor.FieldDescriptor( 43 | name='RoleName', full_name='protocol.ROLE_BASE_INFO.RoleName', index=1, 44 | number=2, type=9, cpp_type=9, label=1, 45 | has_default_value=False, default_value=_b("").decode('utf-8'), 46 | message_type=None, enum_type=None, containing_type=None, 47 | is_extension=False, extension_scope=None, 48 | options=None), 49 | ], 50 | extensions=[ 51 | ], 52 | nested_types=[], 53 | enum_types=[ 54 | ], 55 | options=None, 56 | is_extendable=False, 57 | syntax='proto3', 58 | extension_ranges=[], 59 | oneofs=[ 60 | ], 61 | serialized_start=32, 62 | serialized_end=82, 63 | ) 64 | 65 | 66 | _ROLE_DETAIL_INFO = _descriptor.Descriptor( 67 | name='ROLE_DETAIL_INFO', 68 | full_name='protocol.ROLE_DETAIL_INFO', 69 | filename=None, 70 | file=DESCRIPTOR, 71 | containing_type=None, 72 | fields=[ 73 | _descriptor.FieldDescriptor( 74 | name='BaseInfo', full_name='protocol.ROLE_DETAIL_INFO.BaseInfo', index=0, 75 | number=1, type=11, cpp_type=10, label=1, 76 | has_default_value=False, default_value=None, 77 | message_type=None, enum_type=None, containing_type=None, 78 | is_extension=False, extension_scope=None, 79 | options=None), 80 | ], 81 | extensions=[ 82 | ], 83 | nested_types=[], 84 | enum_types=[ 85 | ], 86 | options=None, 87 | is_extendable=False, 88 | syntax='proto3', 89 | extension_ranges=[], 90 | oneofs=[ 91 | ], 92 | serialized_start=84, 93 | serialized_end=146, 94 | ) 95 | 96 | _ROLE_DETAIL_INFO.fields_by_name['BaseInfo'].message_type = _ROLE_BASE_INFO 97 | DESCRIPTOR.message_types_by_name['ROLE_BASE_INFO'] = _ROLE_BASE_INFO 98 | DESCRIPTOR.message_types_by_name['ROLE_DETAIL_INFO'] = _ROLE_DETAIL_INFO 99 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 100 | 101 | ROLE_BASE_INFO = _reflection.GeneratedProtocolMessageType('ROLE_BASE_INFO', (_message.Message,), dict( 102 | DESCRIPTOR = _ROLE_BASE_INFO, 103 | __module__ = 'lobby_custom_pb2' 104 | # @@protoc_insertion_point(class_scope:protocol.ROLE_BASE_INFO) 105 | )) 106 | _sym_db.RegisterMessage(ROLE_BASE_INFO) 107 | 108 | ROLE_DETAIL_INFO = _reflection.GeneratedProtocolMessageType('ROLE_DETAIL_INFO', (_message.Message,), dict( 109 | DESCRIPTOR = _ROLE_DETAIL_INFO, 110 | __module__ = 'lobby_custom_pb2' 111 | # @@protoc_insertion_point(class_scope:protocol.ROLE_DETAIL_INFO) 112 | )) 113 | _sym_db.RegisterMessage(ROLE_DETAIL_INFO) 114 | 115 | 116 | # @@protoc_insertion_point(module_scope) 117 | -------------------------------------------------------------------------------- /services/internal/protocol/match.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package protocol; 4 | import "lobby_custom.proto"; 5 | 6 | message CMD_MATCH { 7 | enum ENUM { 8 | INVALID = 0; // 未定义 9 | MATCH = 1; // 请求匹配 ( LOBBY -> MATCH ) 10 | } 11 | } 12 | 13 | message ENUM_MATCH_COMMON_ERROR { 14 | enum ENUM { 15 | OK = 0; // 没有错误 16 | SYSTEM_ERROR = 1; // 系统错误 17 | } 18 | } 19 | 20 | // 请求匹配 ( LOBBY -> MATCH ) 21 | message MSG_MATCH_MATCH { 22 | string Account = 1; // 账号 23 | uint64 RoleID = 2; // 角色ID 24 | } 25 | 26 | message MSG_MATCH_MATCH_RESULT { 27 | ENUM_MATCH_COMMON_ERROR.ENUM Err = 1; // 0:成功;非0错误号 28 | string Account = 2; // 账号 29 | uint64 RoleID = 3; // 角色ID 30 | repeated ROLE_BASE_INFO Roles = 4; // 角色列表 31 | } 32 | -------------------------------------------------------------------------------- /services/internal/utility/send_msg.go: -------------------------------------------------------------------------------- 1 | package utility 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fananchong/go-xserver/common" 7 | "github.com/fananchong/go-xserver/common/config" 8 | "github.com/fananchong/go-xserver/common/context" 9 | "github.com/fananchong/gotcp" 10 | "github.com/gogo/protobuf/proto" 11 | ) 12 | 13 | // 框架层使用 []byte 数据块作为参数 14 | // 逻辑层可以自定义协议格式,这里用的是 protobuf 15 | // 封装下接口,方便代码调用 16 | 17 | // SendMsgToClient : 发送数据给客户端 18 | func SendMsgToClient(ctx *common.Context, account string, cmd uint64, msg proto.Message) (bool, error) { 19 | data, flag, err := gotcp.Encode(cmd, msg) 20 | if err != nil { 21 | return false, err 22 | } 23 | if ctx.SendMsgToClient(account, cmd, data, flag) { 24 | return true, nil 25 | } 26 | return false, fmt.Errorf("Sending message failed, account: %s, cmd:%d", account, cmd) 27 | } 28 | 29 | // SendMsgToClientByRoleName : 发送数据给客户端 30 | func SendMsgToClientByRoleName(ctx *common.Context, roleName string, cmd uint64, msg proto.Message) (bool, error) { 31 | account := ctx.IRole2Account.GetAndActive(roleName) 32 | if account != "" { 33 | return SendMsgToClient(ctx, account, cmd, msg) 34 | } 35 | return false, fmt.Errorf("Sending message failed, roleName: %s, cmd:%d, account:%s", roleName, cmd, account) 36 | } 37 | 38 | // BroadcastMsgToClient : 广播数据给客户端 39 | func BroadcastMsgToClient(ctx *common.Context, cmd uint64, msg proto.Message) (bool, error) { 40 | data, flag, err := gotcp.Encode(cmd, msg) 41 | if err != nil { 42 | return false, err 43 | } 44 | if ctx.BroadcastMsgToClient(cmd, data, flag) { 45 | return true, nil 46 | } 47 | return false, fmt.Errorf("Broadcast message failed, cmd:%d", cmd) 48 | } 49 | 50 | // SendMsgToServer : 发送消息给某类型服务(随机一个) 51 | func SendMsgToServer(ctx *common.Context, t config.NodeType, cmd uint64, msg proto.Message) (bool, error) { 52 | data, flag, err := gotcp.Encode(cmd, msg) 53 | if err != nil { 54 | return false, err 55 | } 56 | if ctx.SendMsgToServer(t, cmd, data, flag) { 57 | return true, nil 58 | } 59 | return false, fmt.Errorf("SendMsgToServer message failed, cmd:%d", cmd) 60 | } 61 | 62 | // ReplyMsgToServer : 回发消息给请求服务器 63 | func ReplyMsgToServer(ctx *common.Context, targetID context.NodeID, cmd uint64, msg proto.Message) (bool, error) { 64 | data, flag, err := gotcp.Encode(cmd, msg) 65 | if err != nil { 66 | return false, err 67 | } 68 | if ctx.ReplyMsgToServer(targetID, cmd, data, flag) { 69 | return true, nil 70 | } 71 | return false, fmt.Errorf("ReplyMsgToServer message failed, cmd:%d", cmd) 72 | } 73 | 74 | // BroadcastMsgToServer : 广播消息给某类型服务 75 | func BroadcastMsgToServer(ctx *common.Context, t config.NodeType, cmd uint64, msg proto.Message) (bool, error) { 76 | data, flag, err := gotcp.Encode(cmd, msg) 77 | if err != nil { 78 | return false, err 79 | } 80 | if ctx.BroadcastMsgToServer(t, cmd, data, flag) { 81 | return true, nil 82 | } 83 | return false, fmt.Errorf("BroadcastMsgToServer message failed, cmd:%d", cmd) 84 | } 85 | -------------------------------------------------------------------------------- /services/lobby/account.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "runtime/debug" 6 | "sync/atomic" 7 | 8 | go_redis_orm "github.com/fananchong/go-redis-orm.v2" 9 | "github.com/fananchong/go-xserver/services/internal/db" 10 | ) 11 | 12 | // LimitRoleNum : 一个账号角色数限制,最大为 1 个。 13 | const LimitRoleNum = 1 14 | 15 | // Account : 账号类 16 | type Account struct { 17 | *db.RoleList 18 | account string // 账号字符串 19 | roles []*Role // 角色列表 20 | chanMsg chan ChanMsg // 用于接收该账号消息 21 | chanClose chan int // 用于接收该账号消息循环 22 | closeFlag int32 // 标志该账号是否结束会话 23 | currentRole *Role // 该账号当前角色 24 | inited bool // 该账号是否已经初始化完毕 25 | } 26 | 27 | // NewAccount : 角色列表类构造函数 28 | func NewAccount(account string) *Account { 29 | accountObj := &Account{ 30 | account: account, 31 | chanMsg: make(chan ChanMsg, 1024), 32 | chanClose: make(chan int, 1), 33 | RoleList: db.NewRoleList(Ctx.Config().DbAccount.Name, account), 34 | } 35 | return accountObj 36 | } 37 | 38 | // Init : 账号初始化 39 | func (accountObj *Account) Init() error { 40 | account := accountObj.account 41 | if err := accountObj.RoleList.Load(); err != nil { 42 | if err != go_redis_orm.ERR_ISNOT_EXIST_KEY { 43 | return err 44 | } 45 | if accountObj.FirstInitialization() == false { 46 | return fmt.Errorf("Account failed to initialize for the first time, account:%s", account) 47 | } 48 | } 49 | accountObj.roles = make([]*Role, LimitRoleNum) 50 | ids := accountObj.RoleList.GetRoles(false).GetRoleIDs() 51 | for i := 0; i < LimitRoleNum; i++ { 52 | if roleID, ok := ids[uint32(i)]; ok { 53 | role, err := NewRole(roleID, account) 54 | if err != nil { 55 | return err 56 | } 57 | accountObj.roles[i] = role 58 | } else { 59 | accountObj.roles[i] = nil 60 | } 61 | } 62 | accountObj.inited = true 63 | return nil 64 | } 65 | 66 | // Start : 开始 67 | func (accountObj *Account) Start() { 68 | Ctx.Infoln("Account work coroutine start, account:", accountObj.account) 69 | go func() { 70 | defer func() { 71 | if err := recover(); err != nil { 72 | Ctx.Errorln("[except] ", err, "\n", string(debug.Stack())) 73 | } 74 | }() 75 | for { 76 | select { 77 | case msg := <-accountObj.chanMsg: 78 | accountObj.processMsg(msg.Cmd, msg.Data, msg.Flag) 79 | case <-accountObj.chanClose: 80 | return 81 | } 82 | } 83 | }() 84 | } 85 | 86 | // Close : 结束 87 | func (accountObj *Account) Close() { 88 | Ctx.Infoln("Account work coroutine close#1, account:", accountObj.account) 89 | atomic.StoreInt32(&accountObj.closeFlag, 1) 90 | accountObj.chanClose <- 1 91 | Ctx.Infoln("Account work coroutine close#2, account:", accountObj.account) 92 | } 93 | 94 | // FirstInitialization : 账号首次创建初始化 95 | func (accountObj *Account) FirstInitialization() bool { 96 | return true 97 | } 98 | 99 | // IsColse : 是否已经关闭 100 | func (accountObj *Account) IsColse() bool { 101 | flag := atomic.LoadInt32(&accountObj.closeFlag) 102 | return flag != 0 103 | } 104 | 105 | // GetRoles : 获取账号对应的角色列表 106 | func (accountObj *Account) GetRoles() []*Role { 107 | return accountObj.roles 108 | } 109 | 110 | // Inited : 是否已经初始化 111 | func (accountObj *Account) Inited() bool { 112 | return accountObj.inited 113 | } 114 | 115 | // GetRole : 获取账号当前角色 116 | func (accountObj *Account) GetRole() *Role { 117 | return accountObj.currentRole 118 | } 119 | -------------------------------------------------------------------------------- /services/lobby/account_mgr.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/fananchong/go-xserver/services/internal/protocol" 7 | "github.com/fananchong/go-xserver/services/internal/utility" 8 | ) 9 | 10 | // AccountMgr : 账号管理类 11 | type AccountMgr struct { 12 | accounts map[string]*Account 13 | mutex sync.RWMutex 14 | } 15 | 16 | // NewAccountMgr : 账号管理类构造函数 17 | func NewAccountMgr() *AccountMgr { 18 | accountMgr := &AccountMgr{ 19 | accounts: make(map[string]*Account), 20 | } 21 | return accountMgr 22 | } 23 | 24 | // AddAccount : 加入一个账号 25 | func (accountMgr *AccountMgr) AddAccount(account string) *Account { 26 | accountMgr.mutex.Lock() 27 | defer accountMgr.mutex.Unlock() 28 | if old, ok := accountMgr.accounts[account]; ok { 29 | return old 30 | } 31 | accountObj := NewAccount(account) 32 | accountMgr.accounts[account] = accountObj 33 | return accountObj 34 | } 35 | 36 | // GetAccount : 获取一个账号 37 | func (accountMgr *AccountMgr) GetAccount(account string) *Account { 38 | accountMgr.mutex.RLock() 39 | defer accountMgr.mutex.RUnlock() 40 | if accountObj, ok := accountMgr.accounts[account]; ok { 41 | if accountObj.Inited() { 42 | return accountObj 43 | } 44 | } 45 | return nil 46 | } 47 | 48 | // DelAccount : 删除一个账号 49 | func (accountMgr *AccountMgr) DelAccount(account string) { 50 | accountMgr.mutex.Lock() 51 | defer accountMgr.mutex.Unlock() 52 | if accountObj, ok := accountMgr.accounts[account]; ok { 53 | accountObj.Close() 54 | delete(accountMgr.accounts, account) 55 | } 56 | } 57 | 58 | // PostMsg : 推送消息 59 | func (accountMgr *AccountMgr) PostMsg(account string, cmd uint64, data []byte, flag uint8) { 60 | var accountObj *Account 61 | if protocol.CMD_LOBBY_ENUM(cmd) == protocol.CMD_LOBBY_LOGIN { 62 | accountObj = accountMgr.onLogin(account) 63 | } else { 64 | accountObj = accountMgr.GetAccount(account) 65 | } 66 | if accountObj == nil { 67 | Ctx.Errorln("Account object does not exist. account:", account, "cmd:", cmd) 68 | return 69 | } 70 | if accountObj.IsColse() { 71 | Ctx.Errorln("The account's work coroutine has been closed. account:", account, "cmd:", cmd) 72 | return 73 | } 74 | accountObj.PostMsg(cmd, data, flag) 75 | } 76 | 77 | func (accountMgr *AccountMgr) onLogin(account string) *Account { 78 | if accountObj := accountMgr.AddAccount(account); accountObj != nil { 79 | if !accountObj.Inited() { 80 | var err error 81 | if err = accountObj.Init(); err == nil { 82 | accountObj.Start() 83 | return accountObj 84 | } 85 | Ctx.Errorln("[LOGIN LOBBY]", err, "account:", account) 86 | msg := &protocol.MSG_LOBBY_LOGIN_RESULT{} 87 | msg.Err = protocol.ENUM_LOBBY_COMMON_ERROR_SYSTEM_ERROR 88 | utility.SendMsgToClient(Ctx, account, uint64(protocol.CMD_LOBBY_LOGIN), msg) 89 | } else { 90 | return accountObj 91 | } 92 | } 93 | Ctx.Errorln("[LOGIN LOBBY] New account fail, account:", account) 94 | return nil 95 | } 96 | 97 | func (accountMgr *AccountMgr) onLogout(account string) { 98 | 99 | // TODO: 账号登出处理,需要被触发 100 | 101 | accountMgr.DelAccount(account) 102 | } 103 | -------------------------------------------------------------------------------- /services/lobby/lobby.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/fananchong/go-xserver/common/config" 5 | "github.com/fananchong/go-xserver/common/context" 6 | "github.com/fananchong/go-xserver/services" 7 | ) 8 | 9 | // Lobby : 大厅服务器 10 | type Lobby struct { 11 | accountMgr *AccountMgr 12 | } 13 | 14 | // NewLobby : 构造函数 15 | func NewLobby() *Lobby { 16 | lobby := &Lobby{ 17 | accountMgr: NewAccountMgr(), 18 | } 19 | return lobby 20 | } 21 | 22 | // Start : 启动 23 | func (lobby *Lobby) Start() bool { 24 | Ctx.EnableMessageRelay(true) 25 | Ctx.RegisterFuncOnRelayMsg(lobby.onRelayMsg) 26 | Ctx.RegisterFuncOnLoseAccount(lobby.onLoseAccount) 27 | return true 28 | } 29 | 30 | // Close : 关闭 31 | func (lobby *Lobby) Close() { 32 | 33 | } 34 | 35 | func (lobby *Lobby) onRelayMsg(source config.NodeType, targetID context.NodeID, account string, cmd uint64, data []byte, flag uint8) { 36 | switch source { 37 | case config.Client: 38 | lobby.accountMgr.PostMsg(account, cmd, data, flag) 39 | case services.Match: 40 | lobby.onMatchMsg(targetID, cmd, data, flag) 41 | default: 42 | Ctx.Errorln("Unknown source, type:", source, "(", int(source), ")") 43 | } 44 | } 45 | 46 | func (lobby *Lobby) onLoseAccount(account string) { 47 | lobby.accountMgr.DelAccount(account) 48 | } 49 | -------------------------------------------------------------------------------- /services/lobby/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fananchong/go-xserver/common" 7 | "github.com/fananchong/go-xserver/common/config" 8 | "github.com/fananchong/go-xserver/services" 9 | ) 10 | 11 | // PluginObj : 代表一个插件对象 12 | var PluginObj common.IPlugin 13 | 14 | // PluginType : 插件类型 15 | var PluginType config.NodeType 16 | 17 | // Ctx : 应用程序上下文 18 | var Ctx *common.Context 19 | 20 | var lobby = NewLobby() 21 | 22 | func init() { 23 | fmt.Println("LOAD PLUGIN: LOBBY") 24 | PluginObj = &Plugin{} 25 | PluginType = services.Lobby 26 | } 27 | 28 | // Plugin : 插件类 29 | type Plugin struct { 30 | } 31 | 32 | // Start : 插件类实现启动 33 | func (plugin *Plugin) Start() bool { 34 | Ctx.Infoln("Plugin Start") 35 | return lobby.Start() 36 | } 37 | 38 | // Close : 插件类实现关闭 39 | func (plugin *Plugin) Close() { 40 | Ctx.Infoln("Plugin Close") 41 | lobby.Close() 42 | } 43 | 44 | // main : 作为插件包,该函数可以不存在。添加之,是避免 go-lint 烦人的错误提示 45 | func main() {} 46 | -------------------------------------------------------------------------------- /services/lobby/msg.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/fananchong/go-xserver/services/internal/protocol" 4 | 5 | // ChanMsg : 账号消息 6 | type ChanMsg struct { 7 | Cmd uint64 8 | Data []byte 9 | Flag uint8 10 | } 11 | 12 | // PostMsg : 推送消息 13 | func (accountObj *Account) PostMsg(cmd uint64, data []byte, flag uint8) { 14 | accountObj.chanMsg <- ChanMsg{cmd, data, flag} 15 | } 16 | 17 | // ProcessMsg : 处理消息 18 | func (accountObj *Account) processMsg(cmd uint64, data []byte, flag uint8) { 19 | switch protocol.CMD_LOBBY_ENUM(cmd) { 20 | case protocol.CMD_LOBBY_LOGIN: 21 | accountObj.onLogin(data, flag) 22 | case protocol.CMD_LOBBY_CREATE_ROLE: 23 | accountObj.onCreateRole(data, flag) 24 | case protocol.CMD_LOBBY_ENTER_GAME: 25 | accountObj.onEnterGame(data, flag) 26 | default: 27 | // 上面 3 个协议,属登录 Lobby 相关流程处理。单独拎出来 28 | // 其余协议就在下面处理 29 | if accountObj.GetRole() == nil { 30 | Ctx.Errorln("[LOBBY] Login not completed. account", accountObj.account, ",cmd:", cmd) 31 | return 32 | } 33 | switch protocol.CMD_LOBBY_ENUM(cmd) { 34 | case protocol.CMD_LOBBY_CHAT: 35 | accountObj.onChat(data, flag) 36 | case protocol.CMD_LOBBY_MATCH: 37 | accountObj.onMatch(data, flag) 38 | case protocol.CMD_LOBBY_MATCH_RESULT: 39 | accountObj.onMatchResult(data, flag) 40 | default: 41 | Ctx.Errorln("[LOBBY] Unknown cmd, cmd:", cmd) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /services/lobby/msg_chat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/fananchong/go-xserver/services/internal/protocol" 5 | "github.com/fananchong/go-xserver/services/internal/utility" 6 | "github.com/fananchong/gotcp" 7 | ) 8 | 9 | // onChat : 聊天 10 | func (accountObj *Account) onChat(data []byte, flag uint8) { 11 | Ctx.Infoln("Chat, account:", accountObj.account, "roleid:", accountObj.currentRole.Key) 12 | msg := &protocol.MSG_LOBBY_CHAT{} 13 | if gotcp.Decode(data, flag, msg) == nil { 14 | Ctx.Errorln("Message parsing failed, message number is`protocol.CMD_LOBBY_CHAT`(", int(protocol.CMD_LOBBY_CHAT), "). account", accountObj.account, "roleid:", accountObj.currentRole.Key) 15 | return 16 | } 17 | msg.From = accountObj.GetRole().GetName() 18 | if msg.GetTo() == "" { 19 | // 全服广播 20 | utility.BroadcastMsgToClient(Ctx, uint64(protocol.CMD_LOBBY_CHAT), msg) 21 | } else { 22 | // 私聊 23 | utility.SendMsgToClientByRoleName(Ctx, msg.GetTo(), uint64(protocol.CMD_LOBBY_CHAT), msg) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /services/lobby/msg_login.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/fananchong/go-xserver/services/internal/protocol" 5 | "github.com/fananchong/go-xserver/services/internal/utility" 6 | "github.com/fananchong/gotcp" 7 | ) 8 | 9 | // onLogin : 获取角色列表(登录大厅服务) 10 | func (accountObj *Account) onLogin(data []byte, flag uint8) { 11 | Ctx.Infoln("Login, account:", accountObj.account) 12 | msg := &protocol.MSG_LOBBY_LOGIN_RESULT{} 13 | for i, role := range accountObj.GetRoles() { 14 | info := &protocol.ROLE_BASE_INFO{} 15 | if role != nil { 16 | info.RoleID = role.Key 17 | info.RoleName = role.GetName() 18 | Ctx.Infof("\t[role%d] roleid:%d, rolename:%s\n", i, info.RoleID, info.RoleName) 19 | } else { 20 | Ctx.Infof("\t[role%d] roleid:%d, rolename:%s\n", i, 0, "''") 21 | } 22 | msg.Roles = append(msg.Roles, info) 23 | } 24 | utility.SendMsgToClient(Ctx, accountObj.account, uint64(protocol.CMD_LOBBY_LOGIN), msg) 25 | } 26 | 27 | // onCreateRole : 创建角色 28 | func (accountObj *Account) onCreateRole(data []byte, flag uint8) { 29 | Ctx.Infoln("Create role, account:", accountObj.account) 30 | req := &protocol.MSG_LOBBY_CREATE_ROLE{} 31 | if gotcp.Decode(data, flag, req) == nil { 32 | Ctx.Errorln("Message parsing failed, message number is`protocol.CMD_LOBBY_CREATE_ROLE`(", int(protocol.CMD_LOBBY_CREATE_ROLE), "). account", accountObj.account) 33 | return 34 | } 35 | 36 | msg := &protocol.MSG_LOBBY_CREATE_ROLE_RESULT{} 37 | if req.Slot >= LimitRoleNum { 38 | Ctx.Errorln("Message field error, Slot is ", req.Slot, ", but expect less than", LimitRoleNum, ". account:", accountObj.account) 39 | msg.Err = protocol.ENUM_LOBBY_COMMON_ERROR_SYSTEM_ERROR 40 | utility.SendMsgToClient(Ctx, accountObj.account, uint64(protocol.CMD_LOBBY_CREATE_ROLE), msg) 41 | return 42 | } 43 | 44 | if req.GetInfo() == nil { 45 | Ctx.Errorln("Message field error, Info is nil. account:", accountObj.account) 46 | msg.Err = protocol.ENUM_LOBBY_COMMON_ERROR_SYSTEM_ERROR 47 | utility.SendMsgToClient(Ctx, accountObj.account, uint64(protocol.CMD_LOBBY_CREATE_ROLE), msg) 48 | return 49 | } 50 | 51 | // 名字不能为空 52 | if req.GetInfo().GetRoleName() == "" { 53 | Ctx.Errorln("Role name is ''. account:", accountObj.account) 54 | msg.Err = protocol.ENUM_LOBBY_COMMON_ERROR_DUPLICATION_ROLE_NAME 55 | utility.SendMsgToClient(Ctx, accountObj.account, uint64(protocol.CMD_LOBBY_CREATE_ROLE), msg) 56 | return 57 | } 58 | 59 | // 重名检查 60 | if Ctx.IRole2Account.GetAndActive(req.GetInfo().GetRoleName()) != "" { 61 | Ctx.Errorln("Duplication of role names. account:", accountObj.account) 62 | msg.Err = protocol.ENUM_LOBBY_COMMON_ERROR_DUPLICATION_ROLE_NAME 63 | utility.SendMsgToClient(Ctx, accountObj.account, uint64(protocol.CMD_LOBBY_CREATE_ROLE), msg) 64 | return 65 | } 66 | 67 | role := accountObj.roles[req.Slot] 68 | if role == nil { // 创建角色 69 | // 保存角色名到角色名数据库 70 | if Ctx.IRole2Account.AddAndInsertDB(req.GetInfo().GetRoleName(), accountObj.account) == false { 71 | Ctx.Errorln("Save role name to db fail. account:", accountObj.account) 72 | msg.Err = protocol.ENUM_LOBBY_COMMON_ERROR_SYSTEM_ERROR 73 | utility.SendMsgToClient(Ctx, accountObj.account, uint64(protocol.CMD_LOBBY_CREATE_ROLE), msg) 74 | return 75 | } 76 | // 生成角色ID 77 | roleID, err := Ctx.GetUID("ROLEID") 78 | if err != nil { 79 | Ctx.Errorln(err, "account:", accountObj.account) 80 | msg.Err = protocol.ENUM_LOBBY_COMMON_ERROR_SYSTEM_ERROR 81 | utility.SendMsgToClient(Ctx, accountObj.account, uint64(protocol.CMD_LOBBY_CREATE_ROLE), msg) 82 | return 83 | } 84 | // 生成角色 85 | role, err = NewRole(roleID, accountObj.account) 86 | if err != nil { 87 | Ctx.Errorln(err, "account:", accountObj.account) 88 | msg.Err = protocol.ENUM_LOBBY_COMMON_ERROR_SYSTEM_ERROR 89 | utility.SendMsgToClient(Ctx, accountObj.account, uint64(protocol.CMD_LOBBY_CREATE_ROLE), msg) 90 | return 91 | } 92 | role.SetName(req.GetInfo().GetRoleName()) 93 | if err = role.Save(); err != nil { 94 | Ctx.Errorln(err, "account:", accountObj.account) 95 | msg.Err = protocol.ENUM_LOBBY_COMMON_ERROR_SYSTEM_ERROR 96 | utility.SendMsgToClient(Ctx, accountObj.account, uint64(protocol.CMD_LOBBY_CREATE_ROLE), msg) 97 | return 98 | } 99 | // 关联账号 100 | accountObj.roles[req.Slot] = role 101 | dbIDs := accountObj.RoleList.GetRoles(true) 102 | if dbIDs.GetRoleIDs() == nil { 103 | dbIDs.RoleIDs = make(map[uint32]uint64) 104 | } 105 | dbIDs.RoleIDs[req.Slot] = roleID 106 | if err := accountObj.Save(); err != nil { 107 | Ctx.Errorln(err, "account:", accountObj.account) 108 | msg.Err = protocol.ENUM_LOBBY_COMMON_ERROR_SYSTEM_ERROR 109 | utility.SendMsgToClient(Ctx, accountObj.account, uint64(protocol.CMD_LOBBY_CREATE_ROLE), msg) 110 | return 111 | } 112 | } 113 | utility.SendMsgToClient(Ctx, accountObj.account, uint64(protocol.CMD_LOBBY_CREATE_ROLE), msg) 114 | } 115 | 116 | // onEnterGame : 获取角色详细信息(进入游戏) 117 | func (accountObj *Account) onEnterGame(data []byte, flag uint8) { 118 | Ctx.Infoln("Enter Game, account:", accountObj.account) 119 | req := &protocol.MSG_LOBBY_ENTER_GAME{} 120 | if gotcp.Decode(data, flag, req) == nil { 121 | Ctx.Errorln("Message parsing failed, message number is`protocol.CMD_LOBBY_ENTER_GAME`(", int(protocol.CMD_LOBBY_ENTER_GAME), "). account", accountObj.account) 122 | return 123 | } 124 | 125 | msg := &protocol.MSG_LOBBY_ENTER_GAME_RESULT{} 126 | if req.Slot >= LimitRoleNum { 127 | Ctx.Errorln("Message field error, Slot is ", req.Slot, ", but expect less than", LimitRoleNum, ". account:", accountObj.account) 128 | msg.Err = protocol.ENUM_LOBBY_COMMON_ERROR_SYSTEM_ERROR 129 | utility.SendMsgToClient(Ctx, accountObj.account, uint64(protocol.CMD_LOBBY_ENTER_GAME), msg) 130 | return 131 | } 132 | 133 | role := accountObj.roles[req.Slot] 134 | if role == nil { 135 | // 没有角色 136 | Ctx.Errorln("No role found, Slot is ", req.Slot, ", account:", accountObj.account) 137 | msg.Err = protocol.ENUM_LOBBY_COMMON_ERROR_SYSTEM_ERROR 138 | utility.SendMsgToClient(Ctx, accountObj.account, uint64(protocol.CMD_LOBBY_ENTER_GAME), msg) 139 | return 140 | } 141 | Ctx.IRole2Account.Add(role.GetName(), accountObj.account) 142 | accountObj.currentRole = role 143 | msg.DetailInfo = &protocol.ROLE_DETAIL_INFO{} 144 | msg.DetailInfo.BaseInfo = &protocol.ROLE_BASE_INFO{} 145 | msg.DetailInfo.BaseInfo.RoleID = role.Key 146 | msg.DetailInfo.BaseInfo.RoleName = role.GetName() 147 | 148 | // TODO: 未加载角色各细节数据,则加载之 149 | 150 | utility.SendMsgToClient(Ctx, accountObj.account, uint64(protocol.CMD_LOBBY_ENTER_GAME), msg) 151 | 152 | Ctx.Infoln("Enter Game Success, account:", accountObj.account, "roleid:", role.Key) 153 | } 154 | -------------------------------------------------------------------------------- /services/lobby/msg_match.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/fananchong/go-xserver/common/context" 5 | "github.com/fananchong/go-xserver/services" 6 | "github.com/fananchong/go-xserver/services/internal/protocol" 7 | "github.com/fananchong/go-xserver/services/internal/utility" 8 | "github.com/fananchong/gotcp" 9 | ) 10 | 11 | // onMatch : 请求匹配 12 | func (accountObj *Account) onMatch(data []byte, flag uint8) { 13 | Ctx.Infoln("Match, account:", accountObj.account, "roleid:", accountObj.currentRole.Key) 14 | msg := &protocol.MSG_LOBBY_MATCH{} 15 | if gotcp.Decode(data, flag, msg) == nil { 16 | Ctx.Errorln("Message parsing failed, message number is`protocol.CMD_LOBBY_MATCH`(", int(protocol.CMD_LOBBY_MATCH), "). account", accountObj.account, "roleid:", accountObj.currentRole.Key) 17 | return 18 | } 19 | 20 | // 匹配请求可重入, Match 服加判断忽略 21 | 22 | // 请求 Match 服 23 | matchMsg := &protocol.MSG_MATCH_MATCH{} 24 | matchMsg.Account = accountObj.account 25 | matchMsg.RoleID = accountObj.currentRole.Key 26 | if _, err := utility.SendMsgToServer(Ctx, services.Match, uint64(protocol.CMD_MATCH_MATCH), matchMsg); err != nil { 27 | Ctx.Errorln("error:", err.Error(), "account:", accountObj.account, "roleid:", accountObj.currentRole.Key) 28 | } 29 | } 30 | 31 | func (accountObj *Account) onMatchResult(data []byte, flag uint8) { 32 | Ctx.Infoln("MatchResult, account:", accountObj.account, "roleid:", accountObj.currentRole.Key) 33 | msg := &protocol.MSG_LOBBY_MATCH_RESULT{} 34 | if gotcp.Decode(data, flag, msg) == nil { 35 | Ctx.Errorln("Message parsing failed, message number is`protocol.CMD_LOBBY_MATCH_RESULT`(", int(protocol.CMD_LOBBY_MATCH_RESULT), ")") 36 | return 37 | } 38 | utility.SendMsgToClient(Ctx, accountObj.account, uint64(protocol.CMD_LOBBY_MATCH_RESULT), msg) 39 | } 40 | 41 | func (lobby *Lobby) onMatchMsg(targetID context.NodeID, cmd uint64, data []byte, flag uint8) { 42 | switch protocol.CMD_MATCH_ENUM(cmd) { 43 | case protocol.CMD_MATCH_MATCH: 44 | msg := &protocol.MSG_MATCH_MATCH_RESULT{} 45 | if gotcp.Decode(data, flag, msg) == nil { 46 | Ctx.Errorln("Message parsing failed, message number is`protocol.CMD_MATCH_MATCH`(", int(protocol.CMD_MATCH_MATCH), ")") 47 | return 48 | } 49 | tmpMsg := &protocol.MSG_LOBBY_MATCH_RESULT{} 50 | tmpMsg.Err = protocol.ENUM_LOBBY_COMMON_ERROR_ENUM(msg.GetErr()) 51 | tmpMsg.Roles = append(tmpMsg.Roles, msg.GetRoles()...) 52 | // 暂时序列化下,更好的,应该有传递 msg 的接口 53 | data, flag, err := gotcp.Encode(uint64(protocol.CMD_LOBBY_MATCH_RESULT), tmpMsg) 54 | if err != nil { 55 | Ctx.Errorf("error:%s, account:%s, roleid:%d", err.Error(), msg.GetAccount(), msg.GetRoleID()) 56 | return 57 | } 58 | lobby.accountMgr.PostMsg(msg.GetAccount(), uint64(protocol.CMD_LOBBY_MATCH_RESULT), data, flag) 59 | default: 60 | Ctx.Errorln("Unknown cmd:", cmd) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /services/lobby/role.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | go_redis_orm "github.com/fananchong/go-redis-orm.v2" 5 | "github.com/fananchong/go-xserver/services/internal/db" 6 | ) 7 | 8 | // Role : 角色类 9 | type Role struct { 10 | *db.RoleBase 11 | account string 12 | inited bool 13 | } 14 | 15 | // NewRole : 角色类构造函数 16 | func NewRole(roleID uint64, account string) (*Role, error) { 17 | role := &Role{} 18 | role.RoleBase = db.NewRoleBase(Ctx.Config().DbAccount.Name, roleID) 19 | role.account = account 20 | if err := role.RoleBase.Load(); err != nil { 21 | if err != go_redis_orm.ERR_ISNOT_EXIST_KEY { 22 | return nil, err 23 | } 24 | } 25 | return role, nil 26 | } 27 | 28 | // FirstInitialization : 角色首次创建初始化 29 | func (role *Role) FirstInitialization() bool { 30 | return true 31 | } 32 | 33 | // InitFromDB : 从数据库加载数据 34 | func (role *Role) InitFromDB() bool { 35 | role.inited = true 36 | return true 37 | } 38 | -------------------------------------------------------------------------------- /services/match/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fananchong/go-xserver/common" 7 | "github.com/fananchong/go-xserver/common/config" 8 | "github.com/fananchong/go-xserver/services" 9 | ) 10 | 11 | // PluginObj : 代表一个插件对象 12 | var PluginObj common.IPlugin 13 | 14 | // PluginType : 插件类型 15 | var PluginType config.NodeType 16 | 17 | // Ctx : 应用程序上下文 18 | var Ctx *common.Context 19 | 20 | var match = NewMatch() 21 | 22 | func init() { 23 | fmt.Println("LOAD PLUGIN: MATCH") 24 | PluginObj = &Plugin{} 25 | PluginType = services.Match 26 | } 27 | 28 | // Plugin : 插件类 29 | type Plugin struct { 30 | } 31 | 32 | // Start : 插件类实现启动 33 | func (plugin *Plugin) Start() bool { 34 | Ctx.Infoln("Plugin Start") 35 | return match.Start() 36 | } 37 | 38 | // Close : 插件类实现关闭 39 | func (plugin *Plugin) Close() { 40 | Ctx.Infoln("Plugin Close") 41 | match.Close() 42 | } 43 | 44 | // main : 作为插件包,该函数可以不存在。添加之,是避免 go-lint 烦人的错误提示 45 | func main() {} 46 | -------------------------------------------------------------------------------- /services/match/match.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/fananchong/go-xserver/common/config" 5 | "github.com/fananchong/go-xserver/common/context" 6 | "github.com/fananchong/go-xserver/services" 7 | ) 8 | 9 | // Match : 匹配服务器 10 | type Match struct { 11 | } 12 | 13 | // NewMatch : 构造函数 14 | func NewMatch() *Match { 15 | match := &Match{} 16 | return match 17 | } 18 | 19 | // Start : 启动 20 | func (match *Match) Start() bool { 21 | Ctx.EnableMessageRelay(true) 22 | Ctx.RegisterFuncOnRelayMsg(match.onRelayMsg) 23 | return true 24 | } 25 | 26 | // Close : 关闭 27 | func (match *Match) Close() { 28 | } 29 | 30 | func (match *Match) onRelayMsg(source config.NodeType, targetID context.NodeID, _ string, cmd uint64, data []byte, flag uint8) { 31 | switch source { 32 | case services.Lobby: 33 | match.onLobbyMsg(targetID, cmd, data, flag) 34 | default: 35 | Ctx.Errorln("Unknown source, type:", source, "(", int(source), ")") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /services/match/msg_lobby.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/fananchong/go-xserver/common/context" 5 | "github.com/fananchong/go-xserver/services/internal/protocol" 6 | "github.com/fananchong/go-xserver/services/internal/utility" 7 | "github.com/fananchong/gotcp" 8 | ) 9 | 10 | func (match *Match) onLobbyMsg(targetID context.NodeID, cmd uint64, data []byte, flag uint8) { 11 | switch protocol.CMD_MATCH_ENUM(cmd) { 12 | case protocol.CMD_MATCH_MATCH: 13 | req := &protocol.MSG_MATCH_MATCH{} 14 | if gotcp.Decode(data, flag, req) == nil { 15 | Ctx.Errorln("Message parsing failed, message number is`protocol.CMD_MATCH_MATCH`(", int(protocol.CMD_MATCH_MATCH), ")") 16 | return 17 | } 18 | Ctx.Infoln("Request match, roleid:", req.GetRoleID()) 19 | 20 | // TODO: 暂时,直接返回匹配结果,测试下服务器组内消息中继 21 | 22 | rep := &protocol.MSG_MATCH_MATCH_RESULT{} 23 | rep.Account = req.GetAccount() 24 | rep.RoleID = req.GetRoleID() 25 | utility.ReplyMsgToServer(Ctx, targetID, uint64(protocol.CMD_MATCH_MATCH), rep) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /services/server_type_custom.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import "github.com/fananchong/go-xserver/common/config" 4 | 5 | const ( 6 | 7 | // Lobby : 类型 4 ,大厅服 8 | Lobby config.NodeType = 4 9 | 10 | // Match : 类型 5 ,匹配服 11 | Match config.NodeType = 5 12 | 13 | // Room : 类型 6 ,房间服 14 | Room config.NodeType = 6 15 | ) 16 | --------------------------------------------------------------------------------