├── .env.dev ├── .gitee └── ISSUE_TEMPLATE.zh-CN.md ├── .gitignore ├── LICENSE ├── README.cn.md ├── README.en.md ├── README.md ├── component ├── game_config.go ├── game_config_test.go └── init.go ├── config.go ├── db ├── .env.dev ├── cache.go ├── cache_test.go ├── init.go ├── manager.go ├── manager_test.go ├── mysql.go └── redis.go ├── define.go ├── doc ├── framework.excalidraw └── framework.png ├── error.go ├── exp ├── admin │ ├── consul.go │ ├── monitor.go │ └── monitor_test.go └── ast.go ├── go.mod ├── go.sum ├── init.go ├── log ├── init.go ├── logger.go ├── logger_test.go └── trace.go ├── robot ├── client.go ├── tcp_client.go └── tcp_client_test.go ├── rpc ├── .env.dev ├── admin.go ├── admin_test.go ├── gate.go ├── init.go ├── internal │ ├── consul.go │ ├── discovery.go │ └── jwt.go ├── monitor.go ├── plugins.go ├── plugins_test.go ├── rpcserver.go ├── rpcserver_example_test.go ├── rpcserver_test.go ├── service.go ├── service_test.go ├── tcp.go ├── tcp_test.go ├── ws.pb.go └── ws.proto └── util ├── aes.go ├── aes_test.go ├── code.go ├── code_test.go ├── common.go ├── config.go ├── config_test.go ├── example_test.go ├── file.go ├── init.go ├── ip.go ├── ip_test.go ├── math.go ├── pb.go ├── pool.go ├── pool_test.go ├── random.go ├── random_test.go ├── reflect.go ├── slice.go ├── string.go ├── string_test.go ├── timer.go ├── uuid.go ├── uuid_test.go ├── weight.go └── weight_test.go /.env.dev: -------------------------------------------------------------------------------- 1 | #全局配置环境读取,取TGFMODULE常量,如果环境变量中不存在,默认会取dev的配置 2 | #可以在启动的时候传入该常量,动态运行不同环境的配置文件 3 | #示例:go run main.go --env TGFMODULE=dev 4 | 5 | #日志输出路径 6 | LogPath=./log/tgf.log 7 | #日志最低输出级别 8 | LogLevel=info 9 | #日志忽略标签 10 | LogIgnoredTags=trace,login 11 | 12 | #运行环境,有以下可选运行环境 dev test release 13 | RuntimeModule=dev 14 | 15 | #consul地址 16 | ConsulAddress=127.0.0.1:8500 17 | #consul路径,默认使用/tgf,如需区分不同环境可以使用自定义的不同的路径 例如 /test 或者 /dev /tim 18 | ConsulPath=/tgf 19 | 20 | #redis地址 127.0.0.1:6379 21 | RedisAddr=127.0.0.1:6379 22 | #redis密码 23 | RedisPassword=123456 24 | #redis的db 25 | RedisDB=1 26 | 27 | #mysql用户名 28 | MySqlUser=root 29 | #mysql密码 30 | MySqlPwd=123456 31 | #mysql地址 32 | MySqlAddr=127.0.0.1 33 | #mysql端口 34 | MySqlPort=3306 35 | #mysql库名 36 | MySqlDB=tgf_game 37 | 38 | #当前服务提供的服务端口 39 | ServicePort=8021 40 | #绑定自定义服务地址 41 | ServiceAddress=0.0.0.0 42 | #是否推送用户节点信息到网关,0关闭 43 | GatePush=1 -------------------------------------------------------------------------------- /.gitee/ISSUE_TEMPLATE.zh-CN.md: -------------------------------------------------------------------------------- 1 | ### 该问题是怎么引起的? 2 | 3 | 4 | 5 | ### 重现步骤 6 | 7 | 8 | 9 | ### 报错信息 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | #*.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | *.log 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | /.idea/ 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.cn.md: -------------------------------------------------------------------------------- 1 | # 关于 tgf 2 | tgf框架是使用golang开发的一套游戏分布式框架. 3 | 4 | 属于开箱即用的项目框架,目前适用于中小型团队,独立开发者,快速开发使用. 5 | 6 | 框架提供了一整套开发工具,并且定义了模块开发规范. 7 | 8 | 开发者只需要关注业务逻辑即可,无需关心用户并发和节点状态等复杂情况. 9 | 10 | 11 | [项目地址](https://github.com/thkhxm/tgf) 12 | [项目文档](https://thkhxm.github.io/tgf_writerside/starter-topic.html) 13 | 14 | 15 | ## 交流群 16 | QQ群:7400585 17 | 18 | ## 技术选型 19 | Golang开发版本: 1.21.1 20 | 21 | | 技术 | 说明 | 仓库地址 | 22 | | ---------- | -------------- | ---------------------------------------- | 23 | | rpcx | 底层rpc的实现 | https://github.com/smallnest/rpcx | 24 | | redis | 提供数据缓存 | https://redis.io/ | 25 | | hashmap | 线程安全的集合 | https://github.com/cornelk/hashmap | 26 | | ants | 高性能go协程池 | https://github.com/panjf2000/ants | 27 | | redislock | 分布式redis锁 | https://github.com/bsm/redislock | 28 | | snowflake | 雪花算法 | https://github.com/bwmarrin/snowflake | 29 | | doublejump | 一致性hash | https://github.com/edwingeng/doublejump | 30 | | godotenv | 环境变量工具 | https://github.com/joho/godotenv | 31 | | zap | 日志框架 | https://go.uber.org/zap | 32 | | lumberjack | 日志切割工具 | https://gopkg.in/natefinch/lumberjack.v2 | 33 | | excelize | Excel工具 | https://github.com/qax-os/excelize | 34 | | sonic | json高性能工具 | https://github.com/bytedance/sonic/ | 35 | 36 | 37 | ## 基础架构图 38 | 39 | ![image-20230228031100624](http://oss.yamigame.net/picgo/image-20230228031100624.png) 40 | 41 | ## 规划 42 | 项目后续会更新系列教程文章和视频教程,并且开源项目案例.也会不断的更新和优化项目框架. 43 | 欢迎大家加入qq群一起交流和探讨. -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | # TFramework 2 | 3 | #### Description 4 | 使用golang,搭建的分布式游戏框架。 5 | 6 | #### Software Architecture 7 | Software architecture description 8 | 9 | #### Installation 10 | 11 | 1. xxxx 12 | 2. xxxx 13 | 3. xxxx 14 | 15 | #### Instructions 16 | 17 | 1. xxxx 18 | 2. xxxx 19 | 3. xxxx 20 | 21 | #### Contribution 22 | 23 | 1. Fork the repository 24 | 2. Create Feat_xxx branch 25 | 3. Commit your code 26 | 4. Create Pull Request 27 | 28 | 29 | #### Gitee Feature 30 | 31 | 1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md 32 | 2. Gitee blog [blog.gitee.com](https://blog.gitee.com) 33 | 3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore) 34 | 4. The most valuable open source project [GVP](https://gitee.com/gvp) 35 | 5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help) 36 | 6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Report Card](https://goreportcard.com/badge/github.com/thkhxm/tgf)](https://goreportcard.com/report/github.com/thkhxm/tgf) 2 | 3 | # 关于 tgf 4 | tgf框架是使用golang开发的一套游戏分布式框架. 5 | 6 | 属于开箱即用的项目框架,目前适用于中小型团队,独立开发者,快速开发使用. 7 | 8 | 框架提供了一整套开发工具,并且定义了模块开发规范. 9 | 10 | 开发者只需要关注业务逻辑即可,无需关心用户并发和节点状态等复杂情况. 11 | 12 | [API](https://pkg.go.dev/github.com/thkhxm/tgf) 13 | [项目地址](https://github.com/thkhxm/tgf) 14 | [项目文档](https://thkhxm.github.io/tgf_writerside/starter-topic.html) 15 | [国内项目文档](http://tgf.yamigame.net:8080/) 16 | [项目案例地址](https://github.com/thkhxm/tgf-tutorial) 17 | [知乎博客](https://www.zhihu.com/people/tim-30-83/posts) 18 | [CSDN专栏](https://blog.csdn.net/thkhxm/category_12520142.html) 19 | [B站教程](https://space.bilibili.com/64497732/channel/seriesdetail?sid=3815364) 20 | 21 | 22 | ## 交流群 23 | QQ群:7400585 24 | 25 | ## 技术选型 26 | Golang开发版本: 1.22 27 | 28 | | 技术 | 说明 | 仓库地址 | 29 | | ---------- | -------------- | ---------------------------------------- | 30 | | rpcx | 底层rpc的实现 | https://github.com/smallnest/rpcx | 31 | | redis | 提供数据缓存 | https://redis.io/ | 32 | | hashmap | 线程安全的集合 | https://github.com/cornelk/hashmap | 33 | | ants | 高性能go协程池 | https://github.com/panjf2000/ants | 34 | | redislock | 分布式redis锁 | https://github.com/bsm/redislock | 35 | | snowflake | 雪花算法 | https://github.com/bwmarrin/snowflake | 36 | | doublejump | 一致性hash | https://github.com/edwingeng/doublejump | 37 | | godotenv | 环境变量工具 | https://github.com/joho/godotenv | 38 | | zap | 日志框架 | https://go.uber.org/zap | 39 | | lumberjack | 日志切割工具 | https://gopkg.in/natefinch/lumberjack.v2 | 40 | | excelize | Excel工具 | https://github.com/qax-os/excelize | 41 | | sonic | json高性能工具 | https://github.com/bytedance/sonic/ | 42 | 43 | ## 规划 44 | 项目后续会更新系列教程文章和视频教程,并且开源项目案例.也会不断的更新和优化项目框架. 45 | 欢迎大家加入qq群一起交流和探讨. -------------------------------------------------------------------------------- /component/game_config.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/cornelk/hashmap" 5 | "github.com/thkhxm/tgf/db" 6 | "github.com/thkhxm/tgf/log" 7 | "github.com/thkhxm/tgf/util" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | "reflect" 12 | "strings" 13 | "sync" 14 | ) 15 | 16 | //*************************************************** 17 | //@Link https://github.com/thkhxm/tgf 18 | //@Link https://gitee.com/timgame/tgf 19 | //@QQ群 7400585 20 | //author tim.huang 21 | //@Description 22 | //2023/4/10 23 | //*************************************************** 24 | 25 | var confPath = "./conf/json" 26 | 27 | var contextDataManager db.IAutoCacheService[string, []byte] 28 | var cacheDataManager db.IAutoCacheService[string, *hashmap.Map[string, interface{}]] 29 | 30 | var newLock = &sync.Mutex{} 31 | 32 | func GetGameConf[Val any](id string) (res Val, h bool) { 33 | t := util.ReflectType[Val]() 34 | key := t.Name() 35 | data := getCacheGameConfData[Val](key) 36 | if tmp, x := data.Get(id); x { 37 | res = tmp.([]Val)[0] 38 | h = true 39 | } 40 | return 41 | } 42 | 43 | func GetGameConfBySlice[Val any](id string) (res []Val, h bool) { 44 | t := util.ReflectType[Val]() 45 | key := t.Name() 46 | data := getCacheGameConfData[Val](key) 47 | if tmp, x := data.Get(id); x { 48 | res = tmp.([]Val) 49 | h = true 50 | } 51 | return 52 | } 53 | 54 | // GetAllGameConf [Val any] 55 | // @Description: 不建议使用这个函数除非特殊需求,建议使用RangeGameConf 56 | func GetAllGameConf[Val any]() (res []Val) { 57 | t := util.ReflectType[Val]() 58 | key := t.Name() 59 | data := getCacheGameConfData[Val](key) 60 | tmp := make([]Val, 0, data.Len()) 61 | data.Range(func(s string, i interface{}) bool { 62 | for _, v := range i.([]Val) { 63 | tmp = append(tmp, v) 64 | } 65 | return true 66 | }) 67 | res = tmp 68 | return 69 | } 70 | 71 | // RangeGameConf [Val any] 72 | // @Description: 73 | // @param f 74 | func RangeGameConf[Val any](f func(s string, i Val) bool) { 75 | t := util.ReflectType[Val]() 76 | key := t.Name() 77 | data := getCacheGameConfData[Val](key) 78 | ff := func(a string, b interface{}) bool { 79 | for _, i := range b.([]Val) { 80 | if !f(a, i) { 81 | return false 82 | } 83 | } 84 | return true 85 | } 86 | data.Range(ff) 87 | } 88 | 89 | func getCacheGameConfData[Val any](key string) *hashmap.Map[string, interface{}] { 90 | data, _ := cacheDataManager.Get(key) 91 | if data == nil { 92 | newLock.Lock() 93 | defer newLock.Unlock() 94 | data = LoadGameConf[Val]() 95 | } 96 | return data 97 | } 98 | 99 | // LoadGameConf [Val any] 100 | // 101 | // 泛型传入自动生成的配置即可 102 | // @Description: 预加载 103 | func LoadGameConf[Val any]() *hashmap.Map[string, interface{}] { 104 | t := util.ReflectType[Val]() 105 | key := t.Name() 106 | context, _ := contextDataManager.Get(key) 107 | data, _ := util.StrToAny[[]Val](util.ConvertStringByByteSlice(context)) 108 | cc := hashmap.New[string, interface{}]() 109 | for _, d := range data { 110 | rd := reflect.ValueOf(d).Elem() 111 | id := rd.Field(0) 112 | uniqueId, _ := util.AnyToStr(id.Interface()) 113 | v, _ := cc.Get(uniqueId) 114 | if v == nil { 115 | v = make([]Val, 0) 116 | } 117 | v = append(v.([]Val), d) 118 | cc.Set(uniqueId, v) 119 | } 120 | cacheDataManager.Set(cc, key) 121 | log.DebugTag("GameConf", "load game conf , name=%v", t.Name()) 122 | return cc 123 | } 124 | 125 | func WithConfPath(path string) { 126 | confPath, _ = filepath.Abs(path) 127 | log.InfoTag("GameConf", "set game json file path=%v", confPath) 128 | } 129 | 130 | func InitGameConfToMem() { 131 | builder := db.NewAutoCacheBuilder[string, []byte]() 132 | builder.WithMemCache(0) 133 | contextDataManager = builder.New() 134 | // 135 | cacheBuilder := db.NewAutoCacheBuilder[string, *hashmap.Map[string, interface{}]]() 136 | cacheBuilder.WithMemCache(0) 137 | cacheDataManager = cacheBuilder.New() 138 | // 139 | files := util.GetFileList(confPath, ".json") 140 | for _, filePath := range files { 141 | file, err := os.Open(filePath) 142 | if err != nil { 143 | log.WarnTag("GameConf", "game json file [%v] open error %v", filePath, err) 144 | continue 145 | } 146 | context, err := io.ReadAll(file) 147 | if err != nil { 148 | log.WarnTag("GameConf", "game json file [%v] read error %v", filePath, err) 149 | continue 150 | } 151 | _, fileName := filepath.Split(filePath) 152 | contextDataManager.Set(context, strings.Split(fileName, `.`)[0]+"Conf") 153 | file.Close() 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /component/game_config_test.go: -------------------------------------------------------------------------------- 1 | package component_test 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | //*************************************************** 8 | //@Link https://github.com/thkhxm/tgf 9 | //@Link https://gitee.com/timgame/tgf 10 | //@QQ群 7400585 11 | //author tim.huang 12 | //@Description 13 | //2023/4/11 14 | //*************************************************** 15 | 16 | func TestInitGameConfig(t *testing.T) { 17 | //component.WithConfPath("./example/internal/cmd/json") 18 | //component.InitGameConfToMem() 19 | //heroConf := component.GetGameConf[*conf.HeroConf]("f_01") 20 | } 21 | -------------------------------------------------------------------------------- /component/init.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | //*************************************************** 4 | //@Link https://github.com/thkhxm/tgf 5 | //@Link https://gitee.com/timgame/tgf 6 | //@QQ群 7400585 7 | //author tim.huang 8 | //@Description 9 | //2023/4/10 10 | //*************************************************** 11 | 12 | func init() { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package tgf 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/joho/godotenv" 7 | "github.com/thkhxm/tgf/util" 8 | 9 | "os" 10 | "strings" 11 | ) 12 | 13 | //*************************************************** 14 | //@Link https://github.com/thkhxm/tgf 15 | //@Link https://gitee.com/timgame/tgf 16 | //@QQ群 7400585 17 | //author tim.huang 18 | //@Description 19 | //2023/2/22 20 | //*************************************************** 21 | 22 | type config struct { 23 | env Environment 24 | val string 25 | } 26 | 27 | var mapping = make(map[Environment]*config) 28 | 29 | func initMapping() { 30 | mapping[EnvironmentLoggerPath] = &config{env: EnvironmentLoggerPath, val: defaultLogPath} 31 | mapping[EnvironmentLoggerLevel] = &config{env: EnvironmentLoggerLevel, val: defaultLogLevel} 32 | mapping[EnvironmentLoggerIgnoredTags] = &config{env: EnvironmentLoggerIgnoredTags, val: defaultIgnoredTags} 33 | mapping[EnvironmentRuntimeModule] = &config{env: EnvironmentRuntimeModule, val: defaultRuntimeModule} 34 | mapping[EnvironmentConsulAddress] = &config{env: EnvironmentConsulAddress, val: defaultConsulAddress} 35 | mapping[EnvironmentConsulPath] = &config{env: EnvironmentConsulPath, val: defaultConsulPath} 36 | mapping[EnvironmentRedisAddr] = &config{env: EnvironmentRedisAddr, val: defaultRedisAddr} 37 | mapping[EnvironmentRedisPassword] = &config{env: EnvironmentRedisPassword, val: defaultRedisPassword} 38 | mapping[EnvironmentRedisDB] = &config{env: EnvironmentRedisDB, val: defaultRedisDB} 39 | mapping[EnvironmentRedisCluster] = &config{env: EnvironmentRedisCluster, val: defaultRedisCluster} 40 | mapping[EnvironmentServicePort] = &config{env: EnvironmentServicePort, val: defaultServicePort} 41 | mapping[EnvironmentServiceAddress] = &config{env: EnvironmentServiceAddress, val: defaultServiceAddress} 42 | 43 | mapping[EnvironmentMySqlUser] = &config{env: EnvironmentMySqlUser, val: defaultMySqlUser} 44 | mapping[EnvironmentMySqlPwd] = &config{env: EnvironmentMySqlPwd, val: defaultMySqlPwd} 45 | mapping[EnvironmentMySqlAddr] = &config{env: EnvironmentMySqlAddr, val: defaultMySqlAddr} 46 | mapping[EnvironmentMySqlPort] = &config{env: EnvironmentMySqlPort, val: defaultMySqlPort} 47 | mapping[EnvironmentMySqlDB] = &config{env: EnvironmentMySqlDB, val: defaultMySqlDB} 48 | mapping[EnvironmentGatePush] = &config{env: EnvironmentGatePush, val: defaultGatePush} 49 | 50 | //初始化配置数据 51 | for _, m := range mapping { 52 | m.initVal() 53 | fmt.Sprintf("env=%v val=%v", m.env, m.val) 54 | fmt.Println() 55 | } 56 | } 57 | 58 | // 配置默认值 59 | const ( 60 | defaultMySqlUser = "root" 61 | defaultMySqlPwd = "123456" 62 | defaultMySqlAddr = "127.0.0.1" 63 | defaultMySqlPort = "3306" 64 | defaultMySqlDB = "tgf" 65 | 66 | defaultLogPath = "./log/tgf.log" 67 | defaultLogLevel = "debug" 68 | defaultIgnoredTags = "" 69 | 70 | defaultRuntimeModule = "dev" 71 | 72 | defaultConsulAddress = "127.0.0.1:8500" 73 | defaultConsulPath = "/tgf" 74 | 75 | defaultServicePort = "8082" 76 | defaultServiceAddress = "127.0.0.1" 77 | 78 | defaultRedisAddr = "127.0.0.1:6379" 79 | defaultRedisPassword = "" 80 | defaultRedisDB = "1" 81 | defaultGatePush = "1" 82 | defaultRedisCluster = "0" 83 | ) 84 | 85 | func (c *config) initVal() *config { 86 | var ( 87 | res = os.Getenv(string(c.env)) 88 | ) 89 | if res != "" { 90 | fmt.Sprintf("[init] 配置 env=%v 从 %v 修改为 %v", c.env, c.val, res) 91 | fmt.Println() 92 | c.val = res 93 | } 94 | return c 95 | } 96 | 97 | // 配置缓存 98 | 99 | func GetStrConfig[T int | int32 | string | int64 | float32 | float64](env Environment) (res T) { 100 | res, _ = util.StrToAny[T](mapping[env].val) 101 | return 102 | } 103 | 104 | func GetStrListConfig(env Environment) (res []string) { 105 | res = make([]string, 0) 106 | for _, s := range strings.Split(mapping[env].val, ",") { 107 | res = append(res, s) 108 | } 109 | return 110 | } 111 | 112 | func InitConfig() { 113 | //初始化配置的环境变量 114 | env := os.Getenv("TGFMODULE") 115 | if env == "" { 116 | env = *flag.String("TGFMODULE", RuntimeModuleDev, "RuntimeModule") 117 | if env == "" { 118 | env = defaultRuntimeModule 119 | } 120 | } 121 | fmt.Printf("[tgf/init.go] 当前运行模式 [TGFMODULE] 为 %v", env) 122 | fmt.Println() 123 | fileName := ".env." + env 124 | // 加载环境配置文件 125 | err := godotenv.Load(fileName) 126 | if err != nil { 127 | fmt.Printf("[init] [tgf/init.go] 找不到指定的env文件 %v", fileName) 128 | fmt.Println() 129 | } 130 | initMapping() 131 | } 132 | -------------------------------------------------------------------------------- /db/.env.dev: -------------------------------------------------------------------------------- 1 | RedisAddr=192.168.1.82:6382 2 | RedisDB=6 3 | MySqlAddr=192.168.1.82 4 | LogIgnoredTags=cache,orm:trace,discovery -------------------------------------------------------------------------------- /db/cache.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/bsm/redislock" 7 | "github.com/bytedance/sonic" 8 | "github.com/redis/go-redis/v9" 9 | "github.com/thkhxm/tgf" 10 | "github.com/thkhxm/tgf/log" 11 | "github.com/thkhxm/tgf/util" 12 | "reflect" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | //*************************************************** 18 | //@Link https://github.com/thkhxm/tgf 19 | //@Link https://gitee.com/timgame/tgf 20 | //@QQ群 7400585 21 | //author tim.huang 22 | //@Description 23 | //2023/2/24 24 | //*************************************************** 25 | 26 | var cache iCacheService 27 | 28 | var cacheModule = tgf.CacheModuleRedis 29 | 30 | type iCacheService interface { 31 | Get(key string) (res string) 32 | Set(key string, val any, timeout time.Duration) 33 | GetMap(key string) map[string]string 34 | PutMap(key, filed, val string, timeout time.Duration) 35 | Del(key string) 36 | DelNow(key string) 37 | GetList(key string, start, end int64) (res []string, err error) 38 | SetList(key string, l []interface{}, timeout time.Duration) 39 | GetSet(key string) (res []string, err error) 40 | AddSetItem(key string, val interface{}, timeout time.Duration) 41 | AddListItem(key string, val string, timeout time.Duration) 42 | } 43 | 44 | type iRedisCacheService interface { 45 | TryLock(key string) (*redislock.Lock, error) 46 | TryUnLock(l *redislock.Lock, ctx context.Context) 47 | Incr(key string, timeout time.Duration) (res int64, err error) 48 | IncrBy(key string, val float64, timeout time.Duration) (res float64, err error) 49 | LLen(key string) (res int64, err error) 50 | } 51 | 52 | type IAutoCacheService[Key cacheKey, Val any] interface { 53 | Get(key ...Key) (val Val, err error) 54 | TryGet(key ...Key) (val Val, err error) 55 | Set(val Val, key ...Key) (success bool) 56 | Push(key ...Key) 57 | Remove(key ...Key) (success bool) 58 | Reset() IAutoCacheService[Key, Val] 59 | Range(f func(Key, Val) bool) 60 | } 61 | 62 | type IAutoCacheClearPlugin interface { 63 | PreClear(key string) 64 | PostClear(key string) 65 | } 66 | 67 | type IHashCacheService[Val any] interface { 68 | IAutoCacheService[string, Val] 69 | GetAll(key ...string) (val []Val, err error) 70 | } 71 | 72 | type IHashModel interface { 73 | //主键key 74 | HashCachePkKey(key ...string) string 75 | 76 | //单项Key,调用Get,Set等操作函数的时候,需要保证这里的值有跟主键一起传进来 77 | HashCacheFieldByVal() string 78 | HashCacheFieldByKeys(key ...string) string 79 | } 80 | 81 | // Get [Res any] 82 | // @Description: 通过二级缓存获取数据 83 | // @param key 84 | // @return res 85 | func Get[Res any](key string) (res Res, success bool) { 86 | if cache == nil { 87 | return 88 | } 89 | val := cache.Get(key) 90 | if val != "" { 91 | res, _ = util.StrToAny[Res](val) 92 | success = true 93 | } 94 | return 95 | } 96 | 97 | // FormatKey 98 | // @Description: format redis key,拼接key. 99 | // @example: FormatKey("user",1001) => user:1001 100 | // @param args 101 | // @return string 102 | func FormatKey(args ...string) string { 103 | if len(args) == 0 { 104 | return "" 105 | } 106 | return strings.Join(args, ":") 107 | } 108 | func Set(key string, val any, timeout time.Duration) { 109 | if cache == nil { 110 | return 111 | } 112 | switch val.(type) { 113 | case string: 114 | cache.Set(key, val, timeout) 115 | case interface{}: 116 | data, _ := sonic.Marshal(val) 117 | cache.Set(key, data, timeout) 118 | default: 119 | cache.Set(key, val, timeout) 120 | } 121 | } 122 | 123 | func GetMap[Key cacheKey, Val any](key string) (res map[Key]Val, success bool) { 124 | if cache == nil { 125 | return 126 | } 127 | data := cache.GetMap(key) 128 | if data != nil && len(data) > 0 { 129 | res = make(map[Key]Val, len(data)) 130 | for k, v := range data { 131 | kk, _ := util.StrToAny[Key](k) 132 | vv, _ := util.StrToAny[Val](v) 133 | res[kk] = vv 134 | } 135 | success = true 136 | } 137 | return 138 | } 139 | 140 | func PutMap[Key cacheKey, Val any](key string, field Key, val Val, timeout time.Duration) { 141 | if cache == nil { 142 | return 143 | } 144 | f, _ := util.AnyToStr(field) 145 | v, _ := util.AnyToStr(val) 146 | cache.PutMap(key, f, v, timeout) 147 | } 148 | 149 | func GetList[Res any](key string) []Res { 150 | if cache == nil { 151 | return nil 152 | } 153 | if res, err := cache.GetList(key, 0, -1); err == nil { 154 | data := make([]Res, len(res)) 155 | for i, r := range res { 156 | data[i], _ = util.StrToAny[Res](r) 157 | } 158 | return data 159 | } 160 | return nil 161 | } 162 | 163 | func GetListLimit[Res any](key string, start, end int64) []Res { 164 | if cache == nil { 165 | return nil 166 | } 167 | if res, err := cache.GetList(key, start, end); err == nil { 168 | data := make([]Res, len(res)) 169 | for i, r := range res { 170 | data[i], _ = util.StrToAny[Res](r) 171 | } 172 | return data 173 | } 174 | return nil 175 | } 176 | 177 | func AddListItem[Val any](key string, timeout time.Duration, val ...Val) (err error) { 178 | if cache == nil { 179 | return errors.New("cache is nil") 180 | } 181 | data := make([]interface{}, len(val)) 182 | for i, v := range val { 183 | a, e := util.AnyToStr(v) 184 | if e != nil { 185 | err = e 186 | return 187 | } 188 | data[i] = a 189 | } 190 | cache.SetList(key, data, timeout) 191 | return 192 | } 193 | 194 | func AddListItemL[Val any](key string, timeout time.Duration, val Val) (err error) { 195 | if cache == nil { 196 | return errors.New("cache is nil") 197 | } 198 | a, e := util.AnyToStr(val) 199 | if e != nil { 200 | err = e 201 | return 202 | } 203 | cache.AddListItem(key, a, timeout) 204 | return 205 | } 206 | 207 | func GetAllSet[Res any](key string) []Res { 208 | if cache == nil { 209 | return nil 210 | } 211 | if res, err := cache.GetSet(key); err == nil { 212 | data := make([]Res, len(res)) 213 | for i, r := range res { 214 | data[i], _ = util.StrToAny[Res](r) 215 | } 216 | return data 217 | } 218 | return nil 219 | } 220 | 221 | func AddSetItem[Val any](key string, timeout time.Duration, val Val) (err error) { 222 | if cache == nil { 223 | return errors.New("cache is nil") 224 | } 225 | a, e := util.AnyToStr(val) 226 | if e != nil { 227 | err = e 228 | return 229 | } 230 | cache.AddSetItem(key, a, timeout) 231 | return 232 | } 233 | 234 | func Del(key string) { 235 | if cache == nil { 236 | return 237 | } 238 | cache.Del(key) 239 | } 240 | 241 | func DelNow(key string) { 242 | if cache == nil { 243 | return 244 | } 245 | cache.DelNow(key) 246 | } 247 | 248 | // NewLock 249 | // @Description: 创建一个redis锁,用于分布式锁,需要在redis环境下使用,使用完毕后需要调用UnLock释放锁 250 | // @param key 251 | // @return *redislock.Lock 252 | // @return error 253 | func NewLock(key string) (*redislock.Lock, error) { 254 | if cache == nil { 255 | return nil, errors.New("cache is nil") 256 | } 257 | if r, ok := cache.(iRedisCacheService); ok { 258 | return r.TryLock(key) 259 | } 260 | return nil, errors.New("cache is not redis") 261 | } 262 | 263 | // UnLock 264 | // @Description: 释放锁 265 | // @param l 266 | func UnLock(l *redislock.Lock) { 267 | if cache == nil { 268 | return 269 | } 270 | if r, ok := cache.(iRedisCacheService); ok { 271 | r.TryUnLock(l, context.Background()) 272 | } 273 | } 274 | 275 | // Incr 276 | // @Description: 释放锁 277 | // @param l 278 | func Incr(key string, timeout time.Duration) (res int64, err error) { 279 | if cache == nil { 280 | return 281 | } 282 | if r, ok := cache.(iRedisCacheService); ok { 283 | res, err = r.Incr(key, timeout) 284 | } 285 | return 286 | } 287 | 288 | // IncrBy 289 | // @Description: 释放锁 290 | // @param l 291 | func IncrBy(key string, val float64, timeout time.Duration) (res float64, err error) { 292 | if cache == nil { 293 | return 294 | } 295 | if r, ok := cache.(iRedisCacheService); ok { 296 | res, err = r.IncrBy(key, val, timeout) 297 | } 298 | return 299 | } 300 | 301 | // LLen 302 | // @Description: 获取list长度 303 | // @param l 304 | func LLen(key string) (res int64, err error) { 305 | if cache == nil { 306 | return 307 | } 308 | if r, ok := cache.(iRedisCacheService); ok { 309 | res, err = r.LLen(key) 310 | } 311 | return 312 | } 313 | 314 | func GetRedisClient() redis.UniversalClient { 315 | if cache == nil { 316 | return nil 317 | } 318 | if r, ok := cache.(*redisService); ok { 319 | return r.GetClient() 320 | } 321 | return nil 322 | } 323 | 324 | // AutoCacheBuilder [Key comparable,Val any] 325 | // @Description: 自动化缓存Builder 326 | type AutoCacheBuilder[Key cacheKey, Val any] struct { 327 | 328 | //数据是否在本地存储 329 | mem bool 330 | 331 | // 332 | 333 | //数据是否缓存 334 | cache bool 335 | //获取唯一key的拼接函数 336 | keyFun string 337 | 338 | // 339 | 340 | //数据是否持久化 341 | longevity bool 342 | longevityInterval time.Duration 343 | // 344 | //是否自动清除过期数据 345 | autoClear bool 346 | cacheTimeOut time.Duration 347 | memTimeOutSecond int64 348 | 349 | plugins []IAutoCacheClearPlugin 350 | } 351 | 352 | type HashAutoCacheBuilder[Val IHashModel] struct { 353 | AutoCacheBuilder[string, Val] 354 | image Val 355 | } 356 | 357 | func (h *HashAutoCacheBuilder[Val]) New() IHashCacheService[Val] { 358 | manager := &hashAutoCacheManager[Val]{} 359 | manager.builder = &h.AutoCacheBuilder 360 | manager.builder.WithCloseAutoClearCache() 361 | 362 | // Use reflection to create a new instance of Val 363 | valType := reflect.TypeOf(h.image).Elem() 364 | newVal := reflect.New(valType).Interface() 365 | 366 | // Cast the newVal to the type Val 367 | h.image = newVal.(Val) 368 | 369 | manager.InitStruct(h.image) 370 | return manager 371 | } 372 | func (h *HashAutoCacheBuilder[Val]) WithAutoCache(cacheKey string, cacheTimeOut time.Duration) *HashAutoCacheBuilder[Val] { 373 | h.AutoCacheBuilder.WithAutoCache(cacheKey, cacheTimeOut) 374 | return h 375 | } 376 | func (h *HashAutoCacheBuilder[Val]) WithMemCache(memTimeOutSecond uint32) *HashAutoCacheBuilder[Val] { 377 | h.AutoCacheBuilder.WithMemCache(memTimeOutSecond) 378 | return h 379 | } 380 | func (h *HashAutoCacheBuilder[Val]) WithLongevityCache(updateInterval time.Duration) *HashAutoCacheBuilder[Val] { 381 | h.AutoCacheBuilder.WithLongevityCache(updateInterval) 382 | return h 383 | } 384 | func (h *HashAutoCacheBuilder[Val]) WithCloseAutoClearCache() *HashAutoCacheBuilder[Val] { 385 | h.AutoCacheBuilder.WithCloseAutoClearCache() 386 | return h 387 | } 388 | func (h *HashAutoCacheBuilder[Val]) WithAutoClearPlugins(plugin IAutoCacheClearPlugin) *HashAutoCacheBuilder[Val] { 389 | h.AutoCacheBuilder.WithAutoClearPlugins(plugin) 390 | return h 391 | } 392 | 393 | func (a *AutoCacheBuilder[Key, Val]) New() IAutoCacheService[Key, Val] { 394 | var () 395 | manager := &autoCacheManager[Key, Val]{} 396 | manager.builder = a 397 | manager.InitStruct() 398 | return manager 399 | } 400 | func (a *AutoCacheBuilder[Key, Val]) WithAutoCache(cacheKey string, cacheTimeOut time.Duration) *AutoCacheBuilder[Key, Val] { 401 | var () 402 | a.cache = true 403 | a.keyFun = cacheKey 404 | 405 | if cacheTimeOut > 0 { 406 | a.cacheTimeOut = cacheTimeOut 407 | } 408 | 409 | return a 410 | } 411 | func (a *AutoCacheBuilder[Key, Val]) WithCloseAutoClearCache() *AutoCacheBuilder[Key, Val] { 412 | a.autoClear = false 413 | return a 414 | } 415 | func (a *AutoCacheBuilder[Key, Val]) WithMemCache(memTimeOutSecond uint32) *AutoCacheBuilder[Key, Val] { 416 | var () 417 | a.mem = true 418 | if memTimeOutSecond>>31 == 1 { 419 | memTimeOutSecond = 0 420 | } 421 | if memTimeOutSecond != 0 { 422 | a.autoClear = true 423 | } 424 | a.memTimeOutSecond = int64(memTimeOutSecond) 425 | 426 | return a 427 | } 428 | func (a *AutoCacheBuilder[Key, Val]) WithAutoClearPlugins(plugin IAutoCacheClearPlugin) *AutoCacheBuilder[Key, Val] { 429 | var () 430 | a.plugins = append(a.plugins, plugin) 431 | return a 432 | } 433 | 434 | func (a *AutoCacheBuilder[Key, Val]) WithLongevityCache(updateInterval time.Duration) *AutoCacheBuilder[Key, Val] { 435 | a.longevity = true 436 | if updateInterval < time.Second { 437 | log.WarnTag("orm", "updateInterval minimum is 1 second") 438 | updateInterval = time.Second 439 | } 440 | a.longevityInterval = updateInterval 441 | return a 442 | } 443 | 444 | // NewDefaultAutoCacheManager [Key comparable, Val any] 445 | // 446 | // @Description: 创建一个默认的自动化数据管理,默认不包含持久化数据落地(mysql),包含本地缓存,cache缓存(redis) 447 | // @param cacheKey cache缓存使用的组合key,例如user:1001 那么这里应该传入user即可,拼装方式为cacheKey:key 448 | // @return IAutoCacheService [Key comparable, Val any] 返回一个全新的自动化数据缓存管理对象 449 | func NewDefaultAutoCacheManager[Key cacheKey, Val any](cacheKey string) IAutoCacheService[Key, Val] { 450 | builder := &AutoCacheBuilder[Key, Val]{} 451 | builder.keyFun = cacheKey 452 | builder.mem = true 453 | builder.plugins = make([]IAutoCacheClearPlugin, 0) 454 | builder.autoClear = true 455 | builder.cache = true 456 | builder.cacheTimeOut = time.Hour * 24 * 3 457 | builder.memTimeOutSecond = 60 * 60 * 3 458 | builder.longevity = false 459 | return builder.New() 460 | } 461 | 462 | // NewLongevityAutoCacheManager [Key comparable, Val any] 463 | // 464 | // @Description: 创建一个持久化的自动化数据管理,包含持久化数据落地(mysql),包含本地缓存,cache缓存(redis) 465 | // @param cacheKey 466 | // @param tableName 467 | // @return IAutoCacheService [Key comparable, Val any] 468 | func NewLongevityAutoCacheManager[Key cacheKey, Val IModel](cacheKey string) IAutoCacheService[Key, Val] { 469 | builder := &AutoCacheBuilder[Key, Val]{} 470 | builder.keyFun = cacheKey 471 | builder.mem = true 472 | builder.plugins = make([]IAutoCacheClearPlugin, 0) 473 | builder.autoClear = true 474 | builder.cache = true 475 | builder.cacheTimeOut = time.Hour * 24 * 3 476 | builder.memTimeOutSecond = 60 * 60 * 3 477 | builder.longevity = true 478 | return builder.New() 479 | } 480 | 481 | // NewAutoCacheManager [Key comparable, Val any] 482 | // @Description: 创建一个持久化的自动化数据管理,包含本地缓存,不包含持久化数据落地(mysql),cache缓存(redis) 483 | func NewAutoCacheManager[Key cacheKey, Val any](memTimeOutSecond int64) IAutoCacheService[Key, Val] { 484 | builder := &AutoCacheBuilder[Key, Val]{} 485 | builder.plugins = make([]IAutoCacheClearPlugin, 0) 486 | builder.keyFun = "" 487 | builder.mem = true 488 | builder.cache = false 489 | builder.longevity = false 490 | builder.memTimeOutSecond = memTimeOutSecond 491 | return builder.New() 492 | } 493 | 494 | func NewAutoCacheBuilder[Key cacheKey, Val any]() *AutoCacheBuilder[Key, Val] { 495 | builder := &AutoCacheBuilder[Key, Val]{} 496 | builder.plugins = make([]IAutoCacheClearPlugin, 0) 497 | builder.mem = true 498 | builder.memTimeOutSecond = 60 * 60 * 3 499 | return builder 500 | } 501 | 502 | func NewHashAutoCacheBuilder[Val IHashModel]() *HashAutoCacheBuilder[Val] { 503 | builder := &HashAutoCacheBuilder[Val]{} 504 | builder.plugins = make([]IAutoCacheClearPlugin, 0) 505 | builder.mem = true 506 | builder.memTimeOutSecond = 60 * 60 * 3 507 | return builder 508 | } 509 | 510 | func WithCacheModule(module tgf.CacheModule) { 511 | cacheModule = module 512 | } 513 | 514 | func run() { 515 | switch cacheModule { 516 | case tgf.CacheModuleRedis: 517 | cache = newRedisService() 518 | case tgf.CacheModuleClose: 519 | return 520 | } 521 | //初始化mysql 522 | initMySql() 523 | } 524 | -------------------------------------------------------------------------------- /db/cache_test.go: -------------------------------------------------------------------------------- 1 | package db_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/thkhxm/tgf/db" 6 | "reflect" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | //*************************************************** 12 | //@Link https://github.com/thkhxm/tgf 13 | //@Link https://gitee.com/timgame/tgf 14 | //@QQ群 7400585 15 | //author tim.huang 16 | //@Description 17 | //2023/3/14 18 | //*************************************************** 19 | 20 | func TestDefaultAutoCacheManager(t *testing.T) { 21 | db.Run() 22 | cacheManager := db.NewDefaultAutoCacheManager[string, int64]("example") 23 | key := "1001" 24 | var setVal int64 = 10086 25 | val, err := cacheManager.Get(key) 26 | if err != nil { 27 | t.Errorf("[test] cache get error %v", err) 28 | return 29 | } 30 | // 31 | t.Logf("[test] cache first get key %v , val %v", key, val) 32 | 33 | cacheManager.Set(setVal, key) 34 | t.Logf("[test] cache set key %v , val %v ", key, setVal) 35 | val, err = cacheManager.Get(key) 36 | if err != nil { 37 | t.Errorf("[test] cache get error %v", err) 38 | return 39 | } 40 | t.Logf("[test] cache second get key %v , val %v", key, val) 41 | 42 | //first run 43 | //cache_test.go:26: [test] cache first get key 1001 , val 0 44 | //cache_test.go:29: [test] cache set key 1001 , val 0 45 | //cache_test.go:35: [test] cache second get key 1001 , val 10086 46 | 47 | //second run 48 | //cache_test.go:26: [test] cache first get key 1001 , val 10086 49 | //cache_test.go:29: [test] cache set key 1001 , val 10086 50 | //cache_test.go:35: [test] cache second get key 1001 , val 10086 51 | } 52 | 53 | func TestNewAutoCacheBuilder(t *testing.T) { 54 | db.Run() 55 | builder := db.NewAutoCacheBuilder[string, *ExampleUser]() 56 | builder.WithLongevityCache(time.Second * 5) 57 | builder.WithAutoCache("example", time.Minute*5) 58 | manager := builder.New() 59 | for i := 0; i < 50; i++ { 60 | id := fmt.Sprintf("%v", i) 61 | name := fmt.Sprintf("tim2-%v", i) 62 | data, _ := manager.Get(id) 63 | if data == nil { 64 | data = &ExampleUser{ 65 | Id: id, NickName: name, 66 | } 67 | manager.Set(data, id) 68 | } 69 | data.NickName = name 70 | manager.Push(data.Id) 71 | } 72 | manager.Reset() 73 | time.Sleep(time.Second * 5) 74 | //var longevityManager = db.NewLongevityAutoCacheManager[string, *ExampleUser]("test:user") 75 | //data, _ := longevityManager.Get("1", "2") 76 | //t.Log("---->", *data) 77 | } 78 | 79 | type DemoUser struct { 80 | Name string 81 | } 82 | 83 | type ExampleUser struct { 84 | Id string `orm:"pk"` 85 | NickName string 86 | ignore string 87 | } 88 | 89 | func (e ExampleUser) GetTableName() string { 90 | return "t_user" 91 | } 92 | 93 | func TestGet(t *testing.T) { 94 | db.Run() 95 | key := "testKey" 96 | expectedVal := "testValue" 97 | db.Set(key, expectedVal, time.Minute) 98 | 99 | val, success := db.Get[string](key) 100 | if !success || val != expectedVal { 101 | t.Errorf("Get() = %v, want %v", val, expectedVal) 102 | } 103 | } 104 | 105 | func TestSet(t *testing.T) { 106 | db.Run() 107 | key := "testKey" 108 | val := "testValue" 109 | db.Set(key, val, time.Minute) 110 | 111 | retrievedVal, success := db.Get[string](key) 112 | if !success || retrievedVal != val { 113 | t.Errorf("Set() failed, retrieved value = %v, want %v", retrievedVal, val) 114 | } 115 | } 116 | 117 | func TestGetMap(t *testing.T) { 118 | db.Run() 119 | key := "testKey" 120 | expectedMap := map[string]string{"field1": "value1", "field2": "value2"} 121 | db.PutMap(key, "field1", "value1", time.Minute) 122 | db.PutMap(key, "field2", "value2", time.Minute) 123 | 124 | retrievedMap, success := db.GetMap[string, string](key) 125 | if !success || !reflect.DeepEqual(retrievedMap, expectedMap) { 126 | t.Errorf("GetMap() = %v, want %v", retrievedMap, expectedMap) 127 | } 128 | } 129 | 130 | func TestPutMap(t *testing.T) { 131 | db.Run() 132 | 133 | key := "testKey" 134 | field := "testField" 135 | val := "testValue" 136 | db.PutMap(key, field, val, time.Minute) 137 | 138 | retrievedMap, success := db.GetMap[string, string](key) 139 | if !success || retrievedMap[field] != val { 140 | t.Errorf("PutMap() failed, retrieved value = %v, want %v", retrievedMap[field], val) 141 | } 142 | } 143 | 144 | func TestGetList(t *testing.T) { 145 | db.Run() 146 | 147 | key := "testListKey" 148 | expectedList := []string{"value1", "value2", "value3"} 149 | db.DelNow(key) 150 | 151 | db.AddListItem(key, time.Minute, expectedList...) 152 | 153 | retrievedList := db.GetList[string](key) 154 | if !reflect.DeepEqual(retrievedList, expectedList) { 155 | t.Errorf("GetList() = %v, want %v", retrievedList, expectedList) 156 | } 157 | } 158 | 159 | func TestAddListItem(t *testing.T) { 160 | db.Run() 161 | key := "testKey" 162 | val := "testValue" 163 | db.AddListItem(key, time.Minute, val) 164 | 165 | retrievedList := db.GetList[string](key) 166 | if len(retrievedList) != 1 || retrievedList[0] != val { 167 | t.Errorf("AddListItem() failed, retrieved list = %v, want %v", retrievedList, []string{val}) 168 | } 169 | } 170 | 171 | func TestDel(t *testing.T) { 172 | db.Run() 173 | key := "testKey" 174 | val := "testValue" 175 | db.Set(key, val, time.Minute) 176 | db.Del(key) 177 | 178 | _, success := db.Get[string](key) 179 | if success { 180 | t.Errorf("Del() failed, value still exists for key %v", key) 181 | } 182 | } 183 | 184 | func TestDelNow(t *testing.T) { 185 | db.Run() 186 | key := "testKey" 187 | val := "testValue" 188 | db.Set(key, val, time.Minute) 189 | db.DelNow(key) 190 | 191 | _, success := db.Get[string](key) 192 | if !success { 193 | t.Errorf("DelNow() failed, value still exists for key %v", key) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /db/init.go: -------------------------------------------------------------------------------- 1 | // Package db 2 | // @Description: 数据缓存相关 3 | // @Link https://github.com/bsm/redislock redis分布式锁 4 | package db 5 | 6 | //*************************************************** 7 | //@Link https://github.com/thkhxm/tgf 8 | //@Link https://gitee.com/timgame/tgf 9 | //@QQ群 7400585 10 | //author tim.huang 11 | //@Description 12 | //2023/2/25 13 | //*************************************************** 14 | 15 | func init() { 16 | 17 | } 18 | 19 | func Run() { 20 | run() 21 | } 22 | -------------------------------------------------------------------------------- /db/manager_test.go: -------------------------------------------------------------------------------- 1 | package db_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/thkhxm/tgf/db" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | //*************************************************** 13 | //@Link https://github.com/thkhxm/tgf 14 | //@Link https://gitee.com/timgame/tgf 15 | //@QQ群 7400585 16 | //author tim.huang 17 | //@Description 18 | //2023/3/16 19 | //*************************************************** 20 | 21 | type Cc struct { 22 | CacheExampleData 23 | } 24 | 25 | func (c *Cc) GetTableName() string { 26 | return "user" 27 | } 28 | 29 | func Test_autoCacheManager_Get(t *testing.T) { 30 | db.Run() 31 | cacheManager := db.NewLongevityAutoCacheManager[string, *Cc]("example:cachemanager") 32 | key := "123" 33 | val := &Cc{ 34 | CacheExampleData: CacheExampleData{Name: "tim", Model: db.NewModel()}, 35 | } 36 | cacheManager.Set(val, key) 37 | //time.Sleep(time.Second * 2) 38 | //val.Remove() 39 | //cacheManager.Remove(key) 40 | //time.Sleep(time.Second * 1) 41 | if cc, e := cacheManager.Get(key); e == nil { 42 | t.Log("get val ", cc) 43 | } else { 44 | t.Log("get val err ", e) 45 | } 46 | select {} 47 | //data, _ := cacheManager.Get(key) 48 | //data.Name = "sam" 49 | //t.Log("get val ", data, reflect.DeepEqual(val, data)) 50 | //data2, _ := cacheManager.Get(key) 51 | //t.Log("get val ", data2) 52 | 53 | //con, _ := db.GetConn().PrepareContext(context.Background(), "INSERT INTO ... ON DUPLICATE KEY UPDATE") 54 | //rs, err := con.Exec("1", "a1", 1, "1", "a1", 2) 55 | //t.Log("err -> ", err) 56 | //o, _ := rs.RowsAffected() 57 | //t.Log("ok -> ", o) 58 | //defer con.Close() 59 | //w := &sync.WaitGroup{} 60 | //w.Add(1) 61 | //w.Wait() 62 | } 63 | 64 | func BenchmarkRef(b *testing.B) { 65 | for i := 0; i < b.N; i++ { 66 | _ = fmt.Sprintf("%v,%v", 1, 2) 67 | } 68 | } 69 | 70 | func BenchmarkComm(b *testing.B) { 71 | for i := 0; i < b.N; i++ { 72 | v := CacheExampleData{Name: "a"} 73 | StructToString(v) 74 | } 75 | } 76 | 77 | func StructToString(s interface{}) string { 78 | v := reflect.ValueOf(s) 79 | t := v.Type() 80 | var fields []string 81 | for i := 0; i < t.NumField(); i++ { 82 | field := v.Field(i) 83 | // 如果字段是零值则跳过 84 | if field.IsZero() { 85 | continue 86 | } 87 | // 将字段值转换为字符串 88 | fieldValue := field.Interface() 89 | str := fmt.Sprintf("%v", fieldValue) 90 | // 添加到 fields 列表中 91 | fields = append(fields, str) 92 | } 93 | // 使用逗号连接所有字段的字符串值 94 | return strings.Join(fields, ",") 95 | } 96 | 97 | type CacheExampleData struct { 98 | db.Model 99 | Uid string `orm:"pk"` 100 | Name string 101 | } 102 | 103 | func Test_convertCamelToSnake(t *testing.T) { 104 | type args struct { 105 | s string 106 | } 107 | tests := []struct { 108 | name string 109 | args args 110 | want string 111 | }{ 112 | {name: "UserId", args: args{"UserId"}, want: "user_id"}, 113 | } 114 | for _, tt := range tests { 115 | t.Run(tt.name, func(t *testing.T) { 116 | if got := db.ConvertCamelToSnake(tt.args.s); got != tt.want { 117 | t.Errorf("convertCamelToSnake() = %v, want %v", got, tt.want) 118 | } 119 | }) 120 | } 121 | } 122 | 123 | type Item struct { 124 | db.Model 125 | UserId string `orm:"pk;pkList"` 126 | PropId string `orm:"pk"` 127 | Amount uint64 128 | } 129 | 130 | func (i *Item) GetTableName() string { 131 | return "game_prop" 132 | } 133 | 134 | func (i *Item) HashCachePkKey(key ...string) string { 135 | return key[0] 136 | } 137 | 138 | func (i *Item) HashCacheFieldByVal() string { 139 | return i.PropId 140 | } 141 | 142 | func (i *Item) HashCacheFieldByKeys(key ...string) string { 143 | return key[1] 144 | } 145 | 146 | func NewTestHashCacheManager() db.IHashCacheService[*Item] { 147 | db.Run() 148 | builder := db.NewHashAutoCacheBuilder[*Item]() 149 | return builder.WithLongevityCache(time.Second * 5). 150 | WithMemCache(5). 151 | New() 152 | } 153 | 154 | func Test_hashAutoCacheManager_Get(t *testing.T) { 155 | type args struct { 156 | key []string 157 | } 158 | type testCase[Val db.IHashModel] struct { 159 | name string 160 | h db.IHashCacheService[Val] 161 | args args 162 | wantVal db.IHashModel 163 | wantErr bool 164 | } 165 | tests := []testCase[*Item]{ 166 | {name: "example1", h: NewTestHashCacheManager(), 167 | args: args{key: []string{"123", "2"}}, 168 | wantVal: &Item{UserId: "123", PropId: "2", Amount: 1}, 169 | wantErr: false, 170 | }, 171 | } 172 | for _, tt := range tests { 173 | t.Run(tt.name, func(t *testing.T) { 174 | gotVal, err := tt.h.Get(tt.args.key...) 175 | if (err != nil) != tt.wantErr { 176 | t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr) 177 | return 178 | } 179 | if !reflect.DeepEqual(gotVal, tt.wantVal) { 180 | t.Errorf("Get() gotVal = %v, want %v", gotVal, tt.wantVal) 181 | } 182 | }) 183 | } 184 | } 185 | 186 | func Test_hashAutoCacheManager_GetAll(t *testing.T) { 187 | 188 | type args struct { 189 | key []string 190 | } 191 | type testCase[Val db.IHashModel] struct { 192 | name string 193 | h db.IHashCacheService[Val] 194 | args args 195 | wantVal []db.IHashModel 196 | wantErr bool 197 | } 198 | tests := []testCase[*Item]{ 199 | {name: "example1", h: NewTestHashCacheManager(), args: args{key: []string{"123"}}, wantVal: []db.IHashModel{&Item{ 200 | UserId: "123", 201 | PropId: "1", 202 | Amount: 1, 203 | }, &Item{ 204 | UserId: "123", 205 | PropId: "2", 206 | Amount: 1, 207 | }}, 208 | wantErr: false}, 209 | } 210 | for _, tt := range tests { 211 | t.Run(tt.name, func(t *testing.T) { 212 | gotVal, err := tt.h.GetAll(tt.args.key...) 213 | if (err != nil) != tt.wantErr { 214 | t.Errorf("GetAll() error = %v, wantErr %v", err, tt.wantErr) 215 | return 216 | } 217 | if !reflect.DeepEqual(gotVal, tt.wantVal) { 218 | t.Errorf("GetAll() gotVal = %v, want %v", gotVal, tt.wantVal) 219 | } 220 | }) 221 | } 222 | } 223 | 224 | func Test_hashAutoCacheManager_Push(t *testing.T) { 225 | type args struct { 226 | key []string 227 | } 228 | type testCase[Val db.IHashModel] struct { 229 | name string 230 | h db.IHashCacheService[Val] 231 | args args 232 | } 233 | tests := []testCase[*Item]{ 234 | // TODO: Add test cases. 235 | } 236 | for _, tt := range tests { 237 | t.Run(tt.name, func(t *testing.T) { 238 | tt.h.Push(tt.args.key...) 239 | }) 240 | } 241 | } 242 | 243 | func Test_hashAutoCacheManager_Remove(t *testing.T) { 244 | type args struct { 245 | key []string 246 | } 247 | type testCase[Val db.IHashModel] struct { 248 | name string 249 | h db.IHashCacheService[Val] 250 | args args 251 | wantSuccess bool 252 | } 253 | tests := []testCase[*Item]{ 254 | // TODO: Add test cases. 255 | } 256 | for _, tt := range tests { 257 | t.Run(tt.name, func(t *testing.T) { 258 | if gotSuccess := tt.h.Remove(tt.args.key...); gotSuccess != tt.wantSuccess { 259 | t.Errorf("Remove() = %v, want %v", gotSuccess, tt.wantSuccess) 260 | } 261 | }) 262 | } 263 | } 264 | 265 | func Test_hashAutoCacheManager_Set(t *testing.T) { 266 | type args[Val db.IHashModel] struct { 267 | val Val 268 | key []string 269 | } 270 | type testCase[Val db.IHashModel] struct { 271 | name string 272 | h db.IHashCacheService[Val] 273 | args args[Val] 274 | wantSuccess bool 275 | } 276 | tests := []testCase[*Item]{ 277 | {name: "add item", h: NewTestHashCacheManager(), args: args[*Item]{val: &Item{ 278 | UserId: "123", 279 | PropId: "2", 280 | Amount: 1, 281 | }, key: []string{"123", "2"}}, wantSuccess: true}, 282 | } 283 | for _, tt := range tests { 284 | t.Run(tt.name, func(t *testing.T) { 285 | if gotSuccess := tt.h.Set(tt.args.val, tt.args.key...); gotSuccess != tt.wantSuccess { 286 | t.Errorf("Set() = %v, want %v", gotSuccess, tt.wantSuccess) 287 | } 288 | }) 289 | } 290 | select {} 291 | } 292 | -------------------------------------------------------------------------------- /db/mysql.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | _ "github.com/go-sql-driver/mysql" 8 | "github.com/thkhxm/tgf" 9 | "github.com/thkhxm/tgf/log" 10 | "time" 11 | ) 12 | 13 | //*************************************************** 14 | //@Link https://github.com/thkhxm/tgf 15 | //@Link https://gitee.com/timgame/tgf 16 | //@QQ群 7400585 17 | //author tim.huang 18 | //@Description 19 | //2023/3/22 20 | //*************************************************** 21 | 22 | var ( 23 | pk = "pk" 24 | ignore = "ignore" 25 | list = "pkList" 26 | ) 27 | 28 | var dbService *mysqlService 29 | 30 | type Model struct { 31 | State uint8 //数据状态,0为删除,1为正常 32 | } 33 | 34 | func NewModel() Model { 35 | res := Model{ 36 | State: 1, 37 | } 38 | return res 39 | } 40 | 41 | func (m *Model) Remove() { 42 | m.State = 0 43 | } 44 | 45 | // IsValid 46 | // @Description: 是否有效 47 | // @receiver m 48 | // @return bool 49 | func (m *Model) IsValid() bool { 50 | return m.State == 1 51 | } 52 | 53 | type IModel interface { 54 | GetTableName() string 55 | Remove() 56 | } 57 | 58 | type mysqlService struct { 59 | running bool 60 | db *sql.DB 61 | executeChan chan string 62 | } 63 | 64 | func (m *mysqlService) isRunning() bool { 65 | var () 66 | return m.running 67 | } 68 | func (m *mysqlService) getConnection() *sql.Conn { 69 | var () 70 | if !m.isRunning() { 71 | return nil 72 | } 73 | if conn, err := m.db.Conn(context.Background()); err == nil { 74 | return conn 75 | } 76 | return nil 77 | } 78 | 79 | func (m *mysqlService) AsyncExecuteUpdateOrCreate(sqlScript string) { 80 | var () 81 | dbService.executeChan <- sqlScript 82 | } 83 | 84 | func GetConn() *sql.Conn { 85 | return dbService.getConnection() 86 | } 87 | 88 | func initMySql() { 89 | var ( 90 | err error 91 | d *sql.DB 92 | userName = tgf.GetStrConfig[string](tgf.EnvironmentMySqlUser) 93 | password = tgf.GetStrConfig[string](tgf.EnvironmentMySqlPwd) 94 | hostName = tgf.GetStrConfig[string](tgf.EnvironmentMySqlAddr) 95 | port = tgf.GetStrConfig[string](tgf.EnvironmentMySqlPort) 96 | database = tgf.GetStrConfig[string](tgf.EnvironmentMySqlDB) 97 | ) 98 | 99 | dbService = new(mysqlService) 100 | dbService.executeChan = make(chan string) 101 | // 定义 MySQL 数据库连接信息 102 | dataSourceName := fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?charset=utf8mb4&parseTime=True&loc=Local", userName, password, hostName, port, database) 103 | // 创建数据库连接池 104 | d, err = sql.Open("mysql", dataSourceName) 105 | d.SetMaxIdleConns(10) 106 | d.SetMaxOpenConns(200) 107 | d.SetConnMaxLifetime(300 * time.Second) 108 | if err != nil { 109 | log.WarnTag("init", "mysql dataSourceName is wrong") 110 | return 111 | } 112 | //defer db.Close() 113 | if err = d.Ping(); err != nil { 114 | log.WarnTag("init", "mysql unable to connect to database") 115 | return 116 | } 117 | dbService.running = true 118 | dbService.db = d 119 | log.InfoTag("init", "mysql is running hostName=%v port=%v database=%v", hostName, port, database) 120 | } 121 | -------------------------------------------------------------------------------- /db/redis.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/bsm/redislock" 7 | "github.com/redis/go-redis/v9" 8 | "github.com/thkhxm/tgf" 9 | "github.com/thkhxm/tgf/log" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // *************************************************** 15 | // @Link https://github.com/thkhxm/tgf 16 | // @Link https://gitee.com/timgame/tgf 17 | // @QQ群 7400585 18 | // author tim.huang 19 | // @Description 20 | // 2023/2/24 21 | // *************************************************** 22 | 23 | var service *redisService 24 | 25 | type redisService struct { 26 | client redis.UniversalClient 27 | cluster redis.ClusterClient 28 | } 29 | 30 | func (r *redisService) GetClient() redis.UniversalClient { 31 | return r.client 32 | } 33 | 34 | func (r *redisService) Get(key string) (res string) { 35 | var ( 36 | err error 37 | ) 38 | if res, err = r.client.Get(context.Background(), key).Result(); err == nil || errors.Is(err, redis.Nil) { 39 | return 40 | } 41 | log.Error("[redis] 获取缓存数据异常 key=%v,err=%v", key, err) 42 | return 43 | } 44 | 45 | func (r *redisService) Set(key string, val interface{}, timeout time.Duration) { 46 | r.client.Set(context.Background(), key, val, timeout) 47 | } 48 | 49 | func (r *redisService) GetMap(key string) map[string]string { 50 | res, _ := r.client.HGetAll(context.Background(), key).Result() 51 | return res 52 | } 53 | 54 | func (r *redisService) PutMap(key, filed, val string, timeout time.Duration) { 55 | var () 56 | r.client.HSet(context.Background(), key, filed, val) 57 | if timeout > 0 { 58 | r.client.Expire(context.Background(), key, timeout) 59 | } 60 | } 61 | 62 | func (r *redisService) Del(key string) { 63 | var () 64 | r.client.Expire(context.Background(), key, time.Second) 65 | } 66 | 67 | func (r *redisService) DelNow(key string) { 68 | var () 69 | r.client.Del(context.Background(), key) 70 | } 71 | 72 | func (r *redisService) GetList(key string, start, end int64) (res []string, err error) { 73 | var () 74 | res, err = r.client.LRange(context.Background(), key, start, end).Result() 75 | return 76 | } 77 | 78 | func (r *redisService) SetList(key string, l []interface{}, timeout time.Duration) { 79 | var () 80 | r.client.RPush(context.Background(), key, l...) 81 | if timeout > 0 { 82 | r.client.Expire(context.Background(), key, timeout) 83 | } 84 | } 85 | 86 | func (r *redisService) AddListItem(key string, val string, timeout time.Duration) { 87 | var () 88 | r.client.LPush(context.Background(), key, val) 89 | if timeout > 0 { 90 | r.client.Expire(context.Background(), key, timeout) 91 | } 92 | } 93 | 94 | func (r *redisService) TryLock(key string) (*redislock.Lock, error) { 95 | var () 96 | lock := redislock.New(r.client) 97 | return lock.Obtain(context.Background(), key, time.Second*5, nil) 98 | } 99 | 100 | func (r *redisService) TryUnLock(l *redislock.Lock, ctx context.Context) { 101 | var () 102 | l.Release(ctx) 103 | } 104 | 105 | func (r *redisService) Incr(key string, timeout time.Duration) (res int64, err error) { 106 | var () 107 | fc := r.client.Incr(context.Background(), key) 108 | if timeout > 0 { 109 | r.client.Expire(context.Background(), key, timeout) 110 | } 111 | return fc.Val(), nil 112 | } 113 | 114 | func (r *redisService) IncrBy(key string, val float64, timeout time.Duration) (res float64, err error) { 115 | var () 116 | fc := r.client.IncrByFloat(context.Background(), key, val) 117 | if timeout > 0 { 118 | r.client.Expire(context.Background(), key, timeout) 119 | } 120 | return fc.Val(), nil 121 | } 122 | 123 | func (r *redisService) LLen(key string) (res int64, err error) { 124 | var () 125 | i := r.client.LLen(context.Background(), key) 126 | return i.Val(), nil 127 | } 128 | 129 | func (r *redisService) GetSet(key string) (res []string, err error) { 130 | data := r.client.SMembers(context.Background(), key) 131 | return data.Result() 132 | } 133 | func (r *redisService) AddSetItem(key string, val interface{}, timeout time.Duration) { 134 | r.client.SAdd(context.Background(), key, val) 135 | if timeout > 0 { 136 | r.client.Expire(context.Background(), key, timeout) 137 | } 138 | } 139 | 140 | func newRedisService() *redisService { 141 | var ( 142 | addr = tgf.GetStrConfig[string](tgf.EnvironmentRedisAddr) 143 | password = tgf.GetStrConfig[string](tgf.EnvironmentRedisPassword) 144 | db = tgf.GetStrConfig[int](tgf.EnvironmentRedisDB) 145 | cluster = tgf.GetStrConfig[int](tgf.EnvironmentRedisCluster) 146 | ) 147 | 148 | service = new(redisService) 149 | 150 | if cluster == 1 { 151 | redisOptions := &redis.ClusterOptions{} 152 | redisOptions.Addrs = strings.Split(addr, ",") 153 | if password != "" { 154 | redisOptions.Password = password 155 | } 156 | service.client = redis.NewClusterClient(redisOptions) 157 | } else { 158 | redisOptions := &redis.UniversalOptions{} 159 | redisOptions.Addrs = strings.Split(addr, ",") 160 | redisOptions.DB = db 161 | if password != "" { 162 | redisOptions.Password = password 163 | } 164 | service.client = redis.NewUniversalClient(redisOptions) 165 | } 166 | 167 | if stat := service.client.Ping(context.Background()); stat.Err() != nil { 168 | log.WarnTag("init", "启动redis服务异常 addr=%v db=%v err=%v", addr, db, stat.Err()) 169 | return nil 170 | } 171 | 172 | log.InfoTag("init", "启动redis服务 addr=%v db=%v", addr, db) 173 | return service 174 | } 175 | -------------------------------------------------------------------------------- /define.go: -------------------------------------------------------------------------------- 1 | package tgf 2 | 3 | //*************************************************** 4 | //@Link https://github.com/thkhxm/tgf 5 | //@Link https://gitee.com/timgame/tgf 6 | //@QQ群 7400585 7 | //author tim.huang 8 | //@Description 9 | //2023/2/22 10 | //*************************************************** 11 | 12 | const Logo = ` 13 | 14 | 15 | __/\\\\\\\\\\\\\\\_ _____/\\\\\\\\\\\\_ __/\\\\\\\\\\\\\\\_ 16 | _\///////\\\/////__ ___/\\\//////////__ _\/\\\///////////__ 17 | _______\/\\\_______ __/\\\_____________ _\/\\\_____________ 18 | _______\/\\\_______ _\/\\\____/\\\\\\\_ _\/\\\\\\\\\\\_____ 19 | _______\/\\\_______ _\/\\\___\/////\\\_ _\/\\\///////______ 20 | _______\/\\\_______ _\/\\\_______\/\\\_ _\/\\\_____________ 21 | _______\/\\\_______ _\/\\\_______\/\\\_ _\/\\\_____________ 22 | _______\/\\\_______ _\//\\\\\\\\\\\\/__ _\/\\\_____________ 23 | _______\///________ __\////////////____ _\///______________ 24 | 25 | 26 | ` 27 | 28 | // 运行环境 29 | const ( 30 | // RuntimeModuleDev 开发环境 31 | RuntimeModuleDev = "dev" 32 | 33 | // RuntimeModuleTest 测试环境 34 | RuntimeModuleTest = "test" 35 | 36 | // RuntimeModuleRelease 生产环境 37 | RuntimeModuleRelease = "release" 38 | ) 39 | 40 | type Environment string 41 | 42 | // 环境变量 43 | const ( 44 | // EnvironmentLoggerPath 日志输出路径 45 | EnvironmentLoggerPath Environment = "LogPath" 46 | 47 | // EnvironmentLoggerLevel 日志最低输出级别 48 | EnvironmentLoggerLevel Environment = "LogLevel" 49 | EnvironmentLoggerIgnoredTags Environment = "LogIgnoredTags" 50 | 51 | // EnvironmentRuntimeModule 运行环境,有以下可选运行环境 52 | // dev test release 53 | // RuntimeModuleDev RuntimeModuleTest RuntimeModuleRelease 54 | EnvironmentRuntimeModule Environment = "RuntimeModule" 55 | 56 | // EnvironmentConsulAddress consul地址 57 | EnvironmentConsulAddress Environment = "ConsulAddress" 58 | 59 | // EnvironmentConsulPath consul路径 60 | // 61 | // 默认使用/tgf,如需区分不同环境可以使用自定义的不同的路径 例如 /test 或者 /dev /tim 62 | EnvironmentConsulPath Environment = "ConsulPath" 63 | 64 | // EnvironmentRedisAddr redis地址 127.0.0.1::6379 65 | EnvironmentRedisAddr Environment = "RedisAddr" 66 | 67 | // EnvironmentRedisPassword redis密码 68 | EnvironmentRedisPassword Environment = "RedisPassword" 69 | 70 | // EnvironmentRedisDB redis的db 71 | EnvironmentRedisDB Environment = "RedisDB" 72 | // EnvironmentRedisCluster redis cluster 开关 0 关闭 1 开启,默认关闭 73 | EnvironmentRedisCluster Environment = "RedisCluster" 74 | 75 | // EnvironmentMySqlUser mysql用户名 76 | EnvironmentMySqlUser Environment = "MySqlUser" 77 | 78 | // EnvironmentMySqlPwd mysql密码 79 | EnvironmentMySqlPwd Environment = "MySqlPwd" 80 | 81 | // EnvironmentMySqlAddr mysql地址 82 | EnvironmentMySqlAddr Environment = "MySqlAddr" 83 | 84 | // EnvironmentMySqlPort mysql端口 85 | EnvironmentMySqlPort Environment = "MySqlPort" 86 | 87 | // EnvironmentMySqlDB mysql库 88 | EnvironmentMySqlDB Environment = "MySqlDB" 89 | 90 | // EnvironmentServicePort 当前进程提供的服务端口 91 | EnvironmentServicePort = "ServicePort" 92 | EnvironmentServiceAddress = "ServiceAddress" 93 | 94 | EnvironmentGatePush = "GatePush" 95 | ) 96 | 97 | type CacheModule int 98 | 99 | const ( 100 | CacheModuleRedis CacheModule = iota 101 | CacheModuleClose 102 | ) 103 | 104 | // redisKey 105 | const ( 106 | RedisKeyUserNodeMeta = "user:node:meta:%v" 107 | ) 108 | 109 | const ( 110 | ContextKeyUserId = "UserId" 111 | ContextKeyRPCType = "RPCType" 112 | ContextKeyTemplateUserId = "TemplateUserId" 113 | ContextKeyNodeId = "NodeId" 114 | ContextKeyCloseLocalCache = "CloseLocalCache" 115 | ContextKeyBroadcastUserIds = "BroadcastUserIds" 116 | ContextKeyTRACEID = "TraceId" 117 | ContextKeyHash = "__hash" 118 | ) 119 | 120 | const ( 121 | RPCTip = "rpc_tip" 122 | RPCBroadcastTip = "rpc_broadcast_tip" 123 | ) 124 | 125 | const GatewayServiceModuleName = "gate" 126 | const MonitorServiceModuleName = "monitor" 127 | const AdminServiceModuleName = "admin" 128 | 129 | var NodeId = "" 130 | var ServerModule = false 131 | -------------------------------------------------------------------------------- /doc/framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thkhxm/tgf/3f3dff490c4d91d95572fec7a78f467333c5796f/doc/framework.png -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package tgf 2 | 3 | import "errors" 4 | 5 | //*************************************************** 6 | //@Link https://github.com/thkhxm/tgf 7 | //@Link https://gitee.com/timgame/tgf 8 | //@QQ群 7400585 9 | //author tim.huang 10 | //@Description 11 | //2023/3/16 12 | //*************************************************** 13 | 14 | type GameError interface { 15 | Error() string 16 | Code() int32 17 | } 18 | 19 | var ( 20 | ErrorRPCTimeOut = errors.New("rpc time out") 21 | LocalEmpty = errors.New("local cache empty") 22 | RedisEmpty = errors.New("redis cache empty") 23 | DBEmpty = errors.New("db cache empty") 24 | ServiceNotFound = errors.New("service not found") 25 | ) 26 | -------------------------------------------------------------------------------- /exp/admin/consul.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "encoding/base64" 5 | "github.com/bytedance/sonic" 6 | "github.com/rpcxio/libkv" 7 | "github.com/rpcxio/libkv/store" 8 | "github.com/rpcxio/libkv/store/consul" 9 | "github.com/smallnest/rpcx/client" 10 | "github.com/thkhxm/tgf" 11 | "log" 12 | "net/http" 13 | "net/url" 14 | "path" 15 | "strings" 16 | ) 17 | 18 | //*************************************************** 19 | //@Link https://github.com/thkhxm/tgf 20 | //@Link https://gitee.com/timgame/tgf 21 | //@QQ群 7400585 22 | //author tim.huang 23 | //@Description 24 | //2024/2/20 25 | //*************************************************** 26 | 27 | type ConsulData struct { 28 | ID string `json:"id"` 29 | ModuleName string `json:"moduleName"` 30 | Address string `json:"address"` 31 | NodeId string `json:"nodeId"` 32 | Group string `json:"group"` 33 | Metadata []*MetaData `json:"metadata"` 34 | Tags []string `json:"tags"` 35 | } 36 | 37 | type MetaData struct { 38 | Key string `json:"key"` 39 | Data string `json:"data"` 40 | } 41 | 42 | var consulRegistry *ConsulRegistry 43 | 44 | type ConsulRegistry struct { 45 | kv store.Store 46 | baseURL string 47 | registryURL string 48 | StateCallBack func(name, address string, state client.ConsulServerState) 49 | } 50 | 51 | func (r *ConsulRegistry) InitRegistry() { 52 | consul.Register() 53 | r.baseURL = tgf.GetStrConfig[string](tgf.EnvironmentConsulPath) 54 | if strings.HasPrefix(r.baseURL, "/") { 55 | r.baseURL = r.baseURL[1:] 56 | } 57 | r.registryURL = tgf.GetStrConfig[string](tgf.EnvironmentConsulAddress) 58 | // 59 | 60 | kv, err := libkv.NewStore(store.CONSUL, []string{r.registryURL}, nil) 61 | if err != nil { 62 | log.Printf("cannot create etcd registry: %v", err) 63 | return 64 | } 65 | r.kv = kv 66 | 67 | return 68 | } 69 | 70 | func (r *ConsulRegistry) fetchServices() []*ConsulData { 71 | var services []*ConsulData 72 | kvs, err := r.kv.List(r.baseURL) 73 | if err != nil { 74 | log.Printf("failed to list services %s: %v", r.baseURL, err) 75 | return services 76 | } 77 | 78 | for _, value := range kvs { 79 | 80 | nodes, err := r.kv.List(value.Key) 81 | if err != nil { 82 | log.Printf("failed to list %s: %v", value.Key, err) 83 | continue 84 | } 85 | 86 | baseServiceName := strings.TrimPrefix(value.Key, r.baseURL) 87 | 88 | for _, n := range nodes { 89 | key := n.Key[:] 90 | i := strings.LastIndex(key, "/") 91 | serviceName := strings.TrimPrefix(key[0:i], r.baseURL) 92 | if serviceName == "" || baseServiceName != serviceName { 93 | continue 94 | } 95 | var serviceAddr string 96 | fields := strings.Split(key, "/") 97 | if fields != nil && len(fields) > 1 { 98 | serviceAddr = fields[len(fields)-1] 99 | } 100 | v, err := url.ParseQuery(string(n.Value[:])) 101 | if err != nil { 102 | log.Println("etcd value parse failed. error: ", err.Error()) 103 | continue 104 | } 105 | state := "n/a" 106 | group := "" 107 | state = v.Get("state") 108 | if state == "" { 109 | state = "active" 110 | } 111 | 112 | version := "v" + v.Get("version") 113 | 114 | group = v.Get("group") 115 | id := base64.StdEncoding.EncodeToString([]byte(serviceName + "^" + serviceAddr)) 116 | md := string(n.Value[:]) 117 | mdv, _ := url.ParseQuery(md) 118 | var metaData []*MetaData 119 | for k, v := range mdv { 120 | metaData = append(metaData, &MetaData{ 121 | Key: k, 122 | Data: v[0], 123 | }) 124 | } 125 | service := &ConsulData{ID: id, ModuleName: serviceName[1:], 126 | Address: strings.Split(serviceAddr, "@")[1], Metadata: metaData, 127 | Tags: []string{state, version}, Group: group, 128 | NodeId: v.Get("nodeId")} 129 | services = append(services, service) 130 | } 131 | } 132 | return services 133 | } 134 | 135 | func (r *ConsulRegistry) DeactivateService(writer http.ResponseWriter, request *http.Request) { 136 | var name, address string 137 | var kv *store.KVPair 138 | var err error 139 | id := request.PathValue("id") 140 | did, _ := base64.StdEncoding.DecodeString(id) 141 | id = string(did) 142 | name = strings.Split(id, "^")[0] 143 | address = strings.Split(id, "^")[1] 144 | key := path.Join(r.baseURL, name, address) 145 | result := "fail" 146 | defer func() { 147 | if err == nil { 148 | result = "success" 149 | } 150 | writer.Write([]byte(result)) 151 | }() 152 | 153 | kv, err = r.kv.Get(key) 154 | if err != nil { 155 | return 156 | } 157 | 158 | v, err := url.ParseQuery(string(kv.Value[:])) 159 | if err != nil { 160 | log.Println("etcd value parse failed. err ", err.Error()) 161 | return 162 | } 163 | v.Set("state", string(client.ConsulServerStateInActive)) 164 | err = r.kv.Put(kv.Key, []byte(v.Encode()), &store.WriteOptions{IsDir: false}) 165 | if err != nil { 166 | log.Println("etcd set failed, err : ", err.Error()) 167 | } 168 | if r.StateCallBack != nil { 169 | r.StateCallBack(name[1:], address, client.ConsulServerStateInActive) 170 | } 171 | return 172 | } 173 | 174 | func (r *ConsulRegistry) PauseService(writer http.ResponseWriter, request *http.Request) { 175 | var name, address string 176 | var kv *store.KVPair 177 | var err error 178 | id := request.PathValue("id") 179 | did, _ := base64.StdEncoding.DecodeString(id) 180 | id = string(did) 181 | name = strings.Split(id, "^")[0] 182 | address = strings.Split(id, "^")[1] 183 | key := path.Join(r.baseURL, name, address) 184 | result := "fail" 185 | defer func() { 186 | if err == nil { 187 | result = "success" 188 | } 189 | writer.Write([]byte(result)) 190 | }() 191 | 192 | kv, err = r.kv.Get(key) 193 | if err != nil { 194 | return 195 | } 196 | 197 | v, err := url.ParseQuery(string(kv.Value[:])) 198 | if err != nil { 199 | log.Println("etcd value parse failed. err ", err.Error()) 200 | return 201 | } 202 | v.Set("state", string(client.ConsulServerStatePause)) 203 | err = r.kv.Put(kv.Key, []byte(v.Encode()), &store.WriteOptions{IsDir: false}) 204 | if err != nil { 205 | log.Println("etcd set failed, err : ", err.Error()) 206 | } 207 | if r.StateCallBack != nil { 208 | r.StateCallBack(name[1:], address, client.ConsulServerStatePause) 209 | } 210 | return 211 | } 212 | 213 | func (r *ConsulRegistry) ActivateService(writer http.ResponseWriter, request *http.Request) { 214 | var name, address string 215 | var kv *store.KVPair 216 | var err error 217 | result := "fail" 218 | id := request.PathValue("id") 219 | did, _ := base64.StdEncoding.DecodeString(id) 220 | id = string(did) 221 | name = strings.Split(id, "^")[0] 222 | address = strings.Split(id, "^")[1] 223 | defer func() { 224 | if err == nil { 225 | result = "success" 226 | } 227 | writer.Write([]byte(result)) 228 | }() 229 | 230 | key := path.Join(r.baseURL, name, address) 231 | kv, err = r.kv.Get(key) 232 | 233 | v, err := url.ParseQuery(string(kv.Value[:])) 234 | if err != nil { 235 | log.Println("etcd value parse failed. err ", err.Error()) 236 | return 237 | } 238 | v.Set("state", string(client.ConsulServerStateActive)) 239 | err = r.kv.Put(kv.Key, []byte(v.Encode()), &store.WriteOptions{IsDir: false}) 240 | if err != nil { 241 | log.Println("etcdv3 put failed. err: ", err.Error()) 242 | } 243 | if r.StateCallBack != nil { 244 | r.StateCallBack(name[1:], address, client.ConsulServerStateActive) 245 | } 246 | return 247 | } 248 | 249 | func (r *ConsulRegistry) updateMetadata(name, address string, metadata string) error { 250 | key := path.Join(r.baseURL, name, address) 251 | err := r.kv.Put(key, []byte(metadata), &store.WriteOptions{IsDir: false}) 252 | return err 253 | } 254 | 255 | func (r *ConsulRegistry) ConsulList(writer http.ResponseWriter, request *http.Request) { 256 | services := r.fetchServices() 257 | jsonData, _ := sonic.Marshal(services) 258 | writer.Write(jsonData) 259 | } 260 | -------------------------------------------------------------------------------- /exp/admin/monitor.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/bytedance/sonic" 5 | "github.com/cornelk/hashmap" 6 | "github.com/thkhxm/tgf" 7 | "net/http" 8 | "sync/atomic" 9 | "time" 10 | ) 11 | 12 | //*************************************************** 13 | //@Link https://github.com/thkhxm/tgf 14 | //@Link https://gitee.com/timgame/tgf 15 | //@QQ群 7400585 16 | //author tim.huang 17 | //@Description 18 | //2024/2/21 19 | //*************************************************** 20 | 21 | const ( 22 | TimeGroup10 = 10 23 | TimeGroup50 = 51 24 | TimeGroup100 = 102 25 | TimeGroup300 = 303 26 | TimeGroupMax = 1 << 30 27 | ) 28 | 29 | const ( 30 | ServiceMonitor = "service_monitor" 31 | ) 32 | 33 | var monitorSecondCache []nodeSecondData 34 | 35 | type nodeSecondData struct { 36 | s string 37 | d *NodeMonitorData 38 | } 39 | 40 | func AddSecondMonitor(all NodeMonitorData) { 41 | t := time.Now().Format("2011-01-01 21:01:00") 42 | 43 | monitorSecondCache = append(monitorSecondCache, nodeSecondData{ 44 | s: t, 45 | d: &all, 46 | }) 47 | if len(monitorSecondCache) > 50000 { 48 | monitorSecondCache = monitorSecondCache[30000:] 49 | } 50 | } 51 | 52 | var monitorCache = hashmap.New[string, []*KeyValueMonitor]() 53 | 54 | func getMonitor(group, key string) (res *KeyValueMonitor) { 55 | if r, ok := monitorCache.Get(group); ok { 56 | for _, monitor := range r { 57 | if monitor.key == key { 58 | return monitor 59 | } 60 | } 61 | r = append(r, &KeyValueMonitor{ 62 | key: key, 63 | total: atomic.Int64{}, 64 | }) 65 | monitorCache.Set(group, r) 66 | } else { 67 | monitorCache.Set(group, make([]*KeyValueMonitor, 0, 100)) 68 | } 69 | return getMonitor(group, key) 70 | } 71 | 72 | func AllMonitor() NodeMonitorData { 73 | sqd := make([]MonitorItem, 0) 74 | res := make([]MonitorData, 0) 75 | monitorCache.Range(func(group string, monitors []*KeyValueMonitor) bool { 76 | for _, monitor := range monitors { 77 | sqd = append(sqd, MonitorItem{ 78 | Key: monitor.key, 79 | Count: monitor.total.Load(), 80 | }) 81 | } 82 | res = append(res, MonitorData{Values: sqd, Group: group}) 83 | return true 84 | }) 85 | return NodeMonitorData{ 86 | NodeId: tgf.NodeId, 87 | Data: res, 88 | } 89 | } 90 | 91 | type KeyValueMonitor struct { 92 | key string 93 | total atomic.Int64 94 | } 95 | 96 | func (s *KeyValueMonitor) Inr() { 97 | s.total.Add(1) 98 | } 99 | 100 | func PointRPCRequest(module, serviceName string) { 101 | if serviceName == "ASyncMonitor" { 102 | return 103 | } 104 | m := getMonitor(ServiceMonitor, module+"."+serviceName) 105 | m.Inr() 106 | } 107 | 108 | type NodeMonitorData struct { 109 | NodeId string `json:"nodeId"` 110 | Data []MonitorData `json:"data"` 111 | } 112 | 113 | type MonitorData struct { 114 | Group string `json:"group"` 115 | Values []MonitorItem `json:"values"` 116 | } 117 | 118 | type MonitorItem struct { 119 | Key string `json:"key"` 120 | Count int64 `json:"count"` 121 | } 122 | 123 | func QueryMonitor(writer http.ResponseWriter, request *http.Request) { 124 | //group := request.PathValue("group") 125 | data := AllMonitor() 126 | jsonData, _ := sonic.Marshal(data) 127 | writer.Write(jsonData) 128 | } 129 | -------------------------------------------------------------------------------- /exp/admin/monitor_test.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | //*************************************************** 8 | //@Link https://github.com/thkhxm/tgf 9 | //@Link https://gitee.com/timgame/tgf 10 | //@QQ群 7400585 11 | //author tim.huang 12 | //@Description 13 | //2024/2/26 14 | //*************************************************** 15 | 16 | func Test_getMonitor(t *testing.T) { 17 | a := getMonitor("a", "b") 18 | t.Log(a) 19 | } 20 | -------------------------------------------------------------------------------- /exp/ast.go: -------------------------------------------------------------------------------- 1 | package exp 2 | 3 | import ( 4 | "go/ast" 5 | "go/parser" 6 | "go/token" 7 | "log" 8 | "strings" 9 | ) 10 | 11 | //*************************************************** 12 | //@Link https://github.com/thkhxm/tgf 13 | //@Link https://gitee.com/timgame/tgf 14 | //@QQ群 7400585 15 | //author tim.huang 16 | //@Description 17 | //2024/1/29 18 | //*************************************************** 19 | 20 | func parseAst() { 21 | // 解析源文件 22 | fset := token.NewFileSet() 23 | node, err := parser.ParseFile(fset, "C:\\Users\\AUSA\\Documents\\IdeaProject\\tgf-example\\common\\service\\dungeon_service.go", nil, parser.ParseComments) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | 28 | // 遍历AST并找到IDungeonService接口的方法签名 29 | ast.Inspect(node, func(n ast.Node) bool { 30 | // 查找接口类型 31 | if t, ok := n.(*ast.TypeSpec); ok { 32 | if t.Name.Name == "IDungeonService" { 33 | if iface, ok := t.Type.(*ast.InterfaceType); ok { 34 | for _, method := range iface.Methods.List { 35 | // 对每个方法构建签名字符串 36 | if f, ok := method.Type.(*ast.FuncType); ok { 37 | var params, results []string 38 | if f.Params != nil { 39 | for _, param := range f.Params.List { 40 | paramType := exprToString(param.Type) 41 | for _, name := range param.Names { 42 | params = append(params, name.Name+" "+paramType) 43 | } 44 | } 45 | } 46 | if f.Results != nil { 47 | for _, result := range f.Results.List { 48 | resultType := exprToString(result.Type) 49 | for _, name := range result.Names { 50 | results = append(results, name.Name+" "+resultType) 51 | } 52 | } 53 | } 54 | signature := method.Names[0].Name + "(" + strings.Join(params, ", ") + ") " + strings.Join(results, ", ") 55 | log.Println(signature) 56 | } 57 | } 58 | } 59 | } 60 | } 61 | return true 62 | }) 63 | } 64 | 65 | func exprToString(expr ast.Expr) string { 66 | switch e := expr.(type) { 67 | case *ast.StarExpr: 68 | return "*" + exprToString(e.X) 69 | case *ast.SelectorExpr: 70 | return exprToString(e.X) + "." + e.Sel.Name 71 | case *ast.Ident: 72 | return e.Name 73 | case *ast.ArrayType: 74 | return "[]" + exprToString(e.Elt) 75 | case *ast.IndexExpr: 76 | // 添加对泛型类型的基本处理 77 | return exprToString(e.X) + "[" + exprToString(e.Index) + "]" 78 | // 可以根据需要继续添加更多的类型转换 79 | default: 80 | return "" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/thkhxm/tgf 2 | 3 | go 1.23.4 4 | 5 | replace ( 6 | github.com/rpcxio/rpcx-consul v0.1.1 => github.com/thkhxm/rpcx-consul v1.0.1 7 | github.com/smallnest/rpcx v1.8.36 => github.com/thkhxm/rpcx v1.0.6 8 | ) 9 | 10 | require ( 11 | github.com/bsm/redislock v0.9.4 12 | github.com/bwmarrin/snowflake v0.3.0 13 | github.com/bytedance/sonic v1.12.7 14 | github.com/cornelk/hashmap v1.0.8 15 | github.com/edwingeng/doublejump v1.0.1 16 | github.com/go-sql-driver/mysql v1.8.1 17 | github.com/gorilla/websocket v1.5.3 18 | github.com/joho/godotenv v1.5.1 19 | github.com/panjf2000/ants/v2 v2.10.0 20 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 21 | github.com/redis/go-redis/v9 v9.7.0 22 | github.com/rpcxio/libkv v0.5.1 23 | github.com/rpcxio/rpcx-consul v0.1.1 24 | github.com/rs/cors v1.11.1 25 | github.com/smallnest/rpcx v1.8.36 26 | github.com/valyala/bytebufferpool v1.0.0 27 | github.com/xuri/excelize/v2 v2.9.0 28 | go.uber.org/zap v1.27.0 29 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 30 | golang.org/x/net v0.34.0 31 | golang.org/x/sync v0.10.0 32 | golang.org/x/text v0.21.0 33 | google.golang.org/protobuf v1.36.2 34 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 35 | ) 36 | 37 | require ( 38 | filippo.io/edwards25519 v1.1.0 // indirect 39 | github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect 40 | github.com/akutz/memconn v0.1.0 // indirect 41 | github.com/alitto/pond v1.9.2 // indirect 42 | github.com/apache/thrift v0.21.0 // indirect 43 | github.com/armon/go-metrics v0.3.6 // indirect 44 | github.com/bytedance/sonic/loader v0.2.2 // indirect 45 | github.com/cenk/backoff v2.2.1+incompatible // indirect 46 | github.com/cenkalti/backoff v2.2.1+incompatible // indirect 47 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 48 | github.com/cloudwego/base64x v0.1.4 // indirect 49 | github.com/cloudwego/iasm v0.2.0 // indirect 50 | github.com/dgryski/go-jump v0.0.0-20170409065014-e1f439676b57 // indirect 51 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 52 | github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect 53 | github.com/fatih/color v1.18.0 // indirect 54 | github.com/go-echarts/go-echarts/v2 v2.4.6 // indirect 55 | github.com/go-ole/go-ole v1.2.4 // indirect 56 | github.com/go-ping/ping v1.2.0 // indirect 57 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 58 | github.com/godzie44/go-uring v0.0.0-20220926161041-69611e8b13d5 // indirect 59 | github.com/gogo/protobuf v1.3.2 // indirect 60 | github.com/golang/snappy v0.0.4 // indirect 61 | github.com/google/pprof v0.0.0-20240430035430-e4905b036c4e // indirect 62 | github.com/google/uuid v1.2.0 // indirect 63 | github.com/grandcat/zeroconf v1.0.0 // indirect 64 | github.com/hashicorp/consul/api v1.8.1 // indirect 65 | github.com/hashicorp/errwrap v1.0.0 // indirect 66 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 67 | github.com/hashicorp/go-hclog v0.16.0 // indirect 68 | github.com/hashicorp/go-immutable-radix v1.3.0 // indirect 69 | github.com/hashicorp/go-multierror v1.1.1 // indirect 70 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 71 | github.com/hashicorp/golang-lru v1.0.2 // indirect 72 | github.com/hashicorp/serf v0.9.5 // indirect 73 | github.com/jamiealquiza/tachymeter v2.0.0+incompatible // indirect 74 | github.com/juju/ratelimit v1.0.2 // indirect 75 | github.com/julienschmidt/httprouter v1.3.0 // indirect 76 | github.com/kavu/go_reuseport v1.5.0 // indirect 77 | github.com/klauspost/cpuid/v2 v2.2.9 // indirect 78 | github.com/klauspost/reedsolomon v1.12.4 // indirect 79 | github.com/libp2p/go-sockaddr v0.1.1 // indirect 80 | github.com/mattn/go-colorable v0.1.13 // indirect 81 | github.com/mattn/go-isatty v0.0.20 // indirect 82 | github.com/miekg/dns v1.1.27 // indirect 83 | github.com/mitchellh/go-homedir v1.1.0 // indirect 84 | github.com/mitchellh/mapstructure v1.4.1 // indirect 85 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 86 | github.com/onsi/ginkgo/v2 v2.17.2 // indirect 87 | github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect 88 | github.com/pkg/errors v0.9.1 // indirect 89 | github.com/quic-go/quic-go v0.48.2 // indirect 90 | github.com/richardlehane/mscfb v1.0.4 // indirect 91 | github.com/richardlehane/msoleps v1.0.4 // indirect 92 | github.com/rubyist/circuitbreaker v2.2.1+incompatible // indirect 93 | github.com/shirou/gopsutil/v3 v3.20.10 // indirect 94 | github.com/smallnest/quick v0.2.0 // indirect 95 | github.com/smallnest/statsview v1.0.1 // indirect 96 | github.com/soheilhy/cmux v0.1.5 // indirect 97 | github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect 98 | github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect 99 | github.com/tinylib/msgp v1.2.5 // indirect 100 | github.com/tjfoc/gmsm v1.4.1 // indirect 101 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 102 | github.com/valyala/fastrand v1.1.0 // indirect 103 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 104 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 105 | github.com/xtaci/kcp-go v5.4.20+incompatible // indirect 106 | github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 // indirect 107 | github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71 // indirect 108 | go.uber.org/mock v0.4.0 // indirect 109 | go.uber.org/multierr v1.10.0 // indirect 110 | golang.org/x/arch v0.13.0 // indirect 111 | golang.org/x/crypto v0.32.0 // indirect 112 | golang.org/x/mod v0.22.0 // indirect 113 | golang.org/x/sys v0.29.0 // indirect 114 | golang.org/x/tools v0.29.0 // indirect 115 | ) 116 | -------------------------------------------------------------------------------- /init.go: -------------------------------------------------------------------------------- 1 | // Package tgf 2 | // @Description: 框架基础包 3 | package tgf 4 | 5 | import ( 6 | "github.com/thkhxm/tgf/util" 7 | "os" 8 | "os/signal" 9 | ) 10 | 11 | // *************************************************** 12 | // @Link https://github.com/thkhxm/tgf 13 | // @Link https://gitee.com/timgame/tgf 14 | // @QQ群 7400585 15 | // author tim.huang 16 | // @Description init 17 | // 2023/2/22 18 | // *************************************************** 19 | 20 | var destroyList []IDestroyHandler 21 | var closeChan chan bool 22 | 23 | type IDestroyHandler interface { 24 | Destroy() 25 | } 26 | 27 | func AddDestroyHandler(handler IDestroyHandler) { 28 | destroyList = append(destroyList, handler) 29 | } 30 | 31 | func CloseChan() <-chan bool { 32 | return closeChan 33 | } 34 | 35 | func init() { 36 | InitConfig() 37 | destroyList = make([]IDestroyHandler, 0) 38 | closeChan = make(chan bool, 1) 39 | 40 | util.Go(func() { 41 | c := make(chan os.Signal, 1) 42 | signal.Notify(c, os.Interrupt, os.Kill) 43 | <-c 44 | for _, handler := range destroyList { 45 | handler.Destroy() 46 | } 47 | closeChan <- true 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /log/init.go: -------------------------------------------------------------------------------- 1 | // Package log 2 | // @Description: log包,日志相关业务 3 | // @Link https://github.com/natefinch/lumberjack 4 | // @Link https://github.com/uber-go/zap 5 | // 6 | // @Ref https://www.cnblogs.com/stulzq/p/16741844.html 7 | package log 8 | 9 | import "github.com/thkhxm/tgf" 10 | 11 | //*************************************************** 12 | //@Link https://github.com/thkhxm/tgf 13 | //@Link https://gitee.com/timgame/tgf 14 | //@QQ群 7400585 15 | //author tim.huang 16 | //@Description init 17 | //2023/2/22 18 | //*************************************************** 19 | 20 | func init() { 21 | initLogger() 22 | print(tgf.Logo) 23 | } 24 | -------------------------------------------------------------------------------- /log/logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "github.com/thkhxm/tgf" 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zapcore" 8 | "gopkg.in/natefinch/lumberjack.v2" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | //*************************************************** 16 | //@Link https://github.com/thkhxm/tgf 17 | //@Link https://gitee.com/timgame/tgf 18 | //@QQ群 7400585 19 | //author tim.huang 20 | //@Description 21 | //2023/2/22 22 | //*************************************************** 23 | 24 | var logger *zap.Logger 25 | var slogger *zap.SugaredLogger 26 | 27 | var ( 28 | defaultMaxSize = 512 29 | defaultMaxAge = 0 30 | defaultMaxBackups = 100 31 | ignoredTags map[string]bool 32 | ) 33 | 34 | const ( 35 | GAMETAG = "game" 36 | DBTAG = "db" 37 | SERVICETAG = "service" 38 | ) 39 | 40 | func Info(msg string, params ...interface{}) { 41 | logger.Info(fmt.Sprintf(msg, params...)) 42 | } 43 | 44 | func InfoTag(tag string, msg string, params ...interface{}) { 45 | if CheckLogTag(tag) { 46 | logger.Info(fmt.Sprintf(msg, params...), zap.String("tag", tag)) 47 | } 48 | } 49 | 50 | func SLogger() *zap.SugaredLogger { 51 | return slogger 52 | } 53 | 54 | func Game(userId, tag, msg string, params ...interface{}) { 55 | //if CheckLogTag(GAMETAG) { 56 | logger.Info(fmt.Sprintf(msg, params...), zap.String("tag", tag), zap.String("userId", userId)) 57 | //} 58 | } 59 | 60 | func DB(traceId, dbName, script string, count int32) { 61 | //if CheckLogTag(DBTAG) { 62 | logger.Debug(script, zap.String("tag", DBTAG), zap.String("nodeId", tgf.NodeId), zap.String("db", dbName), zap.Int32("count", count), zap.String("traceId", traceId)) 63 | //} 64 | } 65 | 66 | func Service(module, name, version, userId string, consume int64, code int32) { 67 | //if CheckLogTag(SERVICETAG) { 68 | logger.Debug("", zap.String("tag", SERVICETAG), 69 | zap.String("userId", userId), 70 | zap.String("module", module), 71 | zap.String("name", name), 72 | zap.String("version", version), 73 | zap.Int64("consume", consume), 74 | zap.Int32("code", code), 75 | ) 76 | //} 77 | } 78 | 79 | func Debug(msg string, params ...interface{}) { 80 | logger.Debug(fmt.Sprintf(msg, params...)) 81 | } 82 | 83 | func DebugTag(tag string, msg string, params ...interface{}) { 84 | if CheckLogTag(tag) { 85 | logger.Debug(fmt.Sprintf(msg, params...), zap.String("tag", tag)) 86 | } 87 | } 88 | 89 | func Error(msg string, params ...interface{}) { 90 | logger.Error(fmt.Sprintf(msg, params...)) 91 | } 92 | 93 | func ErrorTag(tag string, msg string, params ...interface{}) { 94 | if CheckLogTag(tag) { 95 | logger.Error(fmt.Sprintf(msg, params...), zap.String("tag", tag)) 96 | } 97 | } 98 | 99 | func Warn(msg string, params ...interface{}) { 100 | logger.Warn(fmt.Sprintf(msg, params...)) 101 | } 102 | 103 | func WarnTag(tag string, msg string, params ...interface{}) { 104 | if CheckLogTag(tag) { 105 | logger.Warn(fmt.Sprintf(msg, params...), zap.String("tag", tag)) 106 | } 107 | } 108 | 109 | func CheckLogTag(tag string) bool { 110 | return !ignoredTags[tag] 111 | } 112 | 113 | func initLogger() { 114 | var ( 115 | /*自定义时间格式*/ 116 | customTimeEncoder = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { 117 | enc.AppendString(t.Format("2006-01-02 15:04:05.000")) 118 | } 119 | /*自定义日志级别显示*/ 120 | customLevelEncoder = func(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { 121 | enc.AppendString(level.CapitalString()) 122 | } 123 | /*自定义代码路径、行号输出*/ 124 | customCallerEncoder = func(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) { 125 | enc.AppendString("[" + caller.TrimmedPath() + "]") 126 | } 127 | logLevel = tgf.GetStrConfig[string](tgf.EnvironmentLoggerLevel) 128 | logPath = tgf.GetStrConfig[string](tgf.EnvironmentLoggerPath) 129 | ) 130 | 131 | zapLoggerEncoderConfig := zapcore.EncoderConfig{ 132 | TimeKey: "time", 133 | LevelKey: "level", 134 | NameKey: "logger", 135 | CallerKey: "caller", 136 | MessageKey: "message", 137 | StacktraceKey: "stacktrace", 138 | EncodeCaller: customCallerEncoder, 139 | EncodeTime: customTimeEncoder, 140 | EncodeLevel: customLevelEncoder, 141 | EncodeDuration: zapcore.SecondsDurationEncoder, 142 | LineEnding: "\n", 143 | ConsoleSeparator: " ", 144 | } 145 | 146 | //Dev环境,日志级别使用带颜色的标识 147 | if tgf.GetStrConfig[string](tgf.EnvironmentRuntimeModule) == tgf.RuntimeModuleDev { 148 | zapLoggerEncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder 149 | } 150 | 151 | ignoredTags = make(map[string]bool) 152 | it := tgf.GetStrConfig[string](tgf.EnvironmentLoggerIgnoredTags) 153 | if it != "" { 154 | tags := strings.Split(it, ",") 155 | for _, tag := range tags { 156 | ignoredTags[tag] = true 157 | } 158 | } 159 | 160 | //syncWriter = zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout)) 161 | 162 | //syncWriter := zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(&lumberjack.Logger{ 163 | // Filename: logPath, // ⽇志⽂件路径 164 | // MaxSize: defaultMaxSize, // 单位为MB,默认为512MB 165 | // MaxAge: defaultMaxAge, // 文件最多保存多少天 166 | // LocalTime: true, // 采用本地时间 167 | // Compress: false, // 是否压缩日志 168 | //})) 169 | 170 | //syncWriter = &zapcore.BufferedWriteSyncer{ 171 | // WS: zapcore.AddSync(&lumberjack.Logger{ 172 | // Filename: "logs/app/app.log", // ⽇志⽂件路径 173 | // MaxSize: 100, // 单位为MB,默认为512MB 174 | // MaxAge: 5, // 文件最多保存多少天 175 | // LocalTime: true, // 采用本地时间 176 | // Compress: false, // 是否压缩日志 177 | // }), 178 | // Size: 4096, 179 | //} 180 | //在原有日志基础上增加一层 181 | level, _ := zapcore.ParseLevel(logLevel) 182 | // 183 | st := newCore(logPath, level, zapLoggerEncoderConfig, true) 184 | zapCoreGame := newCore(logPath, level, zapLoggerEncoderConfig, false) 185 | basePath := filepath.Dir(logPath) 186 | zapCoreService := newCore(fmt.Sprintf("%s%sservice%sservice.log", basePath, string(filepath.Separator), string(filepath.Separator)), level, zapLoggerEncoderConfig, false) 187 | zapCoreDB := newCore(fmt.Sprintf("%s%sdb%sdb.log", basePath, string(filepath.Separator), string(filepath.Separator)), level, zapLoggerEncoderConfig, false) 188 | //zapCore := zapcore.NewCore(zapcore.NewConsoleEncoder(zapLoggerEncoderConfig), syncWriter, level) 189 | // 创建一个映射,将标签映射到对应的Core 190 | taggedCores := map[string]zapcore.Core{ 191 | DBTAG: &TaggedCore{Core: zapCoreDB, Tag: DBTAG, Pass: true}, 192 | GAMETAG: &TaggedCore{Core: zapCoreGame, Tag: ""}, 193 | SERVICETAG: &TaggedCore{Core: zapCoreService, Tag: SERVICETAG, Pass: true}, 194 | } 195 | logger = zap.New(zapcore.NewTee(taggedCores[DBTAG], taggedCores[GAMETAG], taggedCores[SERVICETAG], st), zap.AddCaller(), zap.AddCallerSkip(1)) 196 | slogger = logger.Sugar() 197 | defer logger.Sync() 198 | InfoTag("init", "日志初始化完成日志文件:%s 日志级别:%v", logPath, logLevel) 199 | } 200 | 201 | // 为每个日志类型(game, system, all)创建一个专门的Core实例 202 | func newCore(logPath string, level zapcore.Level, zapLoggerEncoderConfig zapcore.EncoderConfig, stdout bool) zapcore.Core { 203 | //如果logPath文件夹不存在则创建 204 | if _, err := os.Stat(filepath.Dir(logPath)); os.IsNotExist(err) { 205 | os.MkdirAll(filepath.Dir(logPath), os.ModePerm) 206 | } 207 | 208 | wys := make([]zapcore.WriteSyncer, 0, 2) 209 | if stdout { 210 | wys = append(wys, zapcore.AddSync(os.Stdout)) 211 | syncWriter := zapcore.NewMultiWriteSyncer(wys...) 212 | return zapcore.NewCore(zapcore.NewConsoleEncoder(zapLoggerEncoderConfig), syncWriter, level) 213 | } 214 | wy := zapcore.AddSync(&lumberjack.Logger{ 215 | Filename: logPath, // ⽇志⽂件路径 216 | MaxBackups: defaultMaxBackups, // 单位为MB,默认为512MB 217 | MaxSize: defaultMaxSize, // 单位为MB,默认为512MB 218 | MaxAge: defaultMaxAge, // 文件最多保存多少天 219 | LocalTime: true, // 采用本地时间 220 | Compress: false, // 是否压缩日志 221 | }) 222 | wys = append(wys, wy) 223 | syncWriter := zapcore.NewMultiWriteSyncer(wys...) 224 | return zapcore.NewCore(zapcore.NewJSONEncoder(zapLoggerEncoderConfig), syncWriter, level) 225 | } 226 | 227 | type TaggedCore struct { 228 | Core zapcore.Core 229 | Tag string 230 | AllowedTags map[string]zapcore.Core 231 | Pass bool 232 | } 233 | 234 | func (t *TaggedCore) Enabled(lvl zapcore.Level) bool { 235 | return t.Core.Enabled(lvl) 236 | } 237 | 238 | func (t *TaggedCore) With(fields []zapcore.Field) zapcore.Core { 239 | return &TaggedCore{ 240 | Core: t.Core.With(fields), 241 | Tag: t.Tag, 242 | AllowedTags: t.AllowedTags, 243 | } 244 | } 245 | 246 | func (t *TaggedCore) Check(entry zapcore.Entry, checkedEntry *zapcore.CheckedEntry) *zapcore.CheckedEntry { 247 | if t.Pass || t.Core.Enabled(entry.Level) { 248 | return checkedEntry.AddCore(entry, t) 249 | } 250 | return nil 251 | } 252 | 253 | func (t *TaggedCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { 254 | // 在这里可以根据标签过滤逻辑处理日志条目 255 | // 例如,你可以在fields中查找特定的标签字段,并根据这个标签决定是否调用t.Core.Write 256 | if t.Tag == "" { 257 | return t.Core.Write(entry, fields) 258 | } 259 | 260 | for _, field := range fields { 261 | if field.Key == "tag" && field.String == t.Tag { 262 | return t.Core.Write(entry, fields) 263 | } 264 | } 265 | return nil 266 | } 267 | 268 | func (t *TaggedCore) Sync() error { 269 | return t.Core.Sync() 270 | } 271 | -------------------------------------------------------------------------------- /log/logger_test.go: -------------------------------------------------------------------------------- 1 | package log_test 2 | 3 | import ( 4 | "github.com/thkhxm/tgf/log" 5 | "testing" 6 | ) 7 | 8 | //*************************************************** 9 | //@Link https://github.com/thkhxm/tgf 10 | //@Link https://gitee.com/timgame/tgf 11 | //@QQ群 7400585 12 | //author tim.huang 13 | //@Description 14 | //2023/2/22 15 | //*************************************************** 16 | 17 | func TestDebug(t *testing.T) { 18 | type args struct { 19 | msg string 20 | params []interface{} 21 | } 22 | tests := []struct { 23 | name string 24 | args args 25 | }{ 26 | // TODO: Add test cases. 27 | {"1", args{"log test 1 %v %v", []interface{}{"a", "b"}}}, 28 | } 29 | for _, tt := range tests { 30 | t.Run(tt.name, func(t *testing.T) { 31 | log.Debug(tt.args.msg, tt.args.params...) 32 | }) 33 | } 34 | } 35 | 36 | func TestError(t *testing.T) { 37 | type args struct { 38 | msg string 39 | params []interface{} 40 | } 41 | tests := []struct { 42 | name string 43 | args args 44 | }{ 45 | // TODO: Add test cases. 46 | {"1", args{"log test 1 %v %v", []interface{}{"a", "b"}}}, 47 | } 48 | for _, tt := range tests { 49 | t.Run(tt.name, func(t *testing.T) { 50 | log.Error(tt.args.msg, tt.args.params...) 51 | }) 52 | } 53 | } 54 | 55 | func TestInfo(t *testing.T) { 56 | type args struct { 57 | msg string 58 | params []interface{} 59 | } 60 | tests := []struct { 61 | name string 62 | args args 63 | }{ 64 | // TODO: Add test cases. 65 | {"1", args{"log test 1 %v %v", []interface{}{"a", "b"}}}, 66 | } 67 | for _, tt := range tests { 68 | t.Run(tt.name, func(t *testing.T) { 69 | log.Info(tt.args.msg, tt.args.params...) 70 | }) 71 | } 72 | } 73 | 74 | func TestWarn(t *testing.T) { 75 | type args struct { 76 | msg string 77 | params []interface{} 78 | } 79 | tests := []struct { 80 | name string 81 | args args 82 | }{ 83 | // TODO: Add test cases. 84 | {"1", args{"log test 1 %v %v", []interface{}{"a", "b"}}}, 85 | } 86 | for _, tt := range tests { 87 | t.Run(tt.name, func(t *testing.T) { 88 | log.Warn(tt.args.msg, tt.args.params...) 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /log/trace.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | //*************************************************** 4 | //@Link https://github.com/thkhxm/tgf 5 | //@Link https://gitee.com/timgame/tgf 6 | //@QQ群 7400585 7 | //author tim.huang 8 | //@Description 9 | //日志收集(直接发送、记录到本地再上传)、 10 | //日志接收(消息队列,直接进入ElasticSearch)、 11 | //数据清洗(Logstach、Storm、SparkStreaming)、 12 | //日志存储(Mysql、Hbase、ElasticSearch)、 13 | //页面展示(自研还是直接用第三方的) 14 | //2024/2/21 15 | //*************************************************** 16 | 17 | type Trace struct { 18 | } 19 | 20 | // SystemSpan 系统级别的span 21 | type SystemSpan struct { 22 | Span 23 | } 24 | 25 | // GameSpan 游戏级别的span 26 | type GameSpan struct { 27 | Span 28 | Tag string 29 | } 30 | 31 | // UserSpan 用户级别的span 32 | type UserSpan struct { 33 | Span 34 | UserId string 35 | } 36 | 37 | type Span struct { 38 | TraceId string // 跟踪id 39 | SourceNode string // 发起节点 40 | TargetNode string // 目标节点 41 | StartTime int64 // 开始时间 42 | } 43 | -------------------------------------------------------------------------------- /robot/client.go: -------------------------------------------------------------------------------- 1 | package robot 2 | 3 | import ( 4 | "google.golang.org/protobuf/proto" 5 | ) 6 | 7 | //*************************************************** 8 | //@Link https://github.com/thkhxm/tgf 9 | //@Link https://gitee.com/timgame/tgf 10 | //@QQ群 7400585 11 | //author tim.huang 12 | //@Description 13 | //2023/4/26 14 | //*************************************************** 15 | 16 | type CallbackLogic func(IRobot, []byte) 17 | 18 | type IRobot interface { 19 | Connect(address string) IRobot 20 | RegisterCallbackMessage(messageType string, f CallbackLogic) IRobot 21 | Send(messageType string, v1 proto.Message) 22 | SendMessage(module, serviceName string, v1 proto.Message) 23 | } 24 | -------------------------------------------------------------------------------- /robot/tcp_client.go: -------------------------------------------------------------------------------- 1 | package robot 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "github.com/cornelk/hashmap" 8 | "github.com/gorilla/websocket" 9 | util2 "github.com/smallnest/rpcx/util" 10 | "github.com/thkhxm/tgf/log" 11 | "github.com/thkhxm/tgf/rpc" 12 | "github.com/thkhxm/tgf/util" 13 | "google.golang.org/protobuf/proto" 14 | "net" 15 | "net/url" 16 | "strings" 17 | "sync/atomic" 18 | "time" 19 | ) 20 | 21 | //*************************************************** 22 | //@Link https://github.com/thkhxm/tgf 23 | //@Link https://gitee.com/timgame/tgf 24 | //@QQ群 7400585 25 | //author tim.huang 26 | //@Description 27 | //2023/4/26 28 | //*************************************************** 29 | 30 | type tcp struct { 31 | callback *hashmap.Map[string, CallbackLogic] 32 | buf *bufio.Reader 33 | client *net.TCPConn 34 | } 35 | 36 | func (t *tcp) Connect(address string) IRobot { 37 | add, err := net.ResolveTCPAddr("tcp", address) 38 | t.client, err = net.DialTCP("tcp", nil, add) 39 | if err != nil { 40 | log.InfoTag("robot", "client error: %v", err) 41 | panic(err) 42 | } 43 | t.buf = bufio.NewReader(t.client) 44 | //心跳 45 | util.Go(func() { 46 | for { 47 | heartbeat := make([]byte, 0, 2) 48 | buff := bytes.NewBuffer(heartbeat) 49 | buff.WriteByte(250) 50 | buff.WriteByte(byte(rpc.Heartbeat)) 51 | t.client.Write(buff.Bytes()) 52 | log.InfoTag("robot", "client heartbeat data: %v", buff.Bytes()) 53 | time.Sleep(time.Second * 10) 54 | } 55 | }) 56 | 57 | //handler response 58 | util.Go(func() { 59 | for { 60 | // [1][1][2][4][n][n] 61 | // message type|compress|request method name size|data size|method name|data 62 | head, e := t.buf.Peek(1) 63 | if e != nil { 64 | log.InfoTag("robot", "client response data: %v", e) 65 | return 66 | } 67 | mt := head[0] 68 | //心跳响应,跳过这个包 69 | if mt == byte(rpc.Heartbeat) { 70 | t.buf.Discard(1) 71 | log.InfoTag("robot", "收到服务器响应的心跳包") 72 | continue 73 | } 74 | //非心跳包,先捕获头 75 | head, e = t.buf.Peek(8) 76 | if e != nil { 77 | log.InfoTag("robot", "client response data: %v", e) 78 | panic(e) 79 | } 80 | compress := head[1] 81 | requestSize := binary.BigEndian.Uint16(head[2:4]) 82 | dataSize := binary.BigEndian.Uint32(head[4:8]) 83 | allSize := 8 + uint32(requestSize) + dataSize 84 | //数据没接收完整 85 | if t.buf.Buffered() < int(allSize) { 86 | continue 87 | } 88 | data := make([]byte, allSize) 89 | n, e := t.buf.Read(data) 90 | if e != nil || n != int(allSize) { 91 | log.InfoTag("robot", "client read data : %v", e) 92 | } 93 | if compress == 1 { 94 | data, e = util2.Unzip(data) 95 | if e != nil { 96 | log.InfoTag("robot", "client data compress : %v", e) 97 | } 98 | } 99 | message := util.ConvertStringByByteSlice(data[8 : 8+requestSize]) 100 | res := util.ConvertStringByByteSlice(data[8+requestSize:]) 101 | if f, has := t.callback.Get(message); has { 102 | f(t, data[8+requestSize:]) 103 | } 104 | log.InfoTag("robot", "收到服务器的响应数据 messageType:%v 数据:%v", message, res) 105 | } 106 | }) 107 | // 108 | return t 109 | } 110 | 111 | func (t *tcp) RegisterCallbackMessage(messageType string, f CallbackLogic) IRobot { 112 | t.callback.Insert(messageType, f) 113 | return t 114 | } 115 | 116 | func (t *tcp) Send(messageType string, v1 proto.Message) { 117 | data, _ := proto.Marshal(v1) 118 | reqName := []byte(messageType) 119 | tmp := make([]byte, 0, 6+len(data)+len(reqName)) 120 | buff := bytes.NewBuffer(tmp) 121 | buff.WriteByte(250) 122 | buff.WriteByte(byte(rpc.Logic)) 123 | reqLenByte := make([]byte, 2) 124 | binary.BigEndian.PutUint16(reqLenByte, uint16(len(reqName))) 125 | buff.Write(reqLenByte) 126 | reqSizeLenByte := make([]byte, 2) 127 | binary.BigEndian.PutUint16(reqSizeLenByte, uint16(len(data))) 128 | buff.Write(reqSizeLenByte) 129 | buff.Write(reqName) 130 | buff.Write(data) 131 | t.client.Write(buff.Bytes()) 132 | log.InfoTag("robot", "发送请求 messageType:%v 数据:%v", messageType, buff.Bytes()) 133 | } 134 | 135 | func (t *tcp) SendMessage(module, serviceName string, v1 proto.Message) { 136 | ms := module + "." + serviceName 137 | t.Send(ms, v1) 138 | } 139 | 140 | type ws struct { 141 | path string 142 | conn *websocket.Conn 143 | heartbeatData []byte 144 | closeChan chan struct{} 145 | sendChan chan message 146 | callback *hashmap.Map[string, CallbackLogic] 147 | start time.Time 148 | } 149 | type wss struct { 150 | ws 151 | } 152 | type message struct { 153 | messageType int 154 | data []byte 155 | } 156 | 157 | var costCache = make([]*atomic.Int64, 6) 158 | var lastReq, curReq = &atomic.Int64{}, &atomic.Int64{} 159 | 160 | func (w *ws) Connect(address string) IRobot { 161 | u := url.URL{Scheme: "ws", Host: address, Path: w.path} 162 | //log.Info("连接到 %s", u.String()) 163 | 164 | conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil) 165 | if err != nil { 166 | log.Info("连接失败:%v", err) 167 | } 168 | w.heartbeatData = []byte{byte(1)} 169 | w.closeChan = make(chan struct{}) 170 | w.sendChan = make(chan message, 100) 171 | w.conn = conn 172 | //监听关闭事件 173 | w.conn.SetCloseHandler(func(code int, text string) error { 174 | w.closeChan <- struct{}{} 175 | return nil 176 | }) 177 | 178 | w.conn.SetPongHandler(func(appData string) error { 179 | // 收到服务器的pong响应 180 | //log.DebugTag("tcp", "收到服务器的pong响应 data=%v", appData) 181 | return nil 182 | }) 183 | 184 | // 启动读取协程,处理从服务器接收到的消息 185 | util.Go(func() { 186 | defer w.conn.Close() 187 | for { 188 | _, message, err := conn.ReadMessage() 189 | if err != nil { 190 | log.Info("读取消息失败:%v", err) 191 | return 192 | } 193 | cost := time.Since(w.start).Milliseconds() 194 | index := getCostIndex(cost) 195 | costCache[index].Add(1) 196 | curReq.Add(1) 197 | res := &rpc.WSResponse{} 198 | proto.Unmarshal(message, res) 199 | //log.Info("收到消息: %s", res.MessageType) 200 | if f, has := w.callback.Get(res.MessageType); has { 201 | data := res.GetData() 202 | if res.Zip { 203 | if data, err = util2.Unzip(res.GetData()); err != nil { 204 | log.Info("解压数据失败:%v", err) 205 | } 206 | } 207 | f(w, data) 208 | } 209 | } 210 | }) 211 | 212 | //启动心跳 213 | util.Go(func() { 214 | for { 215 | select { 216 | //监听关闭信号 217 | case <-w.closeChan: 218 | log.InfoTag("tcp", "连接断开,停止心跳发送") 219 | return 220 | default: 221 | w.sendChan <- message{websocket.PingMessage, w.heartbeatData} 222 | //log.InfoTag("tcp", "心跳发送") 223 | } 224 | time.Sleep(time.Second * 5) 225 | } 226 | }) 227 | 228 | util.Go(func() { 229 | for { 230 | select { 231 | case send := <-w.sendChan: 232 | w.conn.WriteMessage(send.messageType, send.data) 233 | } 234 | } 235 | }) 236 | return w 237 | } 238 | func (w *wss) Connect(address string) IRobot { 239 | u := url.URL{Scheme: "wss", Host: address, Path: w.path} 240 | log.Info("连接到 %s", u.String()) 241 | 242 | conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil) 243 | if err != nil { 244 | log.Info("连接失败:%v", err) 245 | } 246 | w.heartbeatData = []byte{byte(1)} 247 | w.closeChan = make(chan struct{}) 248 | w.sendChan = make(chan message, 100) 249 | w.conn = conn 250 | //监听关闭事件 251 | w.conn.SetCloseHandler(func(code int, text string) error { 252 | w.closeChan <- struct{}{} 253 | return nil 254 | }) 255 | 256 | w.conn.SetPongHandler(func(appData string) error { 257 | // 收到服务器的pong响应 258 | //log.DebugTag("tcp", "收到服务器的pong响应 data=%v", appData) 259 | return nil 260 | }) 261 | 262 | // 启动读取协程,处理从服务器接收到的消息 263 | util.Go(func() { 264 | defer w.conn.Close() 265 | for { 266 | _, message, err := conn.ReadMessage() 267 | cost := time.Since(w.start).Milliseconds() 268 | if cost > 500 { 269 | log.Info("消息处理时间:%v", cost) 270 | } 271 | if err != nil { 272 | log.Info("读取消息失败:%v", err) 273 | return 274 | } 275 | res := &rpc.WSResponse{} 276 | proto.Unmarshal(message, res) 277 | //log.Info("收到消息: %s", res.MessageType) 278 | if f, has := w.callback.Get(res.MessageType); has { 279 | data := res.GetData() 280 | if res.Zip { 281 | if data, err = util2.Unzip(res.GetData()); err != nil { 282 | log.Info("解压数据失败:%v", err) 283 | } 284 | } 285 | f(w, data) 286 | } 287 | } 288 | }) 289 | 290 | //启动心跳 291 | util.Go(func() { 292 | for { 293 | select { 294 | //监听关闭信号 295 | case <-w.closeChan: 296 | log.InfoTag("tcp", "连接断开,停止心跳发送") 297 | return 298 | default: 299 | w.sendChan <- message{websocket.PingMessage, w.heartbeatData} 300 | //log.InfoTag("tcp", "心跳发送") 301 | } 302 | time.Sleep(time.Second * 5) 303 | } 304 | }) 305 | 306 | util.Go(func() { 307 | for { 308 | select { 309 | case send := <-w.sendChan: 310 | w.conn.WriteMessage(send.messageType, send.data) 311 | } 312 | } 313 | }) 314 | return w 315 | } 316 | 317 | func (w *ws) RegisterCallbackMessage(messageType string, f CallbackLogic) IRobot { 318 | w.callback.Insert(messageType, f) 319 | return w 320 | } 321 | 322 | func (w *ws) Send(messageType string, v1 proto.Message) { 323 | ms := strings.Split(messageType, ".") 324 | //time.Sleep(time.Millisecond * 300) 325 | w.SendMessage(ms[0], ms[1], v1) 326 | w.start = time.Now() 327 | } 328 | 329 | func (w *ws) SendMessage(module, serviceName string, v1 proto.Message) { 330 | data, _ := proto.Marshal(v1) 331 | m := &rpc.WSMessage{ 332 | Module: module, 333 | ServiceName: serviceName, 334 | Data: data, 335 | } 336 | md, _ := proto.Marshal(m) 337 | w.sendChan <- message{websocket.BinaryMessage, md} 338 | //err := w.conn.WriteMessage(websocket.BinaryMessage, md) 339 | //if err != nil { 340 | // log.Info("发送消息失败:%v", err) 341 | // return 342 | //} 343 | } 344 | 345 | func NewRobotTcp() IRobot { 346 | t := &tcp{} 347 | t.callback = hashmap.New[string, CallbackLogic]() 348 | return t 349 | } 350 | 351 | func NewRobotWs(path string) IRobot { 352 | t := &ws{} 353 | if path[0:1] != "/" { 354 | path = "/" + path 355 | } 356 | t.path = path 357 | t.callback = hashmap.New[string, CallbackLogic]() 358 | return t 359 | } 360 | 361 | func NewRobotWss(path string) IRobot { 362 | t := &wss{} 363 | if path[0:1] != "/" { 364 | path = "/" + path 365 | } 366 | t.path = path 367 | t.callback = hashmap.New[string, CallbackLogic]() 368 | return t 369 | } 370 | 371 | func init() { 372 | costCache[0] = &atomic.Int64{} 373 | costCache[1] = &atomic.Int64{} 374 | costCache[2] = &atomic.Int64{} 375 | costCache[3] = &atomic.Int64{} 376 | costCache[4] = &atomic.Int64{} 377 | costCache[5] = &atomic.Int64{} 378 | go func() { 379 | t := time.NewTimer(time.Second) 380 | for { 381 | select { 382 | case <-t.C: 383 | qps := curReq.Load() - lastReq.Load() 384 | lastReq.Store(curReq.Load()) 385 | log.InfoTag("tcp", "消息处理时间统计:qps:%v, 0-50ms:%v, 50-100ms:%v, 100-300ms:%v, 300-600ms:%v, 600-1000ms:%v, >1000ms:%v", 386 | qps, costCache[0].Load(), costCache[1].Load(), costCache[2].Load(), costCache[3].Load(), costCache[4].Load(), costCache[5].Load()) 387 | } 388 | t.Reset(time.Second) 389 | } 390 | }() 391 | } 392 | 393 | func getCostIndex(mill int64) int32 { 394 | if mill < 50 { 395 | return 0 396 | } else if mill < 100 { 397 | return 1 398 | } else if mill < 300 { 399 | return 2 400 | } else if mill < 600 { 401 | return 3 402 | } else if mill < 1000 { 403 | return 4 404 | } else { 405 | return 5 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /robot/tcp_client_test.go: -------------------------------------------------------------------------------- 1 | package robot_test 2 | 3 | import ( 4 | "github.com/thkhxm/tgf/robot" 5 | "testing" 6 | ) 7 | 8 | //*************************************************** 9 | //@Link https://github.com/thkhxm/tgf 10 | //@Link https://gitee.com/timgame/tgf 11 | //@QQ群 7400585 12 | //author tim.huang 13 | //@Description 14 | //2023/5/30 15 | //*************************************************** 16 | 17 | func TestNewRobotWs(t *testing.T) { 18 | type args struct { 19 | path string 20 | } 21 | tests := []struct { 22 | name string 23 | args args 24 | want robot.IRobot 25 | }{ 26 | {"1", args{path: "/tgf"}, nil}, 27 | } 28 | for _, tt := range tests { 29 | t.Run(tt.name, func(t *testing.T) { 30 | got := robot.NewRobotWs(tt.args.path) 31 | got.Connect("127.0.0.1:8443") 32 | }) 33 | } 34 | select {} 35 | } 36 | -------------------------------------------------------------------------------- /rpc/.env.dev: -------------------------------------------------------------------------------- 1 | RedisAddr=127.0.0.1:6379 2 | -------------------------------------------------------------------------------- /rpc/admin.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "github.com/rs/cors" 6 | "github.com/smallnest/rpcx/client" 7 | "github.com/thkhxm/tgf" 8 | "github.com/thkhxm/tgf/exp/admin" 9 | "github.com/thkhxm/tgf/log" 10 | "net/http" 11 | "time" 12 | ) 13 | 14 | //*************************************************** 15 | //@Link https://github.com/thkhxm/tgf 16 | //@Link https://gitee.com/timgame/tgf 17 | //@QQ群 7400585 18 | //author tim.huang 19 | //@Description 20 | //2024/2/20 21 | //*************************************************** 22 | 23 | func ServeAdmin(port string) (r <-chan bool) { 24 | mux := http.NewServeMux() 25 | c := &admin.ConsulRegistry{} 26 | c.InitRegistry() 27 | c.StateCallBack = func(name, address string, state client.ConsulServerState) { 28 | s := state 29 | SendNoReplyRPCMessageByAddress(name, address, "StateHandler", &s) 30 | } 31 | mux.HandleFunc("/consul", CorsMiddleware(c.ConsulList)) 32 | mux.HandleFunc("/consul/active/{id}", CorsMiddleware(c.ActivateService)) 33 | mux.HandleFunc("/consul/close/{id}", CorsMiddleware(c.DeactivateService)) 34 | mux.HandleFunc("/consul/pause/{id}", CorsMiddleware(c.PauseService)) 35 | // 36 | mux.HandleFunc("/monitor/service", CorsMiddleware(admin.QueryMonitor)) 37 | r = NewRPCServer().WithService(&Admin{Module: Module{Name: tgf.AdminServiceModuleName, Version: "1.0"}}).WithCache(tgf.CacheModuleClose).Run() 38 | go func() { 39 | corsVar := cors.New(cors.Options{ 40 | AllowedOrigins: []string{"*"}, 41 | AllowedMethods: []string{ 42 | http.MethodPost, 43 | http.MethodGet, 44 | http.MethodOptions, 45 | }, 46 | AllowedHeaders: []string{"*"}, 47 | AllowCredentials: true, 48 | }) 49 | handler := corsVar.Handler(mux) 50 | http.ListenAndServe(":"+port, handler) 51 | log.InfoTag("admin", "admin server start at port %s", port) 52 | }() 53 | return 54 | } 55 | 56 | func CorsMiddleware(handler http.HandlerFunc) http.HandlerFunc { 57 | return func(w http.ResponseWriter, r *http.Request) { 58 | handler.ServeHTTP(w, r) 59 | } 60 | } 61 | 62 | type Admin struct { 63 | Module 64 | autoUpdateTicker *time.Ticker 65 | } 66 | 67 | func (a *Admin) L(ctx context.Context, args *string, reply *string) (err error) { 68 | return 69 | } 70 | 71 | func (a *Admin) Destroy(sub IService) { 72 | } 73 | 74 | func (a *Admin) GetName() string { 75 | return a.Name 76 | } 77 | 78 | func (a *Admin) GetVersion() string { 79 | return a.Version 80 | } 81 | 82 | func (a *Admin) Startup() (bool, error) { 83 | var () 84 | a.autoUpdateTicker = time.NewTicker(time.Second) 85 | go func() { 86 | for { 87 | select { 88 | case <-a.autoUpdateTicker.C: 89 | a.autoUpdateMonitor() 90 | } 91 | } 92 | }() 93 | return true, nil 94 | } 95 | 96 | func (a *Admin) autoUpdateMonitor() { 97 | var ( 98 | rc = getRPCClient() 99 | //xclient = rc.getClient(api.ModuleName) 100 | ) 101 | ctx := NewRPCContext() 102 | all := admin.NodeMonitorData{} 103 | rc.clients.Range(func(s string, xClient client.XClient) bool { 104 | r := &admin.NodeMonitorData{} 105 | arg := "" 106 | xClient.Call(ctx, "ASyncMonitor", &arg, r) 107 | for _, datum := range r.Data { 108 | d1 := false 109 | for _, data := range all.Data { 110 | if data.Group == datum.Group { 111 | d1 = true 112 | for _, value := range datum.Values { 113 | d2 := false 114 | for _, item := range data.Values { 115 | if item.Key == value.Key { 116 | item.Count += value.Count 117 | d2 = true 118 | break 119 | } 120 | } 121 | if !d2 { 122 | data.Values = append(data.Values, value) 123 | } 124 | } 125 | } 126 | } 127 | if !d1 { 128 | all.Data = append(all.Data, datum) 129 | } 130 | } 131 | return true 132 | }) 133 | admin.AddSecondMonitor(all) 134 | } 135 | -------------------------------------------------------------------------------- /rpc/admin_test.go: -------------------------------------------------------------------------------- 1 | package rpc_test 2 | 3 | import ( 4 | "github.com/thkhxm/tgf/rpc" 5 | "strconv" 6 | "testing" 7 | ) 8 | 9 | //*************************************************** 10 | //@Link https://github.com/thkhxm/tgf 11 | //@Link https://gitee.com/timgame/tgf 12 | //@QQ群 7400585 13 | //author tim.huang 14 | //@Description 15 | //2024/2/20 16 | //*************************************************** 17 | 18 | func TestServeAdmin(t *testing.T) { 19 | tests := []struct { 20 | name string 21 | }{ 22 | {"a"}, 23 | } 24 | for _, tt := range tests { 25 | t.Run(tt.name, func(t *testing.T) { 26 | rpc.ServeAdmin(strconv.Itoa(8001)) 27 | }) 28 | } 29 | select {} 30 | } 31 | -------------------------------------------------------------------------------- /rpc/gate.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "github.com/thkhxm/tgf" 5 | "github.com/thkhxm/tgf/log" 6 | "golang.org/x/net/context" 7 | ) 8 | 9 | //*************************************************** 10 | //@Link https://github.com/thkhxm/tgf 11 | //@Link https://gitee.com/timgame/tgf 12 | //@QQ群 7400585 13 | //author tim.huang 14 | //@Description 15 | //2023/2/26 16 | //*************************************************** 17 | 18 | // GateService 19 | // @Description: 默认网关 20 | type GateService struct { 21 | Module 22 | tcpBuilder ITCPBuilder 23 | tcpService ITCPService 24 | } 25 | 26 | func (g *GateService) GetName() string { 27 | return tgf.GatewayServiceModuleName 28 | } 29 | 30 | func (g *GateService) GetVersion() string { 31 | return "1.0" 32 | } 33 | 34 | func (g *GateService) Startup() (bool, error) { 35 | var () 36 | g.tcpService = newDefaultTCPServer(g.tcpBuilder) 37 | g.tcpService.Run() 38 | return true, nil 39 | } 40 | 41 | func (g *GateService) UploadUserNodeInfo(ctx context.Context, args *UploadUserNodeInfoReq, reply *UploadUserNodeInfoRes) error { 42 | var () 43 | if ok := g.tcpService.UpdateUserNodeInfo(args.UserId, args.ServicePath, args.NodeId); !ok { 44 | reply.ErrorCode = -1 45 | } 46 | log.DebugTag("gate", "修改用户节点信息 userId=%v servicePath=%v nodeId=%v res=%v", args.UserId, args.ServicePath, args.NodeId, reply) 47 | return nil 48 | } 49 | 50 | func (g *GateService) Login(ctx context.Context, args *LoginReq, reply *LoginRes) error { 51 | var () 52 | //踢人,重复登录的 53 | if !g.tcpService.Offline(args.UserId, true) { 54 | BorderRPCMessage(ctx, Offline.New(&OfflineReq{UserId: args.UserId}, new(OfflineRes))) 55 | } 56 | err := g.tcpService.DoLogin(args.UserId, args.TemplateUserId) 57 | return err 58 | } 59 | 60 | func (g *GateService) Offline(ctx context.Context, args *OfflineReq, reply *OfflineRes) error { 61 | var () 62 | if GetNodeId(ctx) == tgf.NodeId { 63 | return nil 64 | } 65 | g.tcpService.Offline(args.UserId, args.Replace) 66 | return nil 67 | } 68 | 69 | func (g *GateService) ToUser(ctx context.Context, args *ToUserReq, reply *ToUserRes) error { 70 | var () 71 | for _, userId := range args.UserId { 72 | done := g.tcpService.ToUser(userId, args.MessageType, args.Data) 73 | if done { 74 | log.DebugTag("gate", "主动推送 userId=%v msgLen=%v", args.UserId, len(args.Data)) 75 | } 76 | } 77 | 78 | return nil 79 | } 80 | 81 | func GatewayService(tcpBuilder ITCPBuilder) IService { 82 | service := &GateService{} 83 | service.tcpBuilder = tcpBuilder 84 | return service 85 | } 86 | 87 | var Gate = &Module{Name: "gate", Version: "1.0"} 88 | 89 | var ( 90 | UploadUserNodeInfo = &ServiceAPI[*UploadUserNodeInfoReq, *UploadUserNodeInfoRes]{ 91 | ModuleName: Gate.Name, 92 | Name: "UploadUserNodeInfo", 93 | MessageType: Gate.Name + "." + "UploadUserNodeInfo", 94 | } 95 | 96 | ToUser = &ServiceAPI[*ToUserReq, *ToUserRes]{ 97 | ModuleName: Gate.Name, 98 | Name: "ToUser", 99 | MessageType: Gate.Name + "." + "ToUser", 100 | } 101 | 102 | Login = &ServiceAPI[*LoginReq, *LoginRes]{ 103 | ModuleName: Gate.Name, 104 | Name: "Login", 105 | MessageType: Gate.Name + "." + "Login", 106 | } 107 | 108 | Offline = &ServiceAPI[*OfflineReq, *OfflineRes]{ 109 | ModuleName: Gate.Name, 110 | Name: "Offline", 111 | MessageType: Gate.Name + "." + "Offline", 112 | } 113 | ) 114 | 115 | type UploadUserNodeInfoReq struct { 116 | UserId string 117 | NodeId string 118 | ServicePath string 119 | } 120 | 121 | type UploadUserNodeInfoRes struct { 122 | ErrorCode int32 123 | } 124 | 125 | type ToUserReq struct { 126 | Data []byte 127 | UserId []string 128 | MessageType string 129 | } 130 | 131 | type ToUserRes struct { 132 | ErrorCode int32 133 | } 134 | 135 | type LoginReq struct { 136 | UserId string 137 | TemplateUserId string 138 | } 139 | 140 | type LoginRes struct { 141 | ErrorCode int32 142 | } 143 | 144 | type OfflineReq struct { 145 | UserId string 146 | //是否重复登录踢人行为 147 | Replace bool 148 | } 149 | 150 | type OfflineRes struct { 151 | ErrorCode int32 152 | } 153 | 154 | type DefaultArgs struct { 155 | C string 156 | } 157 | 158 | type DefaultReply struct { 159 | C int32 160 | } 161 | 162 | type DefaultBool struct { 163 | C bool 164 | } 165 | 166 | type EmptyReply struct { 167 | } 168 | -------------------------------------------------------------------------------- /rpc/init.go: -------------------------------------------------------------------------------- 1 | // Package rpc 2 | // @Description: rpc的封装,使用了rpcx框架 3 | // @Link: https://doc.rpcx.io/part4/alias.html rpcx框架 4 | // @Ref: 5 | package rpc 6 | 7 | import ( 8 | "github.com/thkhxm/tgf" 9 | "github.com/thkhxm/tgf/log" 10 | "github.com/thkhxm/tgf/util" 11 | ) 12 | 13 | //*************************************************** 14 | //@Link https://github.com/thkhxm/tgf 15 | //@Link https://gitee.com/timgame/tgf 16 | //@QQ群 7400585 17 | //author tim.huang 18 | //@Description 19 | //2023/2/23 20 | //*************************************************** 21 | 22 | func init() { 23 | tgf.NodeId = util.GenerateSnowflakeId() 24 | log.InfoTag("init", "node id %v", tgf.NodeId) 25 | } 26 | -------------------------------------------------------------------------------- /rpc/internal/consul.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "github.com/cornelk/hashmap" 6 | "github.com/rcrowley/go-metrics" 7 | "github.com/rpcxio/libkv/store" 8 | "github.com/rpcxio/rpcx-consul/client" 9 | "github.com/rpcxio/rpcx-consul/serverplugin" 10 | "github.com/smallnest/rpcx/server" 11 | "github.com/thkhxm/tgf" 12 | "github.com/thkhxm/tgf/log" 13 | "time" 14 | ) 15 | 16 | //*************************************************** 17 | //@Link https://github.com/thkhxm/tgf 18 | //@Link https://gitee.com/timgame/tgf 19 | //@QQ群 7400585 20 | //author tim.huang 21 | //@Description 22 | //2023/2/23 23 | //*************************************************** 24 | 25 | var LocalServerAddress = "" 26 | 27 | type ConsulDiscovery struct { 28 | discoveryMap *hashmap.Map[string, *client.ConsulDiscovery] 29 | } 30 | 31 | func (c *ConsulDiscovery) initStruct() { 32 | var () 33 | c.discoveryMap = hashmap.New[string, *client.ConsulDiscovery]() 34 | } 35 | 36 | func (c *ConsulDiscovery) RegisterServer(ip string) server.Plugin { 37 | var ( 38 | address = tgf.GetStrListConfig(tgf.EnvironmentConsulAddress) 39 | serviceAddress = fmt.Sprintf("tcp@%v", ip) 40 | _logAddressMsg string 41 | _basePath = tgf.GetStrConfig[string](tgf.EnvironmentConsulPath) 42 | ) 43 | //注册服务发现根目录 44 | r := &serverplugin.ConsulRegisterPlugin{ 45 | ServiceAddress: serviceAddress, 46 | ConsulServers: address, 47 | BasePath: _basePath, 48 | Metrics: metrics.NewRegistry(), 49 | UpdateInterval: time.Second * 10, 50 | } 51 | err := r.Start() 52 | if err != nil { 53 | log.Error("[init] 服务发现启动异常 %v", err) 54 | } 55 | for _, s := range address { 56 | _logAddressMsg += s + "," 57 | } 58 | log.InfoTag("init", "服务发现加载成功 注册根目录 consulAddress=%v serviceAddress=%v path=%v", r.ServiceAddress, _logAddressMsg, _basePath) 59 | LocalServerAddress = serviceAddress 60 | return r 61 | } 62 | 63 | func (c *ConsulDiscovery) RegisterDiscovery(moduleName string) *client.ConsulDiscovery { 64 | var () 65 | var ( 66 | address = tgf.GetStrListConfig(tgf.EnvironmentConsulAddress) 67 | basePath = tgf.GetStrConfig[string](tgf.EnvironmentConsulPath) 68 | ) 69 | 70 | //new discovery 71 | 72 | conf := &store.Config{ 73 | ClientTLS: nil, 74 | TLS: nil, 75 | ConnectionTimeout: 0, 76 | Bucket: "", 77 | PersistConnection: false, 78 | Username: "", 79 | Password: "", 80 | } 81 | d, _ := client.NewConsulDiscovery(basePath, moduleName, address, conf) 82 | 83 | //if moduleName != "" { 84 | c.discoveryMap.Set(moduleName, d) 85 | log.InfoTag("init", "注册rpcx discovery moduleName=%v", moduleName) 86 | //} 87 | 88 | return d 89 | } 90 | 91 | func (c *ConsulDiscovery) GetDiscovery(moduleName string) *client.ConsulDiscovery { 92 | if val, ok := c.discoveryMap.Get(moduleName); ok { 93 | return val 94 | } 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /rpc/internal/discovery.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/rpcxio/rpcx-consul/client" 5 | "github.com/smallnest/rpcx/server" 6 | "github.com/thkhxm/tgf/log" 7 | ) 8 | 9 | //*************************************************** 10 | //@Link https://github.com/thkhxm/tgf 11 | //@Link https://gitee.com/timgame/tgf 12 | //@QQ群 7400585 13 | //author tim.huang 14 | //@Description 15 | //2023/2/23 16 | //*************************************************** 17 | 18 | var discovery IRPCDiscovery 19 | 20 | // IRPCDiscovery 21 | // @Description: rpc服务注册接口 22 | type IRPCDiscovery interface { 23 | // RegisterServer 24 | // @Description: 注册rpcx的服务发现 25 | // @param ip 传入注册的本机ip和端口 example: 192.168.1.10:8881 26 | // @return server.Plugin 返回的是rpcx所需的插件类型 27 | RegisterServer(ip string) server.Plugin 28 | 29 | RegisterDiscovery(moduleName string) *client.ConsulDiscovery 30 | GetDiscovery(moduleName string) *client.ConsulDiscovery 31 | } 32 | 33 | func UseConsulDiscovery() { 34 | if discovery != nil { 35 | return 36 | } 37 | cd := new(ConsulDiscovery) 38 | cd.initStruct() 39 | discovery = cd 40 | log.InfoTag("init", "装载consul discovery模块") 41 | } 42 | 43 | func GetDiscovery() IRPCDiscovery { 44 | if discovery == nil { 45 | UseConsulDiscovery() 46 | } 47 | return discovery 48 | } 49 | -------------------------------------------------------------------------------- /rpc/internal/jwt.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | //*************************************************** 4 | //@Link https://github.com/thkhxm/tgf 5 | //@Link https://gitee.com/timgame/tgf 6 | //@QQ群 7400585 7 | //author tim.huang 8 | //@Description 9 | //2023/2/25 10 | //*************************************************** 11 | 12 | type LoginCheck struct { 13 | } 14 | 15 | func (l LoginCheck) CheckLogin(token string) (bool, string) { 16 | return true, token 17 | } 18 | -------------------------------------------------------------------------------- /rpc/monitor.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "github.com/thkhxm/tgf" 6 | "github.com/thkhxm/tgf/exp/admin" 7 | ) 8 | 9 | //*************************************************** 10 | //@Link https://github.com/thkhxm/tgf 11 | //@Link https://gitee.com/timgame/tgf 12 | //@QQ群 7400585 13 | //author tim.huang 14 | //@Description 15 | //2024/2/26 16 | //*************************************************** 17 | 18 | type MonitorService struct { 19 | Module 20 | } 21 | 22 | func (m *MonitorService) ASyncMonitor(ctx context.Context, args *string, reply *admin.NodeMonitorData) (err error) { 23 | all := admin.AllMonitor() 24 | *reply = all 25 | return 26 | } 27 | 28 | func (m *MonitorService) GetName() string { 29 | return tgf.MonitorServiceModuleName 30 | } 31 | 32 | func (m *MonitorService) GetVersion() string { 33 | return "1.0" 34 | } 35 | 36 | func (m *MonitorService) Startup() (bool, error) { 37 | var () 38 | return true, nil 39 | } 40 | -------------------------------------------------------------------------------- /rpc/plugins.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/bytedance/sonic" 7 | "github.com/cornelk/hashmap" 8 | "github.com/edwingeng/doublejump" 9 | client2 "github.com/smallnest/rpcx/client" 10 | "github.com/smallnest/rpcx/protocol" 11 | "github.com/smallnest/rpcx/server" 12 | "github.com/smallnest/rpcx/share" 13 | "github.com/thkhxm/tgf" 14 | "github.com/thkhxm/tgf/db" 15 | "github.com/thkhxm/tgf/exp/admin" 16 | "github.com/thkhxm/tgf/log" 17 | "github.com/thkhxm/tgf/util" 18 | "net/url" 19 | "strings" 20 | "time" 21 | ) 22 | 23 | //*************************************************** 24 | //@Link https://github.com/thkhxm/tgf 25 | //@Link https://gitee.com/timgame/tgf 26 | //@QQ群 7400585 27 | //author tim.huang 28 | //@Description 29 | //2023/2/24 30 | //*************************************************** 31 | 32 | var ( 33 | reqMetaDataTimeout = time.Hour * 24 * 7 34 | localNodeCacheTimeout int64 = 60 * 5 35 | ) 36 | 37 | type ConsulServerInfo struct { 38 | State string 39 | Version int32 40 | SubVersion int32 41 | NodeId string 42 | } 43 | 44 | func newConsulServerInfo(data string) *ConsulServerInfo { 45 | if q, a := url.ParseQuery(data); a == nil { 46 | v, _ := util.StrToAny[int32](strings.Split(q.Get("version"), ".")[0]) 47 | subv, _ := util.StrToAny[int32](strings.Split(q.Get("version"), ".")[1]) 48 | return &ConsulServerInfo{ 49 | State: q.Get("state"), 50 | Version: v, 51 | SubVersion: subv, 52 | NodeId: q.Get("nodeId"), 53 | } 54 | } 55 | return nil 56 | } 57 | 58 | type CustomSelector struct { 59 | moduleName string 60 | h *doublejump.Hash 61 | servers *hashmap.Map[string, *ConsulServerInfo] 62 | cacheManager db.IAutoCacheService[string, string] 63 | pushGate bool 64 | } 65 | 66 | func (c *CustomSelector) clearAllUserCache() { 67 | var () 68 | c.cacheManager = c.cacheManager.Reset() 69 | } 70 | 71 | func (c *CustomSelector) Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) (selected string) { 72 | if sc, ok := ctx.(*share.Context); ok { 73 | size := c.servers.Len() 74 | switch size { 75 | case 0: 76 | return "" 77 | default: 78 | reqMetaData := sc.Value(share.ReqMetaDataKey).(map[string]string) 79 | sc.Lock() 80 | defer sc.Unlock() 81 | selected = reqMetaData[servicePath] 82 | //用户级别的请求 83 | userId := reqMetaData[tgf.ContextKeyUserId] 84 | rpcTip := reqMetaData[tgf.ContextKeyRPCType] 85 | broadcasts := make([]string, 1) 86 | 87 | if userId != "" { 88 | broadcasts[0] = userId 89 | } else { 90 | // 判断是否首次登录,查看templeUserId是否存在 91 | templeUserId := reqMetaData[tgf.ContextKeyTemplateUserId] 92 | if templeUserId != "" { 93 | broadcasts[0] = templeUserId 94 | } 95 | } 96 | var bindNode bool 97 | if rpcTip == tgf.RPCBroadcastTip { 98 | ids := reqMetaData[tgf.ContextKeyBroadcastUserIds] 99 | broadcasts = strings.Split(ids, ",") 100 | if !c.checkServerAlive(selected) { 101 | key := client2.HashString(fmt.Sprintf("%v", time.Now().UnixNano())) 102 | selected, _ = c.h.Get(key).(string) 103 | } 104 | bindNode = true 105 | } 106 | if len(broadcasts) > 0 && broadcasts[0] != "" { 107 | for _, uid := range broadcasts { 108 | var key uint64 109 | //先判断携带节点信息是否存活 110 | if c.checkServerAlive(selected) { 111 | if bindNode { 112 | c.processNode(ctx, uid, selected, reqMetaData, servicePath) 113 | } 114 | continue 115 | } 116 | 117 | //从本地缓存中获取用户的节点数据 118 | selected, _ = c.cacheManager.Get(uid) 119 | if c.checkServerAlive(selected) { 120 | continue 121 | } 122 | //如果上面的用户节点获取,没有命中,那么取当前请求模式 123 | //如果是rpc推送请求, 124 | //if rpcTip == tgf.RPCTip { 125 | //从数据缓存中获取用户的节点数据 126 | reqMetaDataKey := fmt.Sprintf(tgf.RedisKeyUserNodeMeta, uid) 127 | reqMetaCacheData, suc := db.GetMap[string, string](reqMetaDataKey) 128 | if !suc { 129 | reqMetaCacheData = make(map[string]string) 130 | } 131 | selected = reqMetaCacheData[servicePath] 132 | if c.checkServerAlive(selected) { 133 | //将节点数据,放入本地缓存 134 | if reqMetaData[tgf.ContextKeyCloseLocalCache] == "" { 135 | c.cacheManager.Set(uid, selected) 136 | } 137 | continue 138 | } else { 139 | //通过一致性hash的方式,命中一个活跃的业务节点 140 | key = client2.HashString(uid) 141 | selected, _ = c.h.Get(key).(string) 142 | reqMetaData[servicePath] = selected 143 | c.processNode(ctx, uid, selected, reqMetaData, servicePath) 144 | } 145 | } 146 | } else { 147 | if c.checkServerAlive(selected) { 148 | return 149 | } 150 | key := client2.HashString(fmt.Sprintf("%v", time.Now().UnixNano())) 151 | selected, _ = c.h.Get(key).(string) 152 | } 153 | return 154 | } 155 | } 156 | 157 | return "" 158 | } 159 | func (c *CustomSelector) processNode(ctx context.Context, uid string, selected string, reqMetaData map[string]string, servicePath string) { 160 | reqMetaDataKeyTemp := fmt.Sprintf(tgf.RedisKeyUserNodeMeta, uid) 161 | db.PutMap(reqMetaDataKeyTemp, servicePath, selected, reqMetaDataTimeout) 162 | if reqMetaData[tgf.ContextKeyCloseLocalCache] == "" { 163 | c.cacheManager.Set(selected, uid) 164 | } 165 | if c.pushGate && UploadUserNodeInfo.ModuleName != servicePath { 166 | util.Go(func() { 167 | if _, err := SendRPCMessage(ctx, UploadUserNodeInfo.New(&UploadUserNodeInfoReq{ 168 | UserId: uid, 169 | NodeId: selected, 170 | ServicePath: servicePath, 171 | }, &UploadUserNodeInfoRes{ErrorCode: 0})); err != nil { 172 | log.Warn("[rpc] 节点更新异常 %v", err) 173 | } 174 | }) 175 | } 176 | } 177 | 178 | func (c *CustomSelector) UpdateServer(servers map[string]string) { 179 | // TODO: 新增虚拟节点,优化hash的命中分布 180 | var serverInfos string 181 | clearUserCache := false 182 | for k, v := range servers { 183 | if v == "" { 184 | continue 185 | } 186 | if log.CheckLogTag("discovery") { 187 | serverInfos += fmt.Sprintf("%v:%v,", k, v) 188 | } 189 | c.h.Add(k) 190 | if c.servers.Insert(k, newConsulServerInfo(v)) { 191 | clearUserCache = true 192 | } else { 193 | c.servers.Set(k, newConsulServerInfo(v)) 194 | } 195 | } 196 | 197 | c.servers.Range(func(k string, v *ConsulServerInfo) bool { 198 | if servers[k] == "" { // remove 199 | c.h.Remove(k) 200 | c.servers.Del(k) 201 | clearUserCache = true 202 | log.DebugTag("discovery", "remove server %v", v) 203 | } 204 | if v.State == string(client2.ConsulServerStatePause) { 205 | c.h.Remove(k) 206 | log.DebugTag("discovery", "server %v state is %v", v, v.State) 207 | } 208 | return true 209 | }) 210 | 211 | if clearUserCache { 212 | c.clearAllUserCache() 213 | log.DebugTag("discovery", "moduleName=%v 更新服务节点", c.moduleName) 214 | } 215 | log.DebugTag("discovery", "moduleName=%v 节点数据%v", c.moduleName, serverInfos) 216 | } 217 | 218 | func (c *CustomSelector) checkServerAlive(server string) (h bool) { 219 | var () 220 | if server == "" { 221 | return false 222 | } 223 | 224 | _, h = c.servers.Get(server) 225 | return 226 | } 227 | 228 | func (c *CustomSelector) initStruct(moduleName string) { 229 | c.servers = hashmap.New[string, *ConsulServerInfo]() 230 | c.h = doublejump.NewHash() 231 | c.moduleName = moduleName 232 | c.pushGate = tgf.GetStrConfig[int32](tgf.EnvironmentGatePush) == 1 233 | c.cacheManager = db.NewAutoCacheManager[string, string](localNodeCacheTimeout) 234 | } 235 | 236 | type XClientHandler struct { 237 | } 238 | 239 | func (r *XClientHandler) PreCall(ctx context.Context, serviceName, methodName string, args interface{}) error { 240 | var traceId string 241 | if sc, ok := ctx.(*share.Context); ok { 242 | traceId = sc.GetReqMetaDataByKey(tgf.ContextKeyTRACEID) 243 | sc.SetValue(tgf.ContextKeyNodeId, tgf.NodeId) 244 | } 245 | argStr, _ := sonic.MarshalString(args) 246 | log.DebugTag("trace", "[%s] client [%s] 发送 [%v-%v] 请求 , 参数 [%v]", traceId, tgf.NodeId, serviceName, methodName, argStr) 247 | return nil 248 | } 249 | 250 | func (r *XClientHandler) PostCall(ctx context.Context, servicePath, serviceMethod string, args interface{}, reply interface{}, err error) error { 251 | var traceId string 252 | if sc, ok := ctx.(*share.Context); ok { 253 | traceId = sc.GetReqMetaDataByKey(tgf.ContextKeyTRACEID) 254 | } 255 | replyStr, _ := sonic.MarshalString(reply) 256 | log.DebugTag("trace", "[%s] client [%s] 接收 [%v-%v] 响应 , 返回结果 [%v] ", traceId, tgf.NodeId, servicePath, serviceMethod, replyStr) 257 | return err 258 | } 259 | 260 | type XServerHandler struct { 261 | } 262 | 263 | func (r *XServerHandler) PreCall(ctx context.Context, serviceName, methodName string, args interface{}) (result interface{}, e error) { 264 | var traceId string 265 | if sc, ok := ctx.(*share.Context); ok { 266 | traceId = sc.GetReqMetaDataByKey(tgf.ContextKeyTRACEID) 267 | sc.SetValue("timestamp", time.Now().UnixMilli()) 268 | } 269 | argStr, _ := sonic.MarshalString(args) 270 | log.DebugTag("trace", "[%s] server [%s] 接收 [%v-%v] 请求 , 参数 [%v]", traceId, tgf.NodeId, serviceName, methodName, argStr) 271 | return args, nil 272 | } 273 | 274 | func (r *XServerHandler) PostCall(ctx context.Context, servicePath, serviceMethod string, args interface{}, reply interface{}, err error) (result interface{}, e error) { 275 | var d int64 276 | var traceId string 277 | if sc, ok := ctx.(*share.Context); ok { 278 | traceId = sc.GetReqMetaDataByKey(tgf.ContextKeyTRACEID) 279 | t := sc.Value("timestamp") 280 | if t != nil { 281 | d = time.Now().UnixMilli() - t.(int64) 282 | } 283 | } 284 | replyStr, _ := sonic.MarshalString(reply) 285 | log.DebugTag("trace", "[%s] server [%s] 执行 [%v-%v] 完毕 耗时[%d], 返回结果 [%v] ", traceId, tgf.NodeId, servicePath, serviceMethod, d, replyStr) 286 | return reply, err 287 | } 288 | 289 | // PostReadRequest counts read 290 | func (r *XServerHandler) PostReadRequest(ctx context.Context, m *protocol.Message, e error) error { 291 | sp := m.ServicePath 292 | sm := m.ServiceMethod 293 | 294 | if sp == "" { 295 | return nil 296 | } 297 | admin.PointRPCRequest(sp, sm) 298 | return nil 299 | } 300 | 301 | type ILoginCheck interface { 302 | CheckLogin(token string) (bool, string) 303 | } 304 | 305 | func NewCustomSelector(moduleName string) client2.Selector { 306 | res := &CustomSelector{} 307 | res.initStruct(moduleName) 308 | return res 309 | } 310 | 311 | func NewRPCXClientHandler() client2.PostCallPlugin { 312 | res := &XClientHandler{} 313 | return res 314 | } 315 | 316 | func NewRPCXServerHandler() server.PostCallPlugin { 317 | res := &XServerHandler{} 318 | return res 319 | } 320 | -------------------------------------------------------------------------------- /rpc/plugins_test.go: -------------------------------------------------------------------------------- 1 | package rpc_test 2 | 3 | import ( 4 | "github.com/thkhxm/tgf/rpc" 5 | "testing" 6 | ) 7 | 8 | //*************************************************** 9 | //@Link https://github.com/thkhxm/tgf 10 | //@Link https://gitee.com/timgame/tgf 11 | //@QQ群 7400585 12 | //author tim.huang 13 | //@Description 14 | //2023/3/10 15 | //*************************************************** 16 | 17 | func TestCustomSelector_UpdateServer(t *testing.T) { 18 | type fields struct { 19 | moduleName string 20 | } 21 | type args struct { 22 | servers map[string]string 23 | } 24 | tests := []struct { 25 | name string 26 | fields fields 27 | args args 28 | }{ 29 | // TODO: Add test cases. 30 | {"t1", fields{moduleName: "test"}, args{map[string]string{"a": "1", "b": "2", "c": "3", "d": "4"}}}, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | this := rpc.NewCustomSelector(tt.fields.moduleName) 35 | this.UpdateServer(tt.args.servers) 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /rpc/rpcserver_example_test.go: -------------------------------------------------------------------------------- 1 | package rpc_test 2 | 3 | import "github.com/thkhxm/tgf/rpc" 4 | 5 | //*************************************************** 6 | //@Link https://github.com/thkhxm/tgf 7 | //@Link https://gitee.com/timgame/tgf 8 | //@QQ群 7400585 9 | //author tim.huang 10 | //@Description 11 | //2024/8/16 12 | //*************************************************** 13 | 14 | // ExampleNewRPCServer 15 | // 16 | // @Description: rpc server 17 | func ExampleNewRPCServer() { 18 | // rpc server 19 | r := rpc.NewRPCServer().WithService(new(rpc.GateService)).Run() 20 | <-r 21 | } 22 | -------------------------------------------------------------------------------- /rpc/rpcserver_test.go: -------------------------------------------------------------------------------- 1 | package rpc_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | client2 "github.com/rpcxio/rpcx-consul/client" 8 | "github.com/smallnest/rpcx/client" 9 | "github.com/thkhxm/tgf" 10 | "github.com/thkhxm/tgf/log" 11 | "github.com/thkhxm/tgf/rpc" 12 | "golang.org/x/net/context" 13 | "net" 14 | "sync" 15 | "testing" 16 | ) 17 | 18 | //*************************************************** 19 | //@Link https://github.com/thkhxm/tgf 20 | //@Link https://gitee.com/timgame/tgf 21 | //@QQ群 7400585 22 | //author tim.huang 23 | //@Description 24 | //2023/2/23 25 | //*************************************************** 26 | 27 | func TestStartRpcServer(t *testing.T) { 28 | rpcServer := rpc.NewRPCServer() 29 | service := new(DemoService) 30 | 31 | service2 := new(Demo2Service) 32 | rpcServer. 33 | WithService(service). 34 | WithService(service2). 35 | WithGateway("8038"). 36 | Run() 37 | 38 | w := sync.WaitGroup{} 39 | w.Add(1) 40 | w.Wait() 41 | } 42 | 43 | func TestWssServer(t *testing.T) { 44 | rpcServer := rpc.NewRPCServer() 45 | service := new(DemoService) 46 | 47 | rpcServer. 48 | WithService(service). 49 | WithGatewayWSS("8038", "/wss", "cert.pem", "key.pem"). 50 | Run() 51 | 52 | w := sync.WaitGroup{} 53 | w.Add(1) 54 | w.Wait() 55 | } 56 | 57 | func TestTcpClientSender(t *testing.T) { 58 | 59 | // [1][1][2][2][n][n] 60 | // magic number|message type|request method name size|data size|method name|data 61 | //for i := 0; i < 10; i++ { 62 | // go func() { 63 | add, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8038") 64 | client, err := net.DialTCP("tcp", nil, add) 65 | if err != nil { 66 | t.Logf("client error: %v", err) 67 | return 68 | } 69 | //for i := 0; i < 100; i++ { 70 | //var msg = "say hello - " + strconv.Itoa(i) 71 | //Login 72 | loginBuff := LoginByteTest() 73 | cnt, er := client.Write(loginBuff.Bytes()) 74 | t.Logf("send login message : %v", loginBuff.Bytes()) 75 | //Logic 76 | buff := LogicByteTest() 77 | cnt, er = client.Write(buff.Bytes()) 78 | if er != nil { 79 | t.Logf("write len %v error : %v", cnt, er) 80 | } 81 | t.Logf("send logic message : %v", buff.Bytes()) 82 | 83 | //for { 84 | // resBytes := make([]byte, 1024) 85 | // client.Read(resBytes) 86 | // t.Logf("response message : %v", resBytes) 87 | //} 88 | 89 | // } 90 | //}() 91 | //time.Sleep(time.Second * 3) 92 | //buf := make([]byte, 1024) 93 | //rcnt, er2 := client.Read(buf) 94 | //if er2 != nil { 95 | // t.Logf("write len %v error : %v", rcnt, er) 96 | //} 97 | //t.Logf("callback message : %v", string(buf)) 98 | //} 99 | wg := sync.WaitGroup{} 100 | wg.Add(1) 101 | wg.Wait() 102 | } 103 | 104 | func LoginByteTest() *bytes.Buffer { 105 | var token = "token-test2022" 106 | data := []byte(token) 107 | tmp := make([]byte, 0, 4+len(data)) 108 | buff := bytes.NewBuffer(tmp) 109 | buff.WriteByte(250) 110 | reqSizeLenByte := make([]byte, 2) 111 | binary.BigEndian.PutUint16(reqSizeLenByte, uint16(len(data))) 112 | buff.Write(reqSizeLenByte) 113 | buff.Write(data) 114 | return buff 115 | } 116 | 117 | func LogicByteTest() *bytes.Buffer { 118 | service := new(Demo2Service) 119 | var msg = "say hello - " 120 | data := []byte(msg) 121 | reqName := []byte(fmt.Sprintf("%v.%v", service.GetName(), "RPCSayHello")) 122 | 123 | tmp := make([]byte, 0, 6+len(data)+len(reqName)) 124 | buff := bytes.NewBuffer(tmp) 125 | buff.WriteByte(250) 126 | buff.WriteByte(byte(rpc.Logic)) 127 | reqLenByte := make([]byte, 2) 128 | binary.BigEndian.PutUint16(reqLenByte, uint16(len(reqName))) 129 | buff.Write(reqLenByte) 130 | reqSizeLenByte := make([]byte, 2) 131 | binary.BigEndian.PutUint16(reqSizeLenByte, uint16(len(data))) 132 | buff.Write(reqSizeLenByte) 133 | buff.Write(reqName) 134 | buff.Write(data) 135 | return buff 136 | } 137 | 138 | func TestClientSender(t *testing.T) { 139 | service := new(Demo2Service) 140 | serviceName := fmt.Sprintf("%v", service.GetName()) 141 | d, _ := client2.NewConsulDiscovery(tgf.GetStrConfig[string](tgf.EnvironmentConsulPath), serviceName, tgf.GetStrListConfig(tgf.EnvironmentConsulAddress), nil) 142 | xclient := client.NewXClient(serviceName, client.Failtry, client.RandomSelect, d, client.DefaultOption) 143 | defer xclient.Close() 144 | xclient.Call(context.Background(), "RPCSayHello", nil, nil) 145 | w := sync.WaitGroup{} 146 | w.Add(1) 147 | w.Wait() 148 | } 149 | 150 | type Demo2Service struct { 151 | rpc.Module 152 | } 153 | 154 | func (this *Demo2Service) GetName() string { 155 | return "example" 156 | } 157 | 158 | func (this *Demo2Service) GetVersion() string { 159 | return "v1.0" 160 | } 161 | 162 | func (this *Demo2Service) Startup() (bool, error) { 163 | var () 164 | return true, nil 165 | } 166 | 167 | func (this *Demo2Service) RPCSayHello(ctx context.Context, args *interface{}, reply *interface{}) error { 168 | var () 169 | log.Info("[test] rpcx2请求抵达 ") 170 | return nil 171 | } 172 | 173 | type DemoService struct { 174 | rpc.Module 175 | } 176 | 177 | func (this *DemoService) GetName() string { 178 | return "demo" 179 | } 180 | 181 | func (this *DemoService) GetVersion() string { 182 | return "v1.0" 183 | } 184 | 185 | func (this *DemoService) Startup() (bool, error) { 186 | var () 187 | return true, nil 188 | } 189 | func (this *DemoService) RPCSayHello(ctx context.Context, args *interface{}, reply *interface{}) error { 190 | var () 191 | log.Info("[test] rpcx请求抵达 ") 192 | return nil 193 | } 194 | -------------------------------------------------------------------------------- /rpc/service.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "github.com/smallnest/rpcx/client" 5 | "github.com/smallnest/rpcx/share" 6 | "github.com/thkhxm/tgf" 7 | "github.com/thkhxm/tgf/log" 8 | "golang.org/x/net/context" 9 | "reflect" 10 | ) 11 | 12 | //*************************************************** 13 | //@Link https://github.com/thkhxm/tgf 14 | //@Link https://gitee.com/timgame/tgf 15 | //@QQ群 7400585 16 | //author tim.huang 17 | //@Description 18 | //2023/2/23 19 | //*************************************************** 20 | 21 | // IService 22 | // 23 | // @Description: 逻辑服务接口 24 | type IService interface { 25 | GetName() string 26 | GetVersion() string 27 | Startup() (bool, error) 28 | Destroy(sub IService) 29 | GetLogicSyncMethod() []string 30 | } 31 | 32 | type Module struct { 33 | Name string 34 | Version string 35 | State client.ConsulServerState 36 | userLoginHook []loginHook 37 | userOfflineHook []offlineHook 38 | } 39 | 40 | func (m *Module) GetName() string { 41 | return m.Name 42 | } 43 | 44 | func (m *Module) GetVersion() string { 45 | return m.Version 46 | } 47 | 48 | func (m *Module) Destroy(sub IService) { 49 | var () 50 | log.InfoTag("system", "destroy module=%v version=%v", sub.GetName(), sub.GetVersion()) 51 | } 52 | 53 | func (m *Module) GetLogicSyncMethod() []string { 54 | return nil 55 | } 56 | 57 | func (m *Module) StateHandler(ctx context.Context, args *client.ConsulServerState, reply *string) (err error) { 58 | m.State = *args 59 | log.InfoTag("system", "update module state %s to %s module=%v version=%v", m.State, args, m.Name, m.Version) 60 | return 61 | } 62 | 63 | func (m *Module) AddUserLoginHook(hook loginHook) { 64 | m.userLoginHook = append(m.userLoginHook, hook) 65 | } 66 | func (m *Module) AddUserOfflineHook(hook offlineHook) { 67 | m.userOfflineHook = append(m.userOfflineHook, hook) 68 | } 69 | 70 | func (m *Module) OfflineHook(ctx context.Context, args *OfflineReq, reply *EmptyReply) (err error) { 71 | if len(m.userOfflineHook) == 0 { 72 | return 73 | } 74 | for _, hook := range m.userOfflineHook { 75 | err = hook(ctx, args.UserId, args.Replace) 76 | if err != nil { 77 | return 78 | } 79 | } 80 | return 81 | } 82 | 83 | func (m *Module) LoginHook(ctx context.Context, args *DefaultArgs, reply *EmptyReply) (err error) { 84 | if len(m.userLoginHook) == 0 { 85 | return 86 | } 87 | for _, hook := range m.userLoginHook { 88 | err = hook(ctx, args.C) 89 | if err != nil { 90 | return 91 | } 92 | } 93 | return 94 | } 95 | 96 | type ServiceAPI[Req, Res any] struct { 97 | ModuleName string 98 | Name string 99 | MessageType string 100 | Des string 101 | args Req 102 | reply Res 103 | } 104 | 105 | func (s *ServiceAPI[Req, Res]) New(req Req, res Res) *ServiceAPI[Req, Res] { 106 | var () 107 | return &ServiceAPI[Req, Res]{ModuleName: s.ModuleName, Name: s.Name, args: req, reply: res, MessageType: s.MessageType} 108 | } 109 | 110 | func (s *ServiceAPI[Req, Res]) NewRPC(req Req) *ServiceAPI[Req, Res] { 111 | var () 112 | var res Res 113 | resType := reflect.TypeOf((*Res)(nil)).Elem() // 获取Res的类型 114 | resValue := reflect.New(resType) // 创建Res的新实例 115 | 116 | // 如果Res是一个指针类型,我们需要通过.Elem()获取其指向的值 117 | //if resType.Kind() == reflect.Ptr { 118 | // res = resValue.Interface().(Res) 119 | //} else { 120 | res = resValue.Elem().Interface().(Res) 121 | //} 122 | return &ServiceAPI[Req, Res]{ModuleName: s.ModuleName, Name: s.Name, args: req, reply: res, MessageType: s.MessageType} 123 | } 124 | 125 | func (s *ServiceAPI[Req, Res]) NewEmpty() *ServiceAPI[Req, Res] { 126 | var () 127 | var req Req 128 | var res Res 129 | return &ServiceAPI[Req, Res]{ModuleName: s.ModuleName, Name: s.Name, args: req, reply: res, MessageType: s.MessageType} 130 | } 131 | 132 | func (s *ServiceAPI[Req, Res]) GetResult() Res { 133 | var () 134 | return s.reply 135 | } 136 | 137 | func GetUserId(ctx context.Context) string { 138 | if ct, ok := ctx.(*share.Context); ok { 139 | return ct.GetReqMetaDataByKey(tgf.ContextKeyUserId) 140 | } 141 | return "" 142 | } 143 | 144 | func GetNodeId(ctx context.Context) string { 145 | if ct, ok := ctx.(*share.Context); ok { 146 | return ct.GetReqMetaDataByKey(tgf.ContextKeyNodeId) 147 | } 148 | return "" 149 | } 150 | 151 | func GetTemplateUserId(ctx context.Context) string { 152 | if ct, ok := ctx.(*share.Context); ok { 153 | return ct.GetReqMetaDataByKey(tgf.ContextKeyTemplateUserId) 154 | } 155 | return "" 156 | } 157 | -------------------------------------------------------------------------------- /rpc/service_test.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // *************************************************** 8 | // @Link https://github.com/thkhxm/tgf 9 | // @Link https://gitee.com/timgame/tgf 10 | // @QQ群 7400585 11 | // author tim.huang 12 | // @Description 13 | // 2024/2/23 14 | // *************************************************** 15 | type rpcTestData struct { 16 | DD string 17 | } 18 | 19 | func TestServiceAPI_NewRPC(t *testing.T) { 20 | Login.NewRPC(&LoginReq{}) 21 | } 22 | -------------------------------------------------------------------------------- /rpc/tcp_test.go: -------------------------------------------------------------------------------- 1 | package rpc_test 2 | 3 | //*************************************************** 4 | //@Link https://github.com/thkhxm/tgf 5 | //@Link https://gitee.com/timgame/tgf 6 | //@QQ群 7400585 7 | //author tim.huang 8 | //@Description 9 | //2023/2/25 10 | //*************************************************** 11 | -------------------------------------------------------------------------------- /rpc/ws.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.26.0 4 | // protoc v3.21.12 5 | // source: ws.proto 6 | 7 | package rpc 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type WSMessage struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | Module string `protobuf:"bytes,1,opt,name=module,proto3" json:"module,omitempty"` 29 | ServiceName string `protobuf:"bytes,2,opt,name=serviceName,proto3" json:"serviceName,omitempty"` 30 | Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` 31 | ReqId int32 `protobuf:"varint,4,opt,name=reqId,proto3" json:"reqId,omitempty"` 32 | } 33 | 34 | func (x *WSMessage) Reset() { 35 | *x = WSMessage{} 36 | if protoimpl.UnsafeEnabled { 37 | mi := &file_ws_proto_msgTypes[0] 38 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 39 | ms.StoreMessageInfo(mi) 40 | } 41 | } 42 | 43 | func (x *WSMessage) String() string { 44 | return protoimpl.X.MessageStringOf(x) 45 | } 46 | 47 | func (*WSMessage) ProtoMessage() {} 48 | 49 | func (x *WSMessage) ProtoReflect() protoreflect.Message { 50 | mi := &file_ws_proto_msgTypes[0] 51 | if protoimpl.UnsafeEnabled && x != nil { 52 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 53 | if ms.LoadMessageInfo() == nil { 54 | ms.StoreMessageInfo(mi) 55 | } 56 | return ms 57 | } 58 | return mi.MessageOf(x) 59 | } 60 | 61 | // Deprecated: Use WSMessage.ProtoReflect.Descriptor instead. 62 | func (*WSMessage) Descriptor() ([]byte, []int) { 63 | return file_ws_proto_rawDescGZIP(), []int{0} 64 | } 65 | 66 | func (x *WSMessage) GetModule() string { 67 | if x != nil { 68 | return x.Module 69 | } 70 | return "" 71 | } 72 | 73 | func (x *WSMessage) GetServiceName() string { 74 | if x != nil { 75 | return x.ServiceName 76 | } 77 | return "" 78 | } 79 | 80 | func (x *WSMessage) GetData() []byte { 81 | if x != nil { 82 | return x.Data 83 | } 84 | return nil 85 | } 86 | 87 | func (x *WSMessage) GetReqId() int32 { 88 | if x != nil { 89 | return x.ReqId 90 | } 91 | return 0 92 | } 93 | 94 | type WSResponse struct { 95 | state protoimpl.MessageState 96 | sizeCache protoimpl.SizeCache 97 | unknownFields protoimpl.UnknownFields 98 | 99 | MessageType string `protobuf:"bytes,1,opt,name=messageType,proto3" json:"messageType,omitempty"` 100 | Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` 101 | ReqId int32 `protobuf:"varint,3,opt,name=reqId,proto3" json:"reqId,omitempty"` 102 | Code int32 `protobuf:"varint,4,opt,name=code,proto3" json:"code,omitempty"` 103 | Zip bool `protobuf:"varint,5,opt,name=zip,proto3" json:"zip,omitempty"` 104 | } 105 | 106 | func (x *WSResponse) Reset() { 107 | *x = WSResponse{} 108 | if protoimpl.UnsafeEnabled { 109 | mi := &file_ws_proto_msgTypes[1] 110 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 111 | ms.StoreMessageInfo(mi) 112 | } 113 | } 114 | 115 | func (x *WSResponse) String() string { 116 | return protoimpl.X.MessageStringOf(x) 117 | } 118 | 119 | func (*WSResponse) ProtoMessage() {} 120 | 121 | func (x *WSResponse) ProtoReflect() protoreflect.Message { 122 | mi := &file_ws_proto_msgTypes[1] 123 | if protoimpl.UnsafeEnabled && x != nil { 124 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 125 | if ms.LoadMessageInfo() == nil { 126 | ms.StoreMessageInfo(mi) 127 | } 128 | return ms 129 | } 130 | return mi.MessageOf(x) 131 | } 132 | 133 | // Deprecated: Use WSResponse.ProtoReflect.Descriptor instead. 134 | func (*WSResponse) Descriptor() ([]byte, []int) { 135 | return file_ws_proto_rawDescGZIP(), []int{1} 136 | } 137 | 138 | func (x *WSResponse) GetMessageType() string { 139 | if x != nil { 140 | return x.MessageType 141 | } 142 | return "" 143 | } 144 | 145 | func (x *WSResponse) GetData() []byte { 146 | if x != nil { 147 | return x.Data 148 | } 149 | return nil 150 | } 151 | 152 | func (x *WSResponse) GetReqId() int32 { 153 | if x != nil { 154 | return x.ReqId 155 | } 156 | return 0 157 | } 158 | 159 | func (x *WSResponse) GetCode() int32 { 160 | if x != nil { 161 | return x.Code 162 | } 163 | return 0 164 | } 165 | 166 | func (x *WSResponse) GetZip() bool { 167 | if x != nil { 168 | return x.Zip 169 | } 170 | return false 171 | } 172 | 173 | var File_ws_proto protoreflect.FileDescriptor 174 | 175 | var file_ws_proto_rawDesc = []byte{ 176 | 0x0a, 0x08, 0x77, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x6f, 0x0a, 0x09, 0x57, 0x53, 177 | 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 178 | 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x12, 179 | 0x20, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 180 | 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 181 | 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 182 | 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x71, 0x49, 0x64, 0x18, 0x04, 183 | 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x72, 0x65, 0x71, 0x49, 0x64, 0x22, 0x7e, 0x0a, 0x0a, 0x57, 184 | 0x53, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x65, 0x73, 185 | 0x73, 0x61, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 186 | 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 187 | 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 188 | 0x14, 0x0a, 0x05, 0x72, 0x65, 0x71, 0x49, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 189 | 0x72, 0x65, 0x71, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 190 | 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x7a, 0x69, 0x70, 191 | 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x7a, 0x69, 0x70, 0x42, 0x07, 0x5a, 0x05, 0x2e, 192 | 0x2f, 0x3b, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 193 | } 194 | 195 | var ( 196 | file_ws_proto_rawDescOnce sync.Once 197 | file_ws_proto_rawDescData = file_ws_proto_rawDesc 198 | ) 199 | 200 | func file_ws_proto_rawDescGZIP() []byte { 201 | file_ws_proto_rawDescOnce.Do(func() { 202 | file_ws_proto_rawDescData = protoimpl.X.CompressGZIP(file_ws_proto_rawDescData) 203 | }) 204 | return file_ws_proto_rawDescData 205 | } 206 | 207 | var file_ws_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 208 | var file_ws_proto_goTypes = []interface{}{ 209 | (*WSMessage)(nil), // 0: WSMessage 210 | (*WSResponse)(nil), // 1: WSResponse 211 | } 212 | var file_ws_proto_depIdxs = []int32{ 213 | 0, // [0:0] is the sub-list for method output_type 214 | 0, // [0:0] is the sub-list for method input_type 215 | 0, // [0:0] is the sub-list for extension type_name 216 | 0, // [0:0] is the sub-list for extension extendee 217 | 0, // [0:0] is the sub-list for field type_name 218 | } 219 | 220 | func init() { file_ws_proto_init() } 221 | func file_ws_proto_init() { 222 | if File_ws_proto != nil { 223 | return 224 | } 225 | if !protoimpl.UnsafeEnabled { 226 | file_ws_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 227 | switch v := v.(*WSMessage); i { 228 | case 0: 229 | return &v.state 230 | case 1: 231 | return &v.sizeCache 232 | case 2: 233 | return &v.unknownFields 234 | default: 235 | return nil 236 | } 237 | } 238 | file_ws_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 239 | switch v := v.(*WSResponse); i { 240 | case 0: 241 | return &v.state 242 | case 1: 243 | return &v.sizeCache 244 | case 2: 245 | return &v.unknownFields 246 | default: 247 | return nil 248 | } 249 | } 250 | } 251 | type x struct{} 252 | out := protoimpl.TypeBuilder{ 253 | File: protoimpl.DescBuilder{ 254 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 255 | RawDescriptor: file_ws_proto_rawDesc, 256 | NumEnums: 0, 257 | NumMessages: 2, 258 | NumExtensions: 0, 259 | NumServices: 0, 260 | }, 261 | GoTypes: file_ws_proto_goTypes, 262 | DependencyIndexes: file_ws_proto_depIdxs, 263 | MessageInfos: file_ws_proto_msgTypes, 264 | }.Build() 265 | File_ws_proto = out.File 266 | file_ws_proto_rawDesc = nil 267 | file_ws_proto_goTypes = nil 268 | file_ws_proto_depIdxs = nil 269 | } 270 | -------------------------------------------------------------------------------- /rpc/ws.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; //指定版本信息 2 | option go_package="./;pb"; 3 | 4 | message WSMessage{ 5 | string module =1; //模块名称 6 | string serviceName =2; //服务名称 7 | bytes data =3; //请求数据 8 | int32 reqId =4; //请求id 9 | } 10 | 11 | message WSResponse{ 12 | string messageType =1; //响应消息 13 | bytes data =2; //响应数据 14 | int32 reqId =3; //请求id 15 | int32 code =4; //响应状态码 16 | bool zip =5; //是否压缩 17 | } -------------------------------------------------------------------------------- /util/aes.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/rand" 8 | "errors" 9 | "io" 10 | ) 11 | 12 | type Aes struct { 13 | block cipher.Block 14 | mode cipher.BlockMode 15 | } 16 | 17 | // GenerateKey 生成一个 AES 密钥。 18 | func GenerateKey() ([]byte, error) { 19 | key := make([]byte, 32) // AES-256 需要 32 字节长的密钥 20 | if _, err := io.ReadFull(rand.Reader, key); err != nil { 21 | return nil, err 22 | } 23 | return key, nil 24 | } 25 | 26 | // pad 对数据进行填充,使其长度为块大小的倍数。 27 | func pad(src []byte) []byte { 28 | padding := aes.BlockSize - len(src)%aes.BlockSize 29 | padtext := bytes.Repeat([]byte{byte(padding)}, padding) 30 | return append(src, padtext...) 31 | } 32 | 33 | // unpad 移除填充数据。 34 | func unpad(src []byte) []byte { 35 | length := len(src) 36 | unpadding := int(src[length-1]) 37 | return src[:(length - unpadding)] 38 | } 39 | 40 | func (a *Aes) EncryptAES(text []byte) ([]byte, error) { 41 | msg := pad(text) 42 | ciphertext := make([]byte, aes.BlockSize+len(msg)) 43 | iv := ciphertext[:aes.BlockSize] 44 | if _, err := io.ReadFull(rand.Reader, iv); err != nil { 45 | return nil, err 46 | } 47 | 48 | mode := cipher.NewCBCEncrypter(a.block, iv) 49 | mode.CryptBlocks(ciphertext[aes.BlockSize:], msg) 50 | 51 | return ciphertext, nil 52 | } 53 | 54 | func (a *Aes) DecryptAES(ciphertext []byte) ([]byte, error) { 55 | if len(ciphertext) < aes.BlockSize { 56 | return nil, errors.New("ciphertext too short") 57 | } 58 | iv := ciphertext[:aes.BlockSize] 59 | ciphertext = ciphertext[aes.BlockSize:] 60 | 61 | mode := cipher.NewCBCDecrypter(a.block, iv) 62 | mode.CryptBlocks(ciphertext, ciphertext) 63 | return unpad(ciphertext), nil 64 | } 65 | 66 | func NewAes(key []byte) (*Aes, error) { 67 | block, err := aes.NewCipher(key) 68 | if err != nil { 69 | return nil, err 70 | } 71 | return &Aes{block: block}, nil 72 | } 73 | -------------------------------------------------------------------------------- /util/aes_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "encoding/hex" 5 | "github.com/thkhxm/tgf/util" 6 | "testing" 7 | ) 8 | 9 | var strKey = "hduiorlkjuysbxgthduiorlkjuysbxgt" 10 | 11 | func TestEncrypt(t *testing.T) { 12 | key := util.StringToSliceByte(strKey) 13 | t.Logf("key: %v", hex.EncodeToString(key)) 14 | a, _ := util.NewAes(key) 15 | b := util.StringToSliceByte("hello world") 16 | tt, _ := a.EncryptAES(b) 17 | t.Logf("encrypt: %v", hex.EncodeToString(tt)) 18 | } 19 | 20 | func TestDecrypt(t *testing.T) { 21 | key := util.StringToSliceByte(strKey) 22 | t.Logf("key: %v, length %d", key, len(key)) 23 | a, _ := util.NewAes(key) 24 | d, _ := hex.DecodeString("4ee7b0e6119f0cd6d6b8e8eca02a85c5ccf86a9a48082868f27977c9f4ba41ae") 25 | tt, _ := a.DecryptAES(d) 26 | t.Logf("encrypt: %s", tt) 27 | } 28 | -------------------------------------------------------------------------------- /util/code.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "golang.org/x/text/cases" 6 | "golang.org/x/text/language" 7 | "os" 8 | "path/filepath" 9 | "reflect" 10 | "regexp" 11 | "sort" 12 | "strings" 13 | "text/template" 14 | "time" 15 | ) 16 | 17 | //*************************************************** 18 | //@Link https://github.com/thkhxm/tgf 19 | //@Link https://gitee.com/timgame/tgf 20 | //@QQ群 7400585 21 | //author tim.huang 22 | //@Description 23 | //2023/4/27 24 | //*************************************************** 25 | 26 | var autoGenerateAPICodePath, _ = filepath.Abs(".") 27 | var autoGenerateAPICSCodePath = "" 28 | var autoGenerateAPICSCodeNamespace = "" 29 | 30 | //func SetAutoGenerateAPICodePath(path string) { 31 | // var err error 32 | // autoGenerateAPICodePath, err = filepath.Abs(path) 33 | // if err != nil { 34 | // panic(err) 35 | // } 36 | // fmt.Printf("设置api代码自动生成路径为 %v", autoGenerateAPICodePath) 37 | //} 38 | 39 | func SetAutoGenerateAPICSCode(path, namespace string) { 40 | var err error 41 | autoGenerateAPICSCodePath, err = filepath.Abs(path) 42 | autoGenerateAPICSCodeNamespace = namespace 43 | if err != nil { 44 | panic(err) 45 | } 46 | fmt.Printf("设置C# api代码自动生成路径为 %v", autoGenerateAPICSCodePath) 47 | } 48 | 49 | func getProjectModulePath() string { 50 | dir, _ := os.Getwd() 51 | var mod []byte 52 | var err error 53 | for i := 0; i < 5; i++ { 54 | filePath := filepath.Join(dir, "go.mod") 55 | mod, err = os.ReadFile(filePath) 56 | if err == nil { 57 | break 58 | } 59 | dir = filepath.Join(dir, "..") 60 | } 61 | if err != nil { 62 | fmt.Println(err) 63 | return "" 64 | } 65 | 66 | // 使用正则表达式提取模块路径 67 | re := regexp.MustCompile(`module\s+([^\s]+)`) 68 | match := re.FindStringSubmatch(string(mod)) 69 | if len(match) == 2 { 70 | fmt.Println("模块路径:", match[1]) 71 | } 72 | return match[1] 73 | } 74 | func GeneratorRPC[T any](moduleName, version string) { 75 | var t T 76 | v := reflect.ValueOf(&t) 77 | ty := v.Type().Elem() 78 | s := make([]struct { 79 | Args string 80 | Reply string 81 | NewReply string 82 | MethodName string 83 | LowerMethodName string 84 | }, 0) 85 | a := struct { 86 | PackageImports []string 87 | Apis []struct { 88 | Args string 89 | Reply string 90 | NewReply string 91 | MethodName string 92 | LowerMethodName string 93 | } 94 | }{} 95 | tt := make(map[string]bool) 96 | tt["github.com/thkhxm/tgf/rpc"] = true 97 | 98 | for i := 0; i < ty.NumMethod(); i++ { 99 | m := ty.Method(i) 100 | // 遍历方法的参数列表 101 | for j := 0; j < m.Type.NumIn(); j++ { 102 | // 获取参数类型对象 103 | argType := m.Type.In(j) 104 | pkg := argType.PkgPath() 105 | if argType.Kind() == reflect.Pointer { 106 | pkg = argType.Elem().PkgPath() 107 | tt[pkg] = true 108 | } 109 | } 110 | d := struct { 111 | Args string 112 | Reply string 113 | NewReply string 114 | MethodName string 115 | LowerMethodName string 116 | }{Args: m.Type.In(1).String(), Reply: m.Type.In(2).String(), 117 | NewReply: m.Type.In(2).String()[1:], MethodName: m.Name, LowerMethodName: strings.ToLower(string(m.Name[0])) + m.Name[1:]} 118 | 119 | var r = regexp.MustCompile("[A-Za-z0-9_]+\\.[A-Za-z0-9_]+\\[(.*)\\]") 120 | match := r.FindStringSubmatch(d.Args) 121 | if len(match) > 1 { 122 | pointIndex := strings.LastIndex(match[1], ".") 123 | pk := match[1][1:pointIndex] 124 | l := strings.LastIndex(pk, "/") 125 | d.Args = "*" + pk[l+1:] + match[1][pointIndex:] 126 | tt[pk] = true 127 | } 128 | 129 | match = r.FindStringSubmatch(d.Reply) 130 | if len(match) > 1 { 131 | pointIndex := strings.LastIndex(match[1], ".") 132 | pk := match[1][1:pointIndex] 133 | l := strings.LastIndex(pk, "/") 134 | d.Reply = "*" + pk[l+1:] + match[1][pointIndex:] 135 | tt[pk] = true 136 | } 137 | s = append(s, d) 138 | } 139 | pi := make([]string, 0) 140 | 141 | for k, _ := range tt { 142 | if k == "" { 143 | continue 144 | } 145 | pi = append(pi, k) 146 | } 147 | sort.Strings(pi) 148 | if len(s) == 0 { 149 | return 150 | } 151 | a.Apis = s 152 | a.PackageImports = pi 153 | packageName := moduleName + "_service" 154 | tpl := fmt.Sprintf(` 155 | //Auto generated by tgf util 156 | //created at %v 157 | //%v module rpc code, version: %v 158 | 159 | package %v 160 | 161 | import ( 162 | {{range .PackageImports}} "{{.}}" 163 | {{end}} 164 | ) 165 | 166 | var ( 167 | {{range .Apis}} 168 | {{.MethodName}} = rpc.ServiceAPI[{{.Args}}, {{.Reply}}]{ 169 | ModuleName: "%v", 170 | Name: "{{.MethodName}}", 171 | MessageType: "%v."+"{{.MethodName}}", 172 | } 173 | {{end}} 174 | ) 175 | `, time.Now().String(), moduleName, version, packageName, moduleName, moduleName) 176 | tm := template.New("apiStruct") 177 | tp, _ := tm.Parse(tpl) 178 | fileName := strings.ToLower(moduleName) + ".rpc.service.go" 179 | dirPath := autoGenerateAPICodePath + string(filepath.Separator) + "generated" + string(filepath.Separator) 180 | path := dirPath + moduleName + "_service" 181 | 182 | // 创建路径中的所有必要的目录 183 | err := os.MkdirAll(path, os.ModePerm) 184 | if err != nil { 185 | panic(err) 186 | } 187 | file, err := os.Create(path + string(filepath.Separator) + fileName) 188 | if err != nil { 189 | panic(err) 190 | } 191 | defer file.Close() 192 | tp.Execute(file, a) 193 | // 194 | modulePath := getProjectModulePath() + "/generated/" + moduleName + "_service" 195 | packageName = moduleName + "_rpc" 196 | tt["context"] = true 197 | tt[modulePath] = true 198 | pi = make([]string, 0) 199 | for k, _ := range tt { 200 | if k == "" { 201 | continue 202 | } 203 | pi = append(pi, k) 204 | } 205 | sort.Strings(pi) 206 | a.PackageImports = pi 207 | tpl2 := fmt.Sprintf(` 208 | //Auto generated by tgf util 209 | //created at %v 210 | //%v module rpc code, version: %v 211 | 212 | package %v 213 | 214 | import ( 215 | {{range .PackageImports}} "{{.}}" 216 | {{end}} 217 | ) 218 | 219 | {{range .Apis}} 220 | func {{.MethodName}}(ctx context.Context, args {{.Args}}, async bool) (reply {{.Reply}},err error) { 221 | reply = new({{.NewReply}}) 222 | if async { 223 | err = rpc.SendNoReplyRPCMessage(ctx, %s_service.{{.MethodName}}.New(args, reply)) 224 | } else { 225 | reply, err = rpc.SendRPCMessage(ctx, %s_service.{{.MethodName}}.New(args, reply)) 226 | } 227 | return 228 | } 229 | {{end}} 230 | `, time.Now().String(), moduleName, version, packageName, moduleName, moduleName) 231 | tm2 := template.New("rpcStruct") 232 | tp2, _ := tm2.Parse(tpl2) 233 | fileName2 := strings.ToLower(moduleName) + ".rpc.go" 234 | path2 := dirPath + moduleName + "_rpc" 235 | 236 | // 创建路径中的所有必要的目录 237 | err2 := os.MkdirAll(path2, os.ModePerm) 238 | if err2 != nil { 239 | panic(err2) 240 | } 241 | file2, err2 := os.Create(path2 + string(filepath.Separator) + fileName2) 242 | if err2 != nil { 243 | panic(err2) 244 | } 245 | defer file2.Close() 246 | tp2.Execute(file2, a) 247 | 248 | fmt.Println() 249 | fmt.Printf("rpc generate done . module[%s] version[%s]", moduleName, version) 250 | } 251 | 252 | type CSStruct struct { 253 | PackageImports []string 254 | Apis []struct { 255 | Args string 256 | Reply string 257 | MethodName string 258 | } 259 | ModuleName string 260 | ModuleNameUpper string 261 | PushServices []string 262 | } 263 | 264 | var csStructCache = &CSStruct{} 265 | 266 | // GeneratorAPI 267 | // @Description: 生成api文件 268 | // @param ref 269 | func GeneratorAPI[T any](moduleName, version string, pushServices ...string) { 270 | var t T 271 | v := reflect.ValueOf(&t) 272 | ty := v.Type().Elem() 273 | packageName := moduleName + "_service" 274 | tt := make(map[string]bool) 275 | tt["github.com/thkhxm/tgf/rpc"] = true 276 | goStructCache := &CSStruct{} 277 | goStructCache.ModuleName = moduleName 278 | goStructCache.ModuleNameUpper = cases.Title(language.English).String(moduleName) 279 | csStructCache.ModuleName = moduleName 280 | csStructCache.ModuleNameUpper = cases.Title(language.English).String(moduleName) 281 | for i := 0; i < ty.NumMethod(); i++ { 282 | m := ty.Method(i) 283 | // 遍历方法的参数列表 284 | for j := 0; j < m.Type.NumIn(); j++ { 285 | // 获取参数类型对象 286 | argType := m.Type.In(j) 287 | pkg := argType.PkgPath() 288 | if argType.Kind() == reflect.Pointer { 289 | pkg = argType.Elem().PkgPath() 290 | tt[pkg] = true 291 | } 292 | } 293 | 294 | d := struct { 295 | Args string 296 | Reply string 297 | MethodName string 298 | }{Args: m.Type.In(1).String(), Reply: m.Type.In(2).String(), MethodName: m.Name} 299 | var r = regexp.MustCompile("[A-Za-z0-9_]+\\.[A-Za-z0-9_]+\\[(.*)\\]") 300 | match := r.FindStringSubmatch(d.Args) 301 | if len(match) > 1 { 302 | pointIndex := strings.LastIndex(match[1], ".") 303 | pk := match[1][1:pointIndex] 304 | l := strings.LastIndex(pk, "/") 305 | d.Args = "*" + pk[l+1:] + match[1][pointIndex:] 306 | tt[pk] = true 307 | } 308 | 309 | match = r.FindStringSubmatch(d.Reply) 310 | if len(match) > 1 { 311 | pointIndex := strings.LastIndex(match[1], ".") 312 | pk := match[1][1:pointIndex] 313 | l := strings.LastIndex(pk, "/") 314 | d.Reply = "*" + pk[l+1:] + match[1][pointIndex:] 315 | tt[pk] = true 316 | } 317 | csStructCache.Apis = append(csStructCache.Apis, struct { 318 | Args string 319 | Reply string 320 | MethodName string 321 | }{Args: d.Args, Reply: strings.Split(d.Reply, ".")[1], MethodName: d.MethodName}) 322 | goStructCache.Apis = append(goStructCache.Apis, d) 323 | } 324 | pi := make([]string, 0) 325 | for k, _ := range tt { 326 | pi = append(pi, k) 327 | } 328 | sort.Strings(pi) 329 | 330 | goStructCache.PackageImports = pi 331 | goStructCache.PushServices = pushServices 332 | csStructCache.PushServices = append(csStructCache.PushServices, pushServices...) 333 | tpl := fmt.Sprintf(` 334 | //Auto generated by tgf util 335 | //created at %v 336 | 337 | package %v 338 | 339 | import ( 340 | {{range .PackageImports}} "{{.}}" 341 | {{end}} 342 | ) 343 | 344 | var %vService = &rpc.Module{Name: "%v", Version: "%v"} 345 | 346 | var ( 347 | {{range .Apis}} 348 | {{.MethodName}} = rpc.ServiceAPI[{{.Args}}, {{.Reply}}]{ 349 | ModuleName: %vService.Name, 350 | Name: "{{.MethodName}}", 351 | MessageType: %vService.Name+"."+"{{.MethodName}}", 352 | } 353 | {{end}} 354 | ) 355 | `, time.Now().String(), packageName, cases.Title(language.English).String(moduleName), moduleName, version, cases.Title(language.English).String(moduleName), cases.Title(language.English).String(moduleName)) 356 | tm := template.New("apiStruct") 357 | tp, _ := tm.Parse(tpl) 358 | fileName := strings.ToLower(moduleName) + ".api.service.go" 359 | // 创建路径中的所有必要的目录 360 | pt := autoGenerateAPICodePath + string(filepath.Separator) + "generated" + string(filepath.Separator) + moduleName + "_service" 361 | err := os.MkdirAll(pt, os.ModePerm) 362 | if err != nil { 363 | panic(err) 364 | } 365 | file, err := os.Create(pt + string(filepath.Separator) + fileName) 366 | if err != nil { 367 | panic(err) 368 | } 369 | defer file.Close() 370 | tp.Execute(file, goStructCache) 371 | 372 | // 373 | packageName = moduleName + "_api" 374 | tpl2 := fmt.Sprintf(` 375 | //Auto generated by tgf util 376 | //created at %v 377 | 378 | package %v 379 | 380 | const( 381 | {{range .PushServices}} {{.}} = "{{.}}" 382 | {{end}} 383 | ) 384 | 385 | `, time.Now().String(), packageName) 386 | tm2 := template.New("pushStruct") 387 | tp2, _ := tm2.Parse(tpl2) 388 | fileName2 := strings.ToLower(moduleName) + ".api.go" 389 | rt2 := autoGenerateAPICodePath + string(filepath.Separator) + "generated" + string(filepath.Separator) + moduleName + "_api" 390 | // 创建路径中的所有必要的目录 391 | err2 := os.MkdirAll(rt2, os.ModePerm) 392 | if err2 != nil { 393 | panic(err2) 394 | } 395 | file2, err2 := os.Create(rt2 + string(filepath.Separator) + fileName2) 396 | if err != nil { 397 | panic(err) 398 | } 399 | defer file2.Close() 400 | tp2.Execute(file2, goStructCache) 401 | 402 | //tp.Execute(os.Stdout, a) 403 | } 404 | 405 | func GenerateCSApiService() { 406 | if autoGenerateAPICSCodeNamespace != "" { 407 | 408 | tplCS := fmt.Sprintf(` 409 | //Auto generated by tgf util 410 | //created at %v 411 | 412 | namespace %v 413 | { 414 | public struct %sServerApi 415 | { 416 | {{range .Apis}} 417 | public static readonly Api<{{.Reply}}> {{.MethodName}} = new("%s","{{.MethodName}}", false, {{.Reply}}.Parser.ParseFrom); 418 | {{end}} 419 | {{range .PushServices}} 420 | public static readonly string {{.}} = "{{.}}"; 421 | {{end}} 422 | } 423 | 424 | } 425 | `, time.Now().String(), autoGenerateAPICSCodeNamespace, csStructCache.ModuleNameUpper, csStructCache.ModuleName) 426 | tmCS := template.New("apiCSStruct") 427 | tpCS, _ := tmCS.Parse(tplCS) 428 | fileCSName := csStructCache.ModuleNameUpper + "ServerApi.cs" 429 | err := os.MkdirAll(autoGenerateAPICodePath, os.ModePerm) 430 | if err != nil { 431 | panic(err) 432 | } 433 | fileCS, errCS := os.Create(autoGenerateAPICSCodePath + string(filepath.Separator) + fileCSName) 434 | if errCS != nil { 435 | panic(errCS) 436 | } 437 | defer fileCS.Close() 438 | tpCS.Execute(fileCS, csStructCache) 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /util/code_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "context" 5 | "github.com/thkhxm/tgf/rpc" 6 | "github.com/thkhxm/tgf/util" 7 | "testing" 8 | ) 9 | 10 | // *************************************************** 11 | // @Link https://github.com/thkhxm/tgf 12 | // @Link https://gitee.com/timgame/tgf 13 | // @QQ群 7400585 14 | // author tim.huang 15 | // @Description 16 | // 2023/4/27 17 | // *************************************************** 18 | //type IExampleService interface { 19 | // Login(ctx context.Context, args *rpc.Args[*hallpb.HallSayRequest], reply *rpc.Reply[*hallpb.HallSayRequest]) (err error) 20 | // Login2(ctx context.Context, args *rpc.Args[*hallpb.HallSayRequest], reply *rpc.Reply[*hallpb.HallSayRequest]) (err error) 21 | //} 22 | 23 | type IGenerateCodeRPCTest interface { 24 | RPCTest(ctx context.Context, args *rpc.EmptyReply, reply *rpc.EmptyReply) (err error) 25 | } 26 | 27 | func TestGeneratorAPI(x *testing.T) { 28 | ////generate client api 29 | //util.GeneratorAPI[chat_module.IChatService](internal.ModuleName, internal.Version, 30 | // "ChatPush") 31 | ////generate rpc api 32 | util.GeneratorRPC[IGenerateCodeRPCTest]("code_test", "1.0.0") 33 | ////generate cs api 34 | //util.SetAutoGenerateAPICSCode("E:\\unity\\project\\t2\\Assets\\HotFix\\Code", "HotFix.Code") 35 | //util.GenerateCSApiService() 36 | } 37 | -------------------------------------------------------------------------------- /util/common.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | //*************************************************** 4 | //@Link https://github.com/thkhxm/tgf 5 | //@Link https://gitee.com/timgame/tgf 6 | //@QQ群 7400585 7 | //author tim.huang 8 | //@Description 9 | //2023/3/14 10 | //*************************************************** 11 | 12 | func IsNil[Val any](val Val) { 13 | 14 | switch any(val).(type) { 15 | 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /util/config_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "github.com/thkhxm/tgf" 5 | "github.com/thkhxm/tgf/component" 6 | "github.com/thkhxm/tgf/db" 7 | "github.com/thkhxm/tgf/util" 8 | "testing" 9 | ) 10 | 11 | //*************************************************** 12 | //@Link https://github.com/thkhxm/tgf 13 | //@Link https://gitee.com/timgame/tgf 14 | //@QQ群 7400585 15 | //author tim.huang 16 | //@Description 17 | //2023/4/10 18 | //*************************************************** 19 | 20 | func TestExcelToJson(t *testing.T) { 21 | util.SetExcelPath("C:\\Users\\AUSA\\Desktop\\配置文件") 22 | util.SetExcelToJsonPath("./") 23 | util.ExcelExport() 24 | } 25 | 26 | func TestExcelToJsonFull(t *testing.T) { 27 | // 28 | db.WithCacheModule(tgf.CacheModuleClose) 29 | //设置excel路径 30 | util.SetExcelPath("./excel") 31 | //设置excel导出的go文件路径 32 | util.SetExcelToGoPath("../common/conf") 33 | //设置excel导出的json文件路径 34 | util.SetExcelToJsonPath("../common/conf/js") 35 | //开始导出excel 36 | util.ExcelExport() 37 | 38 | //设置配置源数据路径 39 | component.WithConfPath("../common/conf/js") 40 | //初始化游戏配置到内存中 41 | component.InitGameConfToMem() 42 | 43 | ////获取配置数据 44 | //codes := component.GetAllGameConf[*conf.ErrorCodeConf]() 45 | ////初始化结构化kv数据源 46 | //data := make([]util.TemplateKeyValueData, len(codes), len(codes)) 47 | //for i, code := range codes { 48 | // data[i] = util.TemplateKeyValueData{ 49 | // FieldName: code.FieldName, 50 | // Values: code.Code, 51 | // } 52 | //} 53 | ////将数据源写入文件 生成kv结构文件 54 | //util.JsonToKeyValueGoFile("errorcodes", "error_code", "../common/define/errorcode", "int32", data) 55 | 56 | } 57 | -------------------------------------------------------------------------------- /util/example_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/thkhxm/tgf/util" 6 | ) 7 | 8 | //*************************************************** 9 | //@Link https://github.com/thkhxm/tgf 10 | //@Link https://gitee.com/timgame/tgf 11 | //@QQ群 7400585 12 | //author tim.huang 13 | //@Description 14 | //2023/3/6 15 | //*************************************************** 16 | 17 | type StrToAnyStruct struct { 18 | Name string 19 | Age int32 20 | } 21 | 22 | func ExampleStrToAny() { 23 | //to int 24 | if numInt, err := util.StrToAny[int]("1024"); err == nil { 25 | fmt.Println("numInt:", numInt) 26 | } 27 | 28 | //to interface{} 29 | if structData, err2 := util.StrToAny[StrToAnyStruct]("{\"Name\":\"tim\",\"Age\":5}"); err2 == nil { 30 | fmt.Println("json data:", structData.Name) 31 | } 32 | // Output: 33 | // numInt: 1024 34 | // json data: tim 35 | } 36 | -------------------------------------------------------------------------------- /util/file.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | //*************************************************** 12 | //@Link https://github.com/thkhxm/tgf 13 | //@Link https://gitee.com/timgame/tgf 14 | //@QQ群 7400585 15 | //author tim.huang 16 | //@Description 17 | //2023/4/10 18 | //*************************************************** 19 | 20 | func GetFileList(path string, ext string) []string { 21 | var all_file []string 22 | finfo, _ := os.ReadDir(path) 23 | for _, info := range finfo { 24 | if filepath.Ext(info.Name()) == ext { 25 | real_path := path + string(filepath.Separator) + info.Name() 26 | if info.IsDir() { 27 | //all_file = append(all_file, getFileList(real_path)...) 28 | } else { 29 | all_file = append(all_file, real_path) 30 | } 31 | } 32 | } 33 | return all_file 34 | } 35 | 36 | func GetFileMd5(file string) string { 37 | f, err := os.OpenFile(file, os.O_RDONLY, 0o600) 38 | if err != nil { 39 | return "" 40 | } 41 | md5h := md5.New() 42 | io.Copy(md5h, f) 43 | f.Close() 44 | return hex.EncodeToString(md5h.Sum(nil)) 45 | } 46 | -------------------------------------------------------------------------------- /util/init.go: -------------------------------------------------------------------------------- 1 | // Package util 2 | // @Description: 工具包 3 | // @Link: https://github.com/panjf2000/ants 是一个高性能且低损耗的 goroutine 池 4 | // @Ref: 5 | package util 6 | 7 | //*************************************************** 8 | //@Link https://github.com/thkhxm/tgf 9 | //@Link https://gitee.com/timgame/tgf 10 | //@QQ群 7400585 11 | //author tim.huang 12 | //@Description 13 | //2023/2/24 14 | //*************************************************** 15 | 16 | func init() { 17 | InitGoroutinePool() 18 | initSnowFlake() 19 | } 20 | -------------------------------------------------------------------------------- /util/ip.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | ) 8 | 9 | //*************************************************** 10 | //@Link https://github.com/thkhxm/tgf 11 | //@Link https://gitee.com/timgame/tgf 12 | //@QQ群 7400585 13 | //author tim.huang 14 | //@Description 15 | //2023/2/23 16 | //*************************************************** 17 | 18 | // GetLocalHost 19 | // @Description: 获取本机ip 20 | // @return ip 21 | func GetLocalHost() (ip string) { 22 | //return GetLocalHost2() 23 | // 使用udp发起网络连接, 这样不需要关注连接是否可通, 随便填一个即可 24 | conn, err := net.Dial("udp", "8.8.8.8:53") 25 | if err != nil { 26 | fmt.Println(err) 27 | return 28 | } 29 | localAddr := conn.LocalAddr().(*net.UDPAddr) 30 | // fmt.Println(localAddr.String()) 31 | ip = strings.Split(localAddr.String(), ":")[0] 32 | return 33 | } 34 | 35 | func GetLocalHost2() (ip string) { 36 | addrList, err := net.InterfaceAddrs() 37 | if err != nil { 38 | panic(err) 39 | } 40 | for _, address := range addrList { 41 | if ipNet, ok := address.(*net.IPNet); ok && !ipNet.IP.IsLoopback() { 42 | if ipNet.IP.To4() != nil { 43 | fmt.Println(ipNet.IP.String()) 44 | return ipNet.IP.String() 45 | } 46 | } 47 | } 48 | return 49 | } 50 | -------------------------------------------------------------------------------- /util/ip_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "github.com/thkhxm/tgf/util" 5 | "testing" 6 | ) 7 | 8 | //*************************************************** 9 | //@Link https://github.com/thkhxm/tgf 10 | //@Link https://gitee.com/timgame/tgf 11 | //@QQ群 7400585 12 | //author tim.huang 13 | //@Description 14 | //2023/9/3 15 | //*************************************************** 16 | 17 | func TestGetLocalHost(t *testing.T) { 18 | t.Logf(util.GetLocalHost()) 19 | } 20 | 21 | func TestGetLocalHost2(t *testing.T) { 22 | t.Logf(util.GetLocalHost2()) 23 | } 24 | -------------------------------------------------------------------------------- /util/math.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | //*************************************************** 4 | //@Link https://github.com/thkhxm/tgf 5 | //@Link https://gitee.com/timgame/tgf 6 | //@QQ群 7400585 7 | //author tim.huang 8 | //@Description 9 | //2023/12/17 10 | //*************************************************** 11 | 12 | // Min Min[T number] 13 | // 14 | // @Description: 获取最小值 15 | // @param a 16 | // @param b 17 | // @return T 18 | func Min[T number](a, b T) T { 19 | if a < b { 20 | return a 21 | } 22 | return b 23 | } 24 | 25 | // Max Max[T number] 26 | // 27 | // @Description: 获取最大值 28 | // @param a 29 | // @param b 30 | // @return T 31 | func Max[T number](a, b T) T { 32 | if a < b { 33 | return b 34 | } 35 | return a 36 | } 37 | -------------------------------------------------------------------------------- /util/pb.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "google.golang.org/protobuf/proto" 5 | "google.golang.org/protobuf/reflect/protoreflect" 6 | "reflect" 7 | ) 8 | 9 | //*************************************************** 10 | //@Link https://github.com/thkhxm/tgf 11 | //@Link https://gitee.com/timgame/tgf 12 | //@QQ群 7400585 13 | //author tim.huang 14 | //@Description 15 | //2023/4/17 16 | //*************************************************** 17 | 18 | func ConvertToPB[T protoreflect.ProtoMessage](data []byte) (t T) { 19 | var () 20 | v := reflect.ValueOf(t) 21 | if v.IsNil() { 22 | v = reflect.New(v.Type().Elem()) 23 | } 24 | t = v.Interface().(T) 25 | proto.Unmarshal(data, t) 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /util/pool.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/panjf2000/ants/v2" 5 | "time" 6 | ) 7 | 8 | //*************************************************** 9 | //@Link https://github.com/thkhxm/tgf 10 | //@Link https://gitee.com/timgame/tgf 11 | //@QQ群 7400585 12 | //author tim.huang 13 | //@Description 14 | //2023/2/24 15 | //*************************************************** 16 | 17 | var goroutinePool *ants.Pool 18 | 19 | // 默认通用协程池大小 20 | var defaultPoolSize int = 2e5 21 | 22 | // Go 23 | // @Description: 无序运行投递的任务, 24 | // @param f 25 | func Go(f func()) { 26 | goroutinePool.Submit(f) 27 | } 28 | 29 | func InitGoroutinePool() { 30 | goroutinePool, _ = ants.NewPool(defaultPoolSize, ants.WithExpiryDuration(time.Minute*3)) 31 | //a, _ := ants.NewPoolWithFunc(10, func(i interface{}) { 32 | // 33 | //}, ants.WithPanicHandler(func(d interface{}) { 34 | // log.Warn("[goroutine] ants线程池异常 %v", d) 35 | //})) 36 | // ants will pre-malloc the whole capacity of pool when you invoke this function 37 | //p, _ := ants.NewPool(100000, ants.WithPreAlloc(true)) 38 | } 39 | -------------------------------------------------------------------------------- /util/pool_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "github.com/thkhxm/tgf/util" 5 | "sync" 6 | "testing" 7 | ) 8 | 9 | //*************************************************** 10 | //@Link https://github.com/thkhxm/tgf 11 | //@Link https://gitee.com/timgame/tgf 12 | //@QQ群 7400585 13 | //author tim.huang 14 | //@Description 15 | //2023/2/22 16 | //*************************************************** 17 | 18 | func TestGo(t *testing.T) { 19 | type args struct { 20 | f func() 21 | } 22 | tests := []struct { 23 | name string 24 | args args 25 | }{ 26 | // TODO: Add test cases. 27 | {"1", args{func() { 28 | t.Logf("ants test %v", 1) 29 | }}}, 30 | {"2", args{func() { 31 | t.Logf("ants test %v", 2) 32 | }}}, 33 | {"3", args{func() { 34 | t.Logf("ants test %v", 3) 35 | }}}, 36 | } 37 | for _, tt := range tests { 38 | t.Run(tt.name, func(t *testing.T) { 39 | util.Go(tt.args.f) 40 | }) 41 | } 42 | w := sync.WaitGroup{} 43 | w.Add(1) 44 | w.Wait() 45 | } 46 | 47 | func TestInitGoroutinePool(t *testing.T) { 48 | tests := []struct { 49 | name string 50 | }{ 51 | // TODO: Add test cases. 52 | } 53 | for _, tt := range tests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | util.InitGoroutinePool() 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /util/random.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "math/rand" 4 | 5 | //*************************************************** 6 | //@Link https://github.com/thkhxm/tgf 7 | //@Link https://gitee.com/timgame/tgf 8 | //@QQ群 7400585 9 | //author tim.huang 10 | //@Description 11 | //2023/12/11 12 | //*************************************************** 13 | 14 | type number interface { 15 | ~int | ~int8 | ~int16 | ~int32 | ~int64 | 16 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 17 | } 18 | 19 | func RandNumber[T number](min, max T) T { 20 | if min == max { 21 | return min 22 | } 23 | return T(rand.Int63n(int64(max)-int64(min)) + int64(min)) 24 | } 25 | -------------------------------------------------------------------------------- /util/random_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "github.com/thkhxm/tgf/util" 5 | "testing" 6 | ) 7 | 8 | //*************************************************** 9 | //@Link https://github.com/thkhxm/tgf 10 | //@Link https://gitee.com/timgame/tgf 11 | //@QQ群 7400585 12 | //author tim.huang 13 | //@Description 14 | //2023/12/11 15 | //*************************************************** 16 | 17 | func BenchmarkRandNumberReturnsNumberWithinRange(b *testing.B) { 18 | min := 1 19 | max := 10 20 | for i := 0; i < b.N; i++ { 21 | result := util.RandNumber[int](min, max) 22 | if result < min || result > max { 23 | b.Errorf("Expected number between %d and %d, got %d", min, max, result) 24 | } 25 | } 26 | } 27 | 28 | func TestRandNumberReturnsMinWhenMinEqualsMax(t *testing.T) { 29 | result := util.RandNumber[int64](5, 5) 30 | if result != 5 { 31 | t.Errorf("Expected 5, got %d", result) 32 | } 33 | } 34 | 35 | func TestRandNumberReturnsNumberWithinRange(t *testing.T) { 36 | min := 1 37 | max := 10 38 | result := util.RandNumber[int](min, max) 39 | if result < min || result > max { 40 | t.Errorf("Expected number between %d and %d, got %d", min, max, result) 41 | } 42 | } 43 | 44 | func TestRandNumberReturnsDifferentNumbers(t *testing.T) { 45 | min := 1 46 | max := 100 47 | result1 := util.RandNumber[int](min, max) 48 | result2 := util.RandNumber[int](min, max) 49 | if result1 == result2 { 50 | t.Errorf("Expected different numbers, got %d and %d", result1, result2) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /util/reflect.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "reflect" 4 | 5 | //*************************************************** 6 | //@Link https://github.com/thkhxm/tgf 7 | //@Link https://gitee.com/timgame/tgf 8 | //@QQ群 7400585 9 | //author tim.huang 10 | //@Description 11 | //2023/4/11 12 | //*************************************************** 13 | 14 | func ReflectType[T any]() reflect.Type { 15 | var t T 16 | v := reflect.ValueOf(t) 17 | if v.IsNil() { 18 | v = reflect.New(v.Type().Elem()) 19 | } 20 | return v.Type().Elem() 21 | } 22 | -------------------------------------------------------------------------------- /util/slice.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | //*************************************************** 4 | //@Link https://github.com/thkhxm/tgf 5 | //@Link https://gitee.com/timgame/tgf 6 | //@QQ群 7400585 7 | //author tim.huang 8 | //@Description 9 | //2024/1/9 10 | //*************************************************** 11 | 12 | // SliceDeduplication 13 | // @Description: 去重 14 | // @param s 15 | // @return []S 16 | func SliceDeduplication[S comparable](s []S) []S { 17 | m := make(map[S]bool) 18 | for _, v := range s { 19 | m[v] = true 20 | } 21 | s = make([]S, 0, len(m)) 22 | for k, _ := range m { 23 | s = append(s, k) 24 | } 25 | return s 26 | } 27 | -------------------------------------------------------------------------------- /util/string.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "github.com/bytedance/sonic" 6 | "strconv" 7 | "unsafe" 8 | ) 9 | 10 | //*************************************************** 11 | //@Link https://github.com/thkhxm/tgf 12 | //@Link https://gitee.com/timgame/tgf 13 | //@QQ群 7400585 14 | //author tim.huang 15 | //@Description 16 | //2023/2/24 17 | //*************************************************** 18 | 19 | // StrToAny 20 | // 21 | // @Description: string转任意类型 22 | // @param a 23 | // @return T 24 | // @return error 25 | func StrToAny[T any](a string) (T, error) { 26 | var t T 27 | switch any(t).(type) { 28 | case bool: 29 | v, err := strconv.ParseBool(a) 30 | if err != nil { 31 | return t, err 32 | } 33 | t = any(v).(T) 34 | case int, uint, int32, uint32: 35 | v, err := strconv.ParseInt(a, 10, 32) 36 | if err != nil { 37 | return t, err 38 | } 39 | t = *(*T)(unsafe.Pointer(&v)) 40 | case int64, uint64: 41 | v, err := strconv.ParseInt(a, 10, 64) 42 | if err != nil { 43 | return t, err 44 | } 45 | t = *(*T)(unsafe.Pointer(&v)) 46 | case float64: 47 | v, err := strconv.ParseFloat(a, 64) 48 | if err != nil { 49 | return t, err 50 | } 51 | t = *(*T)(unsafe.Pointer(&v)) 52 | case float32: 53 | v, err := strconv.ParseFloat(a, 32) 54 | v32 := float32(v) 55 | if err != nil { 56 | return t, err 57 | } 58 | t = *(*T)(unsafe.Pointer(&v32)) 59 | case string: 60 | v := a 61 | t = any(v).(T) 62 | case interface{}: 63 | err := sonic.Unmarshal([]byte(a), &t) 64 | if err != nil { 65 | return t, err 66 | } 67 | default: 68 | return t, fmt.Errorf("the type %T is not supported", t) 69 | } 70 | return t, nil 71 | } 72 | 73 | // AnyToStr 74 | // 75 | // @Description: 任意数据转换成字符串,默认结构化数据使用json序列化 76 | // @param a 77 | // @return string 78 | // @return error 79 | func AnyToStr(a interface{}) (string, error) { 80 | switch a.(type) { 81 | case bool: 82 | return strconv.FormatBool(a.(bool)), nil 83 | case int32: 84 | return strconv.FormatInt(int64(a.(int32)), 10), nil 85 | case int: 86 | return strconv.FormatInt(int64(a.(int)), 10), nil 87 | case int64: 88 | return strconv.FormatInt(a.(int64), 10), nil 89 | case float32: 90 | return strconv.FormatFloat(float64(a.(float32)), 'f', -1, 32), nil 91 | case float64: 92 | return strconv.FormatFloat(a.(float64), 'f', -1, 64), nil 93 | case string: 94 | return a.(string), nil 95 | case interface{}: 96 | js, _ := sonic.Marshal(a) 97 | return ConvertStringByByteSlice(js), nil 98 | default: 99 | return "", fmt.Errorf("the type %T is not supported", a) 100 | } 101 | } 102 | 103 | // ConvertStringByByteSlice 104 | // @Description: 字节转字符串 105 | // @param bytes 106 | // @return string 107 | // 108 | //go:inline 109 | func ConvertStringByByteSlice(bytes []byte) string { 110 | return *(*string)(unsafe.Pointer(&bytes)) 111 | } 112 | 113 | func StringToSliceByte(s string) []byte { 114 | x := (*[2]uintptr)(unsafe.Pointer(&s)) 115 | h := [3]uintptr{x[0], x[1], x[1]} 116 | return *(*[]byte)(unsafe.Pointer(&h)) 117 | } 118 | 119 | func CopyMeta(src, dst map[string]string) { 120 | if dst == nil { 121 | return 122 | } 123 | for k, v := range src { 124 | dst[k] = v 125 | } 126 | } 127 | 128 | func RemoveOneKey(s []string, r string) []string { 129 | for i, v := range s { 130 | if v == r { 131 | return append(s[:i], s[i+1:]...) 132 | } 133 | } 134 | return s 135 | } 136 | 137 | func RemoveAllKey(s []string, r string) []string { 138 | res := []string{} 139 | for _, v := range s { 140 | if v != r { 141 | res = append(res, v) 142 | } 143 | } 144 | return res 145 | } 146 | -------------------------------------------------------------------------------- /util/string_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "github.com/bytedance/sonic" 5 | "github.com/thkhxm/tgf/util" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | //*************************************************** 11 | //@Link https://github.com/thkhxm/tgf 12 | //@Link https://gitee.com/timgame/tgf 13 | //@QQ群 7400585 14 | //author tim.huang 15 | //@Description 16 | //2023/2/24 17 | //*************************************************** 18 | 19 | func TestStrToAny(t *testing.T) { 20 | type args struct { 21 | a string 22 | } 23 | type testCase[T any] struct { 24 | name string 25 | args args 26 | want T 27 | wantErr bool 28 | } 29 | ddd := &StringDemoType{Name: "333"} 30 | arg, _ := sonic.Marshal(ddd) 31 | tests := []testCase[StringDemoType]{ 32 | {name: "1", args: args{string(arg)}, want: *ddd, wantErr: false}, 33 | } 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | got, err := util.StrToAny[StringDemoType](tt.args.a) 37 | if (err != nil) != tt.wantErr { 38 | t.Errorf("StrToAny() error = %v, wantErr %v", err, tt.wantErr) 39 | return 40 | } 41 | if !reflect.DeepEqual(got, tt.want) { 42 | t.Errorf("StrToAny() got = %v, want %v", got, tt.want) 43 | } 44 | }) 45 | } 46 | } 47 | func TestRecover(t *testing.T) { 48 | //s, _ := util.util.StrToAny[float64]("0.38") 49 | //t.Logf("-->%v", s) 50 | //var ( 51 | // a int = 1 52 | // b int32 = 1 53 | // c int64 = 1 54 | // d float32 = 0.38 55 | // e float64 = 0.38 56 | // f bool = true 57 | // g StringDemoType = *new(StringDemoType) 58 | //) 59 | //g.Name = "tim" 60 | //a0, _ := util.AnyToStr(a) 61 | //a1, _ := util.AnyToStr(b) 62 | //a2, _ := util.AnyToStr(c) 63 | //a3, _ := util.AnyToStr(d) 64 | //a4, _ := util.AnyToStr(e) 65 | //a5, _ := util.AnyToStr(f) 66 | //a6, _ := util.AnyToStr(g) 67 | //t.Log("--->", a0) 68 | //t.Log("--->", a1) 69 | //t.Log("--->", a2) 70 | //t.Log("--->", a3) 71 | //t.Log("--->", a4) 72 | //t.Log("--->", a5) 73 | //t.Log("--->", a6) 74 | dd, _ := util.StrToAny[*StringDemoType]("{\"Name\":\"tim\"}") 75 | t.Log("--->", dd) 76 | } 77 | func TestStrToAnyBool(t *testing.T) { 78 | tests := []struct { 79 | name string 80 | arg string 81 | want bool 82 | wantErr bool 83 | }{ 84 | {"valid true", "true", true, false}, 85 | {"valid false", "false", false, false}, 86 | {"invalid bool", "notabool", false, true}, 87 | } 88 | for _, tt := range tests { 89 | t.Run(tt.name, func(t *testing.T) { 90 | got, err := util.StrToAny[bool](tt.arg) 91 | if (err != nil) != tt.wantErr { 92 | t.Errorf("StrToAny() error = %v, wantErr %v", err, tt.wantErr) 93 | return 94 | } 95 | if got != tt.want { 96 | t.Errorf("StrToAny() got = %v, want %v", got, tt.want) 97 | } 98 | }) 99 | } 100 | } 101 | func TestStrToAnyInt(t *testing.T) { 102 | tests := []struct { 103 | name string 104 | arg string 105 | want int 106 | wantErr bool 107 | }{ 108 | {"valid int", "123", 123, false}, 109 | {"invalid int", "notanint", 0, true}, 110 | } 111 | for _, tt := range tests { 112 | t.Run(tt.name, func(t *testing.T) { 113 | got, err := util.StrToAny[int](tt.arg) 114 | if (err != nil) != tt.wantErr { 115 | t.Errorf("StrToAny() error = %v, wantErr %v", err, tt.wantErr) 116 | return 117 | } 118 | if got != tt.want { 119 | t.Errorf("StrToAny() got = %v, want %v", got, tt.want) 120 | } 121 | }) 122 | } 123 | } 124 | func TestStrToAnyFloat64(t *testing.T) { 125 | tests := []struct { 126 | name string 127 | arg string 128 | want float64 129 | wantErr bool 130 | }{ 131 | {"valid float", "123.456", 123.456, false}, 132 | {"invalid float", "notafloat", 0, true}, 133 | } 134 | for _, tt := range tests { 135 | t.Run(tt.name, func(t *testing.T) { 136 | got, err := util.StrToAny[float64](tt.arg) 137 | if (err != nil) != tt.wantErr { 138 | t.Errorf("StrToAny() error = %v, wantErr %v", err, tt.wantErr) 139 | return 140 | } 141 | if got != tt.want { 142 | t.Errorf("StrToAny() got = %v, want %v", got, tt.want) 143 | } 144 | }) 145 | } 146 | } 147 | func TestStrToAnyFloat32(t *testing.T) { 148 | tests := []struct { 149 | name string 150 | arg string 151 | want float32 152 | wantErr bool 153 | }{ 154 | {"valid float", "123.456", 123.456, false}, 155 | {"invalid float", "notafloat", 0, true}, 156 | } 157 | for _, tt := range tests { 158 | t.Run(tt.name, func(t *testing.T) { 159 | got, err := util.StrToAny[float32](tt.arg) 160 | if (err != nil) != tt.wantErr { 161 | t.Errorf("StrToAny() error = %v, wantErr %v", err, tt.wantErr) 162 | return 163 | } 164 | if got != tt.want { 165 | t.Errorf("StrToAny() got = %v, want %v", got, tt.want) 166 | } 167 | }) 168 | } 169 | } 170 | func TestStrToAnyString(t *testing.T) { 171 | tests := []struct { 172 | name string 173 | arg string 174 | want string 175 | wantErr bool 176 | }{ 177 | {"valid string", "hello", "hello", false}, 178 | } 179 | for _, tt := range tests { 180 | t.Run(tt.name, func(t *testing.T) { 181 | got, err := util.StrToAny[string](tt.arg) 182 | if (err != nil) != tt.wantErr { 183 | t.Errorf("StrToAny() error = %v, wantErr %v", err, tt.wantErr) 184 | return 185 | } 186 | if got != tt.want { 187 | t.Errorf("StrToAny() got = %v, want %v", got, tt.want) 188 | } 189 | }) 190 | } 191 | } 192 | func TestStrToAnyStruct(t *testing.T) { 193 | type MyStruct struct { 194 | Name string 195 | } 196 | tests := []struct { 197 | name string 198 | arg string 199 | want MyStruct 200 | wantErr bool 201 | }{ 202 | {"valid struct", "{\"Name\":\"test\"}", MyStruct{Name: "test"}, false}, 203 | {"invalid struct", "notastruct", MyStruct{}, true}, 204 | } 205 | for _, tt := range tests { 206 | t.Run(tt.name, func(t *testing.T) { 207 | got, err := util.StrToAny[MyStruct](tt.arg) 208 | if (err != nil) != tt.wantErr { 209 | t.Errorf("StrToAny() error = %v, wantErr %v", err, tt.wantErr) 210 | return 211 | } 212 | if !reflect.DeepEqual(got, tt.want) { 213 | t.Errorf("StrToAny() got = %v, want %v", got, tt.want) 214 | } 215 | }) 216 | } 217 | } 218 | 219 | type StringDemoType struct { 220 | Name string 221 | } 222 | -------------------------------------------------------------------------------- /util/timer.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | //*************************************************** 4 | //@Link https://github.com/thkhxm/tgf 5 | //@Link https://gitee.com/timgame/tgf 6 | //@QQ群 7400585 7 | //author tim.huang 8 | //@Description 9 | //2023/3/17 10 | //*************************************************** 11 | 12 | var m *timerManager 13 | 14 | type timerManager struct { 15 | } 16 | 17 | func AddTicker(tag string) { 18 | 19 | } 20 | -------------------------------------------------------------------------------- /util/uuid.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/bwmarrin/snowflake" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | //*************************************************** 10 | //@Link https://github.com/thkhxm/tgf 11 | //@Link https://gitee.com/timgame/tgf 12 | //@QQ群 7400585 13 | //author tim.huang 14 | //@Description 15 | //2023/2/25 16 | //*************************************************** 17 | 18 | var Snowflake *snowflake.Node 19 | 20 | func GenerateSnowflakeId() string { 21 | return Snowflake.Generate().Base64() 22 | } 23 | 24 | func initSnowFlake() { 25 | //初始化雪花算法Id 26 | source := rand.NewSource(time.Now().UnixNano()) 27 | ran := rand.New(source) 28 | Snowflake, _ = snowflake.NewNode(ran.Int63n(1024)) 29 | } 30 | 31 | var codes = []string{"0", "1", "2", "3", "4", "5", 32 | "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", 33 | "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", 34 | "W", "X", "Y", "Z"} 35 | 36 | // 37 | //func GenerateKey(count int) []string { 38 | // var () 39 | // mod := len(codes) 40 | // res := make([]string, count) 41 | // index := 0 42 | // for { 43 | // if index >= count { 44 | // break 45 | // } 46 | // fid := GenerateSnowflakeId() 47 | // 48 | // // Check if fid length is less than 32, if so, append "0" to make it 32 characters long 49 | // if len(fid) < 32 { 50 | // c := 32 - len(fid) 51 | // for i := 0; i < c; i++ { 52 | // fid += codes[rand.Int31n(int32(len(codes)))] 53 | // } 54 | // } 55 | // 56 | // //fid = strings.ReplaceAll(fid, "-", "") 57 | // s := strings.Builder{} 58 | // for i := 0; i < 8; i++ { 59 | // subStr := fid[i*4 : i*4+4] 60 | // x, _ := strconv.ParseInt(subStr, 16, 32) 61 | // subIndex := x % int64(mod) 62 | // item := codes[subIndex] 63 | // s.WriteString(item) 64 | // } 65 | // //fmt.Println("code->", s.String()) 66 | // res[index] = s.String() 67 | // index++ 68 | // } 69 | // return res 70 | //} 71 | -------------------------------------------------------------------------------- /util/uuid_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/cornelk/hashmap" 6 | "github.com/thkhxm/tgf/util" 7 | "sync" 8 | "testing" 9 | ) 10 | 11 | //*************************************************** 12 | //@Link https://github.com/thkhxm/tgf 13 | //@Link https://gitee.com/timgame/tgf 14 | //@QQ群 7400585 15 | //author tim.huang 16 | //@Description 17 | //2023/5/17 18 | //*************************************************** 19 | 20 | func TestGenerateSnowflakeId(t *testing.T) { 21 | wg := &sync.WaitGroup{} 22 | m := hashmap.New[string, struct{}]() 23 | size := 10000 24 | wg.Add(size) 25 | for i := 0; i < size; i++ { 26 | go func(i int, wg *sync.WaitGroup) { 27 | defer wg.Done() 28 | id := util.GenerateSnowflakeId() 29 | m.Set(id, struct{}{}) 30 | }(i, wg) 31 | } 32 | wg.Wait() 33 | fmt.Println(m.Len()) 34 | } 35 | 36 | //func TestGenerateKey(t *testing.T) { 37 | // type args struct { 38 | // count int 39 | // } 40 | // tests := []struct { 41 | // name string 42 | // args args 43 | // want []string 44 | // }{ 45 | // {"a", args{10}, nil}, 46 | // } 47 | // for _, tt := range tests { 48 | // t.Run(tt.name, func(t *testing.T) { 49 | // if got := util.GenerateKey(tt.args.count); !reflect.DeepEqual(got, tt.want) { 50 | // t.Errorf("GenerateKey() = %v, want %v", got, tt.want) 51 | // } 52 | // }) 53 | // } 54 | //} 55 | -------------------------------------------------------------------------------- /util/weight.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "golang.org/x/exp/rand" 5 | "time" 6 | ) 7 | 8 | //*************************************************** 9 | //@Link https://github.com/thkhxm/tgf 10 | //@Link https://gitee.com/timgame/tgf 11 | //@QQ群 7400585 12 | //author tim.huang 13 | //@Description 权重通用工具,非线程安全,需要注意 14 | //2023/11/29 15 | //*************************************************** 16 | 17 | type IWeightData[T any] interface { 18 | Data() T 19 | Amount() int32 20 | Ratio() int32 21 | BaseRatio() int32 22 | } 23 | 24 | type IWeightItem[T any] interface { 25 | IWeightData[T] 26 | Hit() (IWeightItem[T], bool) 27 | } 28 | 29 | type IWeight[T any] interface { 30 | // Roll 31 | // @Description: 根据权重随机出一个数据,并且减少物品的数量 32 | // @return T 33 | // 34 | Roll() (res IWeightData[T]) 35 | // AllItem 36 | // @Description: 这里不会进行切片的拷贝,所以在使用的时候需要注意 37 | // @return IWeightItem[T] 38 | // 39 | AllItem() []IWeightData[T] 40 | 41 | TotalRatio() int32 42 | BaseRatio() int32 43 | BaseAmount() int32 44 | Len() int 45 | 46 | OnlyRoll() (res IWeightData[T]) 47 | UpdateItemStock(data IWeightData[T]) 48 | } 49 | 50 | type IWeightBuilder[T any] interface { 51 | AddWeight(weightRatio, amount int32, data T) IWeightBuilder[T] 52 | Build() IWeight[T] 53 | Seed(seed uint64) IWeightBuilder[T] 54 | } 55 | 56 | type weight[T any] struct { 57 | ratio int32 58 | data T 59 | amount int32 60 | } 61 | 62 | type weightOperation[T any] struct { 63 | weights []IWeightItem[T] 64 | totalRatio int32 65 | baseRatio int32 66 | baseAmount int32 67 | // 68 | ran *rand.Rand 69 | } 70 | 71 | type weightBuilder[T any] struct { 72 | weights []IWeightItem[T] 73 | seed uint64 74 | } 75 | 76 | //----------------------------------- 77 | 78 | func (w *weight[T]) Data() T { 79 | return w.data 80 | } 81 | 82 | func (w *weight[T]) Amount() int32 { 83 | return w.amount 84 | } 85 | 86 | func (w *weight[T]) Ratio() int32 { 87 | if w.amount == 0 { 88 | return 0 89 | } 90 | return w.ratio 91 | } 92 | 93 | func (w *weight[T]) BaseRatio() int32 { 94 | return w.ratio 95 | } 96 | 97 | func (w *weight[T]) Hit() (IWeightItem[T], bool) { 98 | //如果数量小于0,则表示该权重无限制 99 | if w.amount < 0 { 100 | return w, false 101 | } 102 | 103 | if w.amount > 0 { 104 | w.amount-- 105 | //避免因为并发导致的负数 106 | if w.amount < 0 { 107 | w.amount = 0 108 | } 109 | return w, w.amount == 0 110 | } 111 | return nil, false 112 | } 113 | 114 | func (w *weightOperation[T]) Roll() (res IWeightData[T]) { 115 | res = w.OnlyRoll() 116 | w.UpdateItemStock(res) 117 | return 118 | } 119 | 120 | // OnlyRoll 121 | // @Description: 只命中,但是不对数量和权重做变更 122 | // @receiver w 123 | // @return res 124 | func (w *weightOperation[T]) OnlyRoll() (res IWeightData[T]) { 125 | if w.totalRatio <= 0 { 126 | return 127 | } 128 | r := w.ran.Int31n(w.totalRatio) 129 | for _, wei := range w.weights { 130 | if r < wei.Ratio() { 131 | return wei 132 | } 133 | r -= wei.Ratio() 134 | } 135 | return 136 | 137 | } 138 | 139 | // UpdateItemStock 140 | // @Description: 变更权重和数量 141 | // @receiver w 142 | // @param data 143 | func (w *weightOperation[T]) UpdateItemStock(data IWeightData[T]) { 144 | item := data.(IWeightItem[T]) 145 | if _, done := item.Hit(); done { 146 | w.totalRatio -= item.BaseRatio() 147 | } 148 | } 149 | 150 | func (w *weightOperation[T]) AllItem() []IWeightData[T] { 151 | res := make([]IWeightData[T], 0, len(w.weights)) 152 | for _, wei := range w.weights { 153 | res = append(res, wei) 154 | } 155 | return res 156 | } 157 | 158 | func (w *weightOperation[T]) Len() int { 159 | return len(w.weights) 160 | } 161 | func (w *weightOperation[T]) TotalRatio() int32 { 162 | return w.totalRatio 163 | } 164 | 165 | func (w *weightOperation[T]) BaseRatio() int32 { 166 | return w.baseRatio 167 | } 168 | 169 | func (w *weightOperation[T]) BaseAmount() int32 { 170 | return w.baseAmount 171 | } 172 | 173 | func (w *weightBuilder[T]) Seed(seed uint64) IWeightBuilder[T] { 174 | w.seed = seed 175 | return w 176 | } 177 | 178 | func (w *weightBuilder[T]) Build() IWeight[T] { 179 | operation := &weightOperation[T]{weights: w.weights} 180 | for _, wei := range w.weights { 181 | operation.totalRatio += wei.Ratio() 182 | if wei.Amount() > 0 { 183 | operation.baseAmount += wei.Amount() 184 | } 185 | } 186 | operation.baseRatio = operation.totalRatio 187 | if w.seed == 0 { 188 | w.seed = uint64(time.Now().UnixMilli()) 189 | } 190 | //自定义随机数种子 191 | operation.ran = rand.New(rand.NewSource(w.seed)) 192 | 193 | return operation 194 | } 195 | func (w *weightBuilder[T]) AddWeight(weightRatio, amount int32, data T) IWeightBuilder[T] { 196 | if weightRatio <= 0 { 197 | return w 198 | } 199 | w.weights = append(w.weights, &weight[T]{ratio: weightRatio, amount: amount, data: data}) 200 | return w 201 | } 202 | 203 | func NewWeightBuilder[T any]() IWeightBuilder[T] { 204 | return &weightBuilder[T]{weights: make([]IWeightItem[T], 0)} 205 | } 206 | -------------------------------------------------------------------------------- /util/weight_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "github.com/thkhxm/tgf/util" 5 | "testing" 6 | ) 7 | 8 | //*************************************************** 9 | //@Link https://github.com/thkhxm/tgf 10 | //@Link https://gitee.com/timgame/tgf 11 | //@QQ群 7400585 12 | //author tim.huang 13 | //@Description 14 | //2023/11/29 15 | //*************************************************** 16 | 17 | func Test_weightOperation_Roll(t *testing.T) { 18 | builder := util.NewWeightBuilder[int32]().Seed(1001) 19 | builder.AddWeight(10, 10, 1) 20 | builder.AddWeight(20, 10, 2) 21 | builder.AddWeight(30, 1, 3) 22 | builder.AddWeight(40, 10, 4) 23 | builder.AddWeight(50, 10, 5) 24 | w := builder.Build() 25 | for i := 0; i < 5; i++ { 26 | t.Logf("roll number : %d", w.Roll()) 27 | } 28 | 29 | //=== RUN Test_weightOperation_Roll 30 | //weight_test.go:26: roll number : &{50 5 9} 31 | //weight_test.go:26: roll number : &{30 3 0} 32 | //weight_test.go:26: roll number : &{50 5 8} 33 | //weight_test.go:26: roll number : &{40 4 9} 34 | //weight_test.go:26: roll number : &{40 4 8} 35 | //--- PASS: Test_weightOperation_Roll (0.00s) 36 | } 37 | --------------------------------------------------------------------------------