• 游戏服务端配置“热更”及“秒启动”终极方案(golang/ygluu/卢益贵)


    游戏服务端配置“热更”及“秒启动”终极方案(golang/ygluu/卢益贵)

    关键词:游戏微服务架构、游戏服务端热更、模块化解耦、golang

    一、前言

    众所周知,游戏服务端配置信息热更有几大问题(非lua架构):

    1、因配置对象的指针被场景对象引用而导致热更复杂度提高

    2、信息量大的配置表热更导致游戏卡顿、玩家闪断

    3、一般重载后的配置信息仅影响重载后新创建的对应场景对象,不能影响已存在的场景对象

    4、在高度解耦的模块化开发模式下导致热更复杂度提高

    本示例代码将使用通用方法来演示在“高度解耦、模块化、模板化”的开发模式下对上述问题的解决方案,并提出游戏服务器秒启动的辅助方案。同时给出了完整示例代码及下载连接(见后)。

    二、异步线程加载/重载方案

    异步加载可以完美解决主线程重载大配置文件时可能引起游戏卡顿的现象。

    图1

    三、配置表碎片化方案

    由多行电子表格或者xml格式自动导出一行对应一个ini文件的ini格式,重载时可大大减少重载时间。当然,采用异步重载的话可以不考虑此项方案。另外此项方案可应用于游戏秒启动,游戏启动时仅加载必要配置,场景等玩家相关的配置,在玩家登录进入场景以后创建场景对象时再在主线程里同步加载碎片化的配置,碎片化的配置加载时所需时间极短,玩家几乎没有太明显的卡断感知

    当然秒启动还有一个重要方案:主从机方案,即采用主从机方式,在主机崩溃释放所占用端口号后,从机立即夺得相同端口的控制权,从机角色瞬间转变为主机角色

    图2

    四、指针间接引用方案

    配置对象Cfg引用配置项Item,场景对象Map引用配置对象Cfg,每次Map使用配置信息时间接访问Cfg.Item。配置加载模块执行重载后,在主线程的回调中重置Cfg对象的Item字段,这样既不不影响主线程复杂的场景应用逻辑,又能让已存在的场景对象Map更新到最新的配置信息。

    图3

    五、重载通知方案

    针对部分需要复制配置信息的场景对象,通过主线程同步执行重载回调函数OnReload,这样每个场景对象实例Map1~N都能及时更新到最新的配置信息。

    图4

    六、示例代码

    (一)示例代码下载连接及运行效果

    1、示例代码(golang)下载链接:

    【免费】游戏服务端配置“热更”终极方案(golang/ygluu/卢益贵)资源-CSDN文库icon-default.png?t=N7T8https://download.csdn.net/download/GuestCode/88977874

    2、示例代码运行效果

    (二)示例代码结构及源码文件

    1、示例代码模块化目录结构

    2、主线程队列代码

    主线程队列采用了匿名函数队列

    1. package queue
    2. /*******************************************************************************
    3. Author: Yigui Lu (卢益贵)
    4. Contact WX/QQ: 48092788
    5. Blog: https://blog.csdn.net/guestcode
    6. Creation by: 2024-3-16
    7. *******************************************************************************/
    8. // 主线程队列(匿名函数),仅主线程调用,无需同步
    9. var MainQ = make(chan func())
    10. // 主线程事件响应函数列表
    11. var events = make(map[string]func())
    12. // 触发主线程事件
    13. func TriggerEvent(name string) {
    14. MainQ <- func() {
    15. on := events[name]
    16. if on != nil {
    17. on()
    18. }
    19. }
    20. }
    21. // 设置事件监听(在主线程中执行onEvent)
    22. func ListenEvent(name string, onEvent func()) {
    23. events[name] = onEvent
    24. }

    3、主线程代码

    1. package main
    2. /*******************************************************************************
    3. Author: Yigui Lu (卢益贵)
    4. Contact WX/QQ: 48092788
    5. Blog: https://blog.csdn.net/guestcode
    6. Creation by: 2024-3-16
    7. *******************************************************************************/
    8. import (
    9. _ "cfgload/mapcfg"
    10. _ "cfgload/mapmgr"
    11. "cfgload/queue"
    12. )
    13. func main() {
    14. // 模拟游戏主线程
    15. for {
    16. // 匿名函数队列
    17. proc := <-queue.MainQ
    18. // 出列并执行
    19. proc()
    20. }
    21. }

    4、地图配置模块示例代码(加载/重载)

    1. package mapcfg
    2. /*******************************************************************************
    3. Author: Yigui Lu (卢益贵)
    4. Contact WX/QQ: 48092788
    5. Blog: https://blog.csdn.net/guestcode
    6. Creation by: 2024-3-16
    7. *******************************************************************************/
    8. // 本配置单元可以作为配置模板加载所有配置
    9. import (
    10. "cfgload/queue"
    11. "io/ioutil"
    12. "log"
    13. "time"
    14. )
    15. const ModuleName = "地图配置模块"
    16. // 地图配置项,一个对应一个
    17. type Item struct {
    18. Name string // 地图名
    19. ResName string // 地址资源名
    20. ResData []byte // 资源数据
    21. Attrs []int // 地图属性
    22. }
    23. // 外部类型:地图配置类
    24. type Cfg struct {
    25. Item *Item // 真正的配置对象是Item
    26. LoadCount int // 加载次数,演示用
    27. onReloads []func() // 触发重载回调,部分场景需求用
    28. }
    29. func (c *Cfg) AddOnReload(onReload func()) {
    30. c.onReloads = append(c.onReloads, onReload)
    31. }
    32. // 配置表
    33. type cfgs = map[string]*Cfg
    34. // 配置表(全局变量/外部接口,仅主线程调用无需同步)
    35. var Cfgs cfgs
    36. // 配置异步加载协程
    37. var loadcount int
    38. // 异步加载/重载通用函数,无需两者区分而额外编写代码
    39. func loadFunc(ch chan bool) {
    40. for {
    41. // 等待加载通知
    42. <-ch
    43. // 以下在加载线程中执行 -->
    44. isReload := Cfgs != nil
    45. loadcount++
    46. if isReload {
    47. log.Println("地图配置模块(加载线程) => 正在重载......", loadcount)
    48. } else {
    49. log.Println("地图配置模块(加载线程) => 正在加载......")
    50. }
    51. var items []*Item
    52. for {
    53. // 模拟配置文件加载过程
    54. item := &Item{
    55. Name: "南天门",
    56. ResName: "res001.map",
    57. }
    58. item.ResData, _ = ioutil.ReadFile(item.ResName)
    59. items = append(items, item)
    60. break // 仅加载一行
    61. }
    62. // <-- 以上在加载线程中执行
    63. // 匿名函数将在主线程执行,达到线程同步目的
    64. onModuleLoadedInMain := func() {
    65. if Cfgs == nil {
    66. // 首次加载
    67. Cfgs = make(cfgs)
    68. }
    69. for _, item := range items {
    70. cfg := Cfgs[item.Name]
    71. if cfg == nil {
    72. // 新增配置
    73. cfg = &Cfg{}
    74. cfg.Item = item
    75. cfg.LoadCount = 1
    76. Cfgs[item.Name] = cfg
    77. } else {
    78. // 替换旧的配置
    79. cfg.Item = item
    80. cfg.LoadCount++
    81. // 在主线程调用重载通知回调(只有部分场景需求才需要通知)
    82. for _, onReload := range cfg.onReloads {
    83. onReload()
    84. }
    85. }
    86. }
    87. }
    88. if isReload {
    89. log.Println("地图配置模块(加载线程) => 重载完毕", loadcount)
    90. } else {
    91. log.Println("地图配置模块(加载线程) => 加载完毕")
    92. }
    93. // 给主消息队列压栈
    94. // 主线程会调用onModuleLoadedInMain执行,达到线程同步目的
    95. queue.MainQ <- onModuleLoadedInMain
    96. // 首次加载,触发事件告诉主线程模块加载完毕
    97. if !isReload {
    98. queue.TriggerEvent(ModuleName)
    99. }
    100. }
    101. }
    102. func reloadTrigger(ch chan bool) {
    103. // 模拟5秒钟触发一次重载请求
    104. for {
    105. ch <- true
    106. time.Sleep(time.Second * 5)
    107. }
    108. }
    109. func init() {
    110. ch := make(chan bool)
    111. go loadFunc(ch)
    112. go reloadTrigger(ch)
    113. }

    5、地图管理模块示例代码(地图对象)

    1. package mapmgr
    2. /*******************************************************************************
    3. Author: Yigui Lu (卢益贵)
    4. Contact WX/QQ: 48092788
    5. Blog: https://blog.csdn.net/guestcode
    6. Creation by: 2024-3-16
    7. *******************************************************************************/
    8. import (
    9. "cfgload/mapcfg"
    10. "cfgload/queue"
    11. "fmt"
    12. "log"
    13. )
    14. type Map struct {
    15. cfg *mapcfg.Cfg
    16. Attrs []int
    17. }
    18. // 间接引用转为直接引用:Map.CfgItem
    19. func (m *Map) CfgItem() *mapcfg.Item {
    20. return m.cfg.Item
    21. }
    22. // 直接引用配置对象字段示例
    23. func (m *Map) Name() string {
    24. return m.cfg.Item.Name
    25. }
    26. // 复制配置信息示例(模拟部分需求场景,无需求则可以省略)
    27. func (m *Map) onReloadCopyInfo() {
    28. const txt = "地图管理模块(主线程) => 复制配置信息,地图名称:%s,加载次数:%d"
    29. log.Println(fmt.Sprintf(txt, m.Name(), m.cfg.LoadCount))
    30. // csdn资源未修复此项bug
    31. m.Attrs = nil
    32. for _, attr := range m.CfgItem().Attrs {
    33. m.Attrs = append(m.Attrs, attr)
    34. }
    35. }
    36. var Maps = make(map[string]*Map)
    37. func createMap() {
    38. log.Println("地图管理模块(主线程) => 正在创建地图......")
    39. for _, cfg := range mapcfg.Cfgs {
    40. mp := &Map{
    41. cfg: cfg,
    42. }
    43. // 无复制配置信息需求时可以省略此步骤
    44. cfg.AddOnReload(mp.onReloadCopyInfo)
    45. // 为避免代码冗余,首次加载自己调用onReloadCopyInfo复制配置信息
    46. mp.onReloadCopyInfo()
    47. }
    48. log.Println("地图管理模块(主线程) => 创建地图完毕")
    49. }
    50. func init() {
    51. // 待地图配置首次加载完毕后创建地图
    52. queue.ListenEvent(mapcfg.ModuleName, createMap)
    53. }

    七、相关连接

    对比脚本型和编译型游戏服务器的热更新方案 - codedump的网络日志icon-default.png?t=N7T8https://www.codedump.info/post/20191206-gameserver-hot-refresh/

    聊聊Golang游戏服务器的热更 | wudaijun's blogicon-default.png?t=N7T8https://wudaijun.com/2022/08/golang-gameserver-hotfix/

    一种基于so的C/C++服务热更新方案_mob604756fea1c5的技术博客_51CTO博客icon-default.png?t=N7T8https://blog.51cto.com/u_15127648/4542189

    游戏开发(九) 之 纯 lua 版 热更新 方案_纯lua的热更新方案-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/zyxjx1314/article/details/106045843

    lua游戏服务器热更新_lua热更函数但不修改变量-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/peter_teng/article/details/52751231

    游戏开发中的热更新:一_热更新从服务器上下载的是什么文件-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/weixin_40695640/article/details/129463767

  • 相关阅读:
    C语言笔记第03章:数组
    Springboot+JWT+Redis实现登陆登出功能
    性能测试 —— Jmeter分布式测试的注意事项和常见问题
    tcpdump 如何使用
    Zookeeper (四) --------- 服务器动态上下线监听案例
    R语言地理加权回归、主成份分析、判别分析等空间异质性数据分析
    gentool gen go自动生成表结构
    Vue项目引入百度统计的正确操作步骤,亲测有效!
    算法练习第六十四天
    基于大仓库的微服务差异化构建工具
  • 原文地址:https://blog.csdn.net/guestcode/article/details/136769600