├── BlogArticle ├── 原来这就是网络 │ ├── README.md │ ├── 物理层-1.png │ ├── 物理层-2.png │ └── 物理层-3.png ├── 史上最全后台开发成长指南 │ └── README.md ├── 后台工程师职业能力提升之道 │ └── README.md ├── 如何画好架构图:7种常用类型与示例 │ ├── README.md │ ├── image-1.png │ ├── image-2.png │ └── image.png ├── 如何给GameServer做压测 │ └── README.md ├── 带你通俗易懂的了解进程、线程和协程 │ ├── README.md │ ├── image-1.png │ ├── image-2.png │ └── image.png ├── 有栈协程与无栈协程 │ └── README.md ├── 服务端开发必备:9大性能优化秘技 │ ├── README.md │ └── 服务端开发必备:9大性能优化秘技.png ├── 深入理解Linux的TCP三次握手 │ └── README.md ├── 游戏推荐业务中基于Sentinel的动态限流实践 │ └── README.md ├── 聊聊布隆过滤器-Go语言实践篇 │ └── README.md ├── 英语进阶指南 │ └── README.md ├── 转转门店基于MQ的Http重试实践 │ └── README.md └── 通过实例理解Web应用用户密码存储方案 │ └── README.md ├── Book ├── TCP-IP详解1-卷1-协议 │ └── README.md ├── 代码大全-第2版-纪念版 │ └── README.md └── 现代操作系统-第4版 │ └── README.md ├── Essays ├── Go接口相关的优化技巧 │ └── README.md ├── ProtoJson可能会限制表设计 │ └── README.md ├── Redis多机部署 │ ├── README.md │ ├── image1.png │ ├── image2.png │ ├── image3.png │ └── image4.png ├── 关于Actor模型的思考 │ └── README.md ├── 初级Go工程师 │ ├── 容器 │ │ └── Map │ │ │ └── README.md │ ├── 泛型 │ │ └── README.md │ ├── 深拷贝与浅拷贝 │ │ ├── README.md │ │ └── image.png │ └── 类型转换 │ │ └── README.md ├── 浅谈游戏ECS架构 │ └── README.md ├── 游戏冷启动阶段后端配置热更 │ ├── README.md │ └── image.png ├── 游戏架构中为啥远程服务调用选型gRPC │ └── README.md ├── 游戏登录系统设计 │ ├── README.md │ └── image.png └── 部署游戏后端,win下使用docker遇见的坑 │ ├── README.md │ └── image.png ├── Pictures ├── Code Complete, 2nd Edition.jpg ├── Computer Systems- A Programmer's Perspective.jpg ├── Modern Operating Systems (4th Edition).jpg ├── Operating Systems- Three Easy Pieces.jpg ├── TCP-IP Illustrated- The Protocols, Volume 1.jpg └── Tencent Game Development Essentials II.jpg └── README.md /BlogArticle/原来这就是网络/README.md: -------------------------------------------------------------------------------- 1 | 链接:https://www.cnblogs.com/flashsun/p/14266148.html 2 | 3 | ## 个人打分 4 | ⭐️⭐️⭐️⭐️⭐️ 5 | 6 | ## 阅读时间 7 | 2024/10/24 - 2024/10/24 8 | 9 | ## 读书心得 10 | 11 | ### 物理层 12 | 通过一根网线,实现两台电脑互相通讯,网线插在电脑的网口上。三台电脑互相通讯时,每台电脑都需要两个网口。 13 | 假如五台电脑呢?每台电脑都需要四个网口。这是不现实的。 14 | ![](./物理层-1.png) 15 | 16 | 于是你发明了一个设备,**集线器**。有了集线器,网线变得简洁多了~ 17 | 发送数据,发给集线器即可,集线器会无脑的将电信号广播到所有插网线到它的电脑,不做任何处理。它笨笨的,所以把它定性在**物理层**。 18 | 19 | ![](./物理层-2.png) 20 | 假如,A 给 B 发数据,数据包如何知道,是不是发给我的呢? 21 | 给电脑起个名字吧!这个名字需要全局唯一,你把这个名字称为 **MAC 地址**。 22 | 有名字以后,A 给 B 发数据时,在数据包中拼接一个头部,就可以了。 23 | 24 | ![](./物理层-3.png) 25 | 26 | 新的问题出现了,A 只需要给 B 发数据,现在发给集线器以后,所有电脑都会收到,这不安全,也不节省网络资源。 27 | 28 | ### 链路层 29 | -------------------------------------------------------------------------------- /BlogArticle/原来这就是网络/物理层-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/BlogArticle/原来这就是网络/物理层-1.png -------------------------------------------------------------------------------- /BlogArticle/原来这就是网络/物理层-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/BlogArticle/原来这就是网络/物理层-2.png -------------------------------------------------------------------------------- /BlogArticle/原来这就是网络/物理层-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/BlogArticle/原来这就是网络/物理层-3.png -------------------------------------------------------------------------------- /BlogArticle/史上最全后台开发成长指南/README.md: -------------------------------------------------------------------------------- 1 | 链接:https://mp.weixin.qq.com/s/NeIJmb2LsCLSQkDEs5jhTQ 2 | 3 | ## 个人打分 4 | ⭐️⭐️⭐️⭐️ 5 | 6 | ## 阅读时间 7 | 2024/11/14 - 2024/11/14 8 | 9 | ## 读书心得 10 | 我认为这个成长路线是可供参考的,其中重点标注的部分,往往是腾讯面试时的重点考察部分,工作 4 年多,我自诩中级开发工程师,但文中提到的很多问题,也是一知半解,刚好用来差缺补漏 -------------------------------------------------------------------------------- /BlogArticle/后台工程师职业能力提升之道/README.md: -------------------------------------------------------------------------------- 1 | 链接:https://mp.weixin.qq.com/s/W-Zk5PMxrSnUoICMBDm8wg 2 | 3 | ## 个人打分 4 | ⭐️⭐️⭐️ 5 | 6 | ## 阅读时间 7 | 2024/11/06 - 2024/11/06 8 | 9 | ## 读书心得 10 | 准确抽象问题,和别人讨论核心问题点,然后一起解决问题的能力。 11 | 12 | 这一点我深有感触,在与其他开源作者讨论问题时,别人总是能精准的抓住问题的关键点,即你的核心诉求是什么。 13 | 14 | -------------------------------------------------------------------------------- /BlogArticle/如何画好架构图:7种常用类型与示例/README.md: -------------------------------------------------------------------------------- 1 | 链接:https://mp.weixin.qq.com/s/AQWckbTwamt3tISh-bKxxQ 2 | 3 | ## 个人打分 4 | ⭐️⭐️⭐️ 5 | 6 | ## 阅读时间 7 | 2024/11/06 - 2024/11/06 8 | 9 | ## 读书心得 10 | 11 | ### 画图工具 12 | 13 | - https://excalidraw.com/ ,简单、快 14 | - https://icraft.gantcloud.com/zh-CN ,3D 的,看着炫,画部署图不错 15 | - https://app.diagrams.net/ ,好用,就是需要适应它调整样式的方式,时序图推荐用它 16 | 17 | ### 时序图 18 | 19 | ![时序图](image.png) 20 | 21 | 用于描述参与者之间的动态调用关系。 22 | 23 | > 动态调用,强调**系统运行时**对象之间的交互过程;静态调用,强调**系统结构**层面的关系。 24 | 25 | 参与者下的垂直虚线,称作生命线,生命线上的矩形条,称作激活条。 26 | 27 | 使用场景:描述系统或组件之间的流程逻辑、调用关系。 28 | 29 | ### 状态图 30 | 31 | ![状态图](image-1.png) 32 | 33 | 单个对象生命周期的状态变迁。 34 | 35 | 每种状态之间变迁的原因,画这种图可以清晰的表示。 36 | 37 | 使用场景:对象内部会有复杂的状态变化。 38 | 39 | ### 部署图 40 | 41 | ![部署图](image-2.png) 42 | 43 | 部署图描述软件系统的最终部署情况。比如,需要部署多少服务器,关键组件都部署在哪些服务器上,现有的系统服务器的关系,和第三方服务器的关系。 44 | 45 | 使用场景:现有的系统服务器的关系,和第三方服务器的关系;让团队了解系统运行在物理上是什么样子。 -------------------------------------------------------------------------------- /BlogArticle/如何画好架构图:7种常用类型与示例/image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/BlogArticle/如何画好架构图:7种常用类型与示例/image-1.png -------------------------------------------------------------------------------- /BlogArticle/如何画好架构图:7种常用类型与示例/image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/BlogArticle/如何画好架构图:7种常用类型与示例/image-2.png -------------------------------------------------------------------------------- /BlogArticle/如何画好架构图:7种常用类型与示例/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/BlogArticle/如何画好架构图:7种常用类型与示例/image.png -------------------------------------------------------------------------------- /BlogArticle/如何给GameServer做压测/README.md: -------------------------------------------------------------------------------- 1 | 链接:https://wudaijun.com/2019/09/gs-pressure-test/ 2 | 3 | ## 个人打分 4 | ⭐️⭐️⭐️⭐️⭐️ 5 | 6 | ## 阅读时间 7 | 2024/10/23 - 2024/10/23 8 | 9 | ## 读书心得 10 | 11 | 我理想中的测试机器人,根本不需要那么复杂。 12 | 13 | 首先,我认为操作上,应该从 Client 客户端的角度去出发,客户端面向的是协议,比如在我的项目中,我会提供 Proto 协议,登录 -> 进入游戏大厅 -> 获取玩家信息等。 14 | 每个协议,都可以构建成组件,拖拽到画布中,用户通过连线,决定执行顺序即可! 15 | 16 | > 上面的描述,属于基本测试,即游戏业务的算法是符合预期的 17 | 18 | ## 游戏基本测试的痛点 19 | 20 | - 痛点一:使用测试用例,测试前需要建立 TCP 连接,这代表每次写测试用例,都很蛋疼的需要重复写 21 | - 建立连接 22 | - (可选)创建玩家 23 | - 进入游戏 24 | - 执行核心逻辑 25 | - 痛点二:返回结果输出不好查看结果 26 | - 痛点三:测试前,需要准备的账号太麻烦 27 | - 痛点四:不写测试代码,根本测试不了 28 | - 痛点五:测试流程中,没有记录每次请求的数据,无法回溯(真实客户端请求的场景中,客户端很可能缓存数据) 29 | 30 | ## 游戏基准测试思路 31 | - 新建压测机器人,压测机器人需要满足如下条件。 32 | - 异步请求:异步才能模拟客户端真实场景下的请求和压力 33 | - 数据同步:像客户端一样缓存和处理服务器响应数据 34 | - 可重入:机器人应该可以在任何时候关闭/重启,而不应该假设初始状态(比如只有注册的时候能跑) 35 | - 随机性:机器人行为尽可能随机分布,并且每次重启重新初始化随机种子 36 | - 确定压测对象 37 | - 服务器比较耗时的 API:大背包多条件排序等 38 | - 玩家越多越耗时的逻辑:视野同步和消息广播等 39 | - 玩家日常频繁操作的行为:技能面板和任务面板等 40 | - 压测结果分析统计,从如下几个方面获取性能指标。 41 | - 函数级分析:Go pprof 简单易用 42 | - 消息级分析:统计每个 Actor 完成逻辑时,单次请求的处理时间(最大/平均/次数) 43 | - 服务器消息统计:统计网关层发送请求,到网关层收到响应的时间差(最大/平均/次数)。相比消息级分析,服务请求统计包含了多个 Actor 处理请求若干消息的时间,以及 Actor 之间通信的开销 44 | - 客户端请求统计:统计从请求发出到响应的(最大/平均/次数)时间,相比服务器消息统计,多了网络层的时延和机器人本身的处理时间,这是最接近客户端实际体验的指标 45 | -------------------------------------------------------------------------------- /BlogArticle/带你通俗易懂的了解进程、线程和协程/README.md: -------------------------------------------------------------------------------- 1 | 链接:https://mp.weixin.qq.com/s/Tq5a7_gF-rHeEz6AxH8iXQ 2 | 3 | ## 个人打分 4 | ⭐️⭐️⭐️⭐️ 5 | 6 | ## 阅读时间 7 | 2024/10/29 - 2024/10/29 8 | 9 | ## 读书心得 10 | 以加载视频资源的场景为例。 11 | 12 | 进程的作用是,让播放视频的代码运(进)行起来。 13 | 14 | 播放视频前,需要缓冲(加载和解码等)视频,我们可以让进程中的不同线程完成播放视频所需工作。A 进程中的 a 线程加载和缓冲,b 线程播放。如下图。 15 | 16 | ![alt text](image.png) 17 | 18 | 为什么不新创建 2 个进程来完成 a 和 b 线程的工作呢? 19 | 因为进程间通信(交换数据)相比线程间通信麻烦的多。如下图。 20 | 21 | ![alt text](image-1.png) 22 | 23 | 线程间通信,它们会共享数据。共享数据需要注意**共享资源覆盖问题**,引起该问题有两点: 24 | 1. 线程切换(上下文切换) 25 | 2. 非原子命令 26 | 27 | 最后是协程,协程实际上可以将加载和解码视频的工作粒度再次拆分(一个线程之前需要完成加载和解码视频),拆分的原则是,**协程可以挂起**,这样能加快视频加载的事件。如下图。 28 | 29 | ![alt text](image-2.png) 30 | 31 | 但线程不也能达到同样的效果吗?不用线程,是因为线程执行加载操作,主要是 I/O 操作,几乎不消耗 CPU 资源,但执行线程会阻塞,另外创建线程本身有开销,切换也有开销,相比之下,协程的开销极小,因为它并非内核态的东西,不涉及到内核调度,并且协程不存在共享资源覆盖的问题,协程的调度执行时机由程序自身控制,它们共享线程的 CPU 时间片资源,并有先后顺序,不能并行执行。 32 | 33 | 协程本身是一个特殊函数,普通的函数一旦执行就会从头到尾的运行,中间不会停止。而协程可以执行到一般暂停(有栈协程),利用这一特性,我们在遇到 I/O 这类不消耗 CPU 资源的操作时,可以将其挂起,继续执行其他任务。 -------------------------------------------------------------------------------- /BlogArticle/带你通俗易懂的了解进程、线程和协程/image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/BlogArticle/带你通俗易懂的了解进程、线程和协程/image-1.png -------------------------------------------------------------------------------- /BlogArticle/带你通俗易懂的了解进程、线程和协程/image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/BlogArticle/带你通俗易懂的了解进程、线程和协程/image-2.png -------------------------------------------------------------------------------- /BlogArticle/带你通俗易懂的了解进程、线程和协程/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/BlogArticle/带你通俗易懂的了解进程、线程和协程/image.png -------------------------------------------------------------------------------- /BlogArticle/有栈协程与无栈协程/README.md: -------------------------------------------------------------------------------- 1 | 链接:https://mthli.xyz/stackful-stackless/ 2 | 3 | ## 个人打分 4 | ⭐️⭐️⭐️⭐️⭐️ 5 | 6 | ## 阅读时间 7 | 2024/10/22 - 2024/10/22 8 | 9 | ## 读书心得 10 | 有栈协程(Go-go)、无栈协程(JavaScript-await) 11 | 前者可以在其任意嵌套函数中被挂起,后者则不可以。压栈的部分,还需要理解 -------------------------------------------------------------------------------- /BlogArticle/服务端开发必备:9大性能优化秘技/README.md: -------------------------------------------------------------------------------- 1 | 链接:https://mp.weixin.qq.com/s/VQzmg31MkZbUJVnNfqXKkQ 2 | 3 | ## 个人打分 4 | ⭐️⭐️⭐️⭐️ 5 | 6 | ## 阅读时间 7 | 2024/10/15 - 2024/10/18 8 | 9 | ## 读书心得 10 | 愈加发现 Actor 模型的优势,它契合了文章中提到的多个优化点:无锁化、缓存一致性、池化。性能分析需要通过工具获取耗时入手 11 | 12 | ![](./服务端开发必备:9大性能优化秘技.png) -------------------------------------------------------------------------------- /BlogArticle/服务端开发必备:9大性能优化秘技/服务端开发必备:9大性能优化秘技.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/BlogArticle/服务端开发必备:9大性能优化秘技/服务端开发必备:9大性能优化秘技.png -------------------------------------------------------------------------------- /BlogArticle/深入理解Linux的TCP三次握手/README.md: -------------------------------------------------------------------------------- 1 | > 链接:https://mp.weixin.qq.com/s/LIeb5DwMS6RS2YhEw47GDA 2 | 3 | > 理解 TCP 实现的部分细节 4 | 5 | 每个人,对于 TCP 协议可以说是熟悉的,也可以说是不熟悉的。 6 | 熟悉,是因为它的三次握手、四次挥手、滑动窗口、慢启动、拥塞避免、拥塞控制等概念好像都有些了解。说不熟悉,是因为它很复杂,在运行过程中网络环境会变化,TCP 的相关机制也会产生相关的适应行为。 7 | 8 | ## TCP 是一个面向连接可靠的传输层协议,但什么是面向连接可靠呢? 9 | TCP 协议复杂的原因,就是因为 IP 协议的交付质量,IP 是面向无连接不可靠的协议,TCP 需要基于它去设计一个面向连接可靠的传输层协议 10 | ## 什么是面向连接? 11 | 发送方关心接收方是否处于可接收数据的状态,并且关心传输时,数据之间的关系。 12 | ## 什么是可靠? 13 | 数据在传输过程中不会被损坏或者丢失,保证数据可以正确到达。 14 | ## 如何解决面向连接的问题? 15 | 通过建立连接,传输数据,断开连接的三步骤,创建一个长期的数据传输机制,在同一个连接中的数据传输有上下文关系。所以引申出了: 16 | seq 序列号字段,维护传输数据的顺序关系,保证按序交付,同时,解决数据包重复的问题。 17 | syn,ack,fin,rst 字段的值,创建、断开和维护一个连接。 18 | 19 | ``` 20 | - SYN:建立连接。在三次握手的第一步和第二步中使用。SYN 为 1 时,表示这是一个连接请求或连接接受请求 21 | - ACK:确认收到的数据。TCP 规定,连接建立后,ACK 必须置为 1 22 | - FIN:释放连接 23 | - RST:重置连接 24 | ``` 25 | 26 | ## 如何解决可靠性的问题? 27 | 引入数据确认机制,发送方发送数据后,等待对方确认。(停止等待协议)维护确认字段 Acknowledgement 和 ack 状态。 28 | 引入数据确认机制后,引发了带宽利用率不高的问题,解决方案:引入滑动窗口确认机制,即发送多个包之后,一起确认。 29 | 引入窗口后,如何在不同延时的网络上,确定窗口的大小?解决方案:引入窗口变量和窗口检测通告。 30 | 31 | ``` 32 | 因为 TCP 协议是双向数据流,所以双方(发送方和接收方)都需要维护自己的发送窗口和接收窗口大小(左右边界),窗口大小的调整是一个动态过程,如下。 33 | - 发送方根据网络状况和自身处理能力调整发送窗口 34 | - 接收方根据自身处理能力调整接收窗口,并通过 ACK 通告给发送方 35 | - 发送方根据接收方通告的窗口大小调整自己的发送窗口 36 | 这个过程持续进行且不是严格的轮流顺序。窗口的初始大小在三次握手时会协商好。 37 | 窗口检测通告,是为了让发送方检测到接收方的窗口何时再打开的定时器机制。例如,接收方缓冲区已满,会设置 ACK=0 并响应给发送方。发送方收到后,会启动一个持续计时器,每次倒计时结束,都会发送一个窗口探测数据包。 38 | ``` 39 | 40 | ``` 41 | 窗口检测通告机制,避免了死锁。场景如下。 42 | - 接收方的窗口变为 0,通过 ACK 告诉发送方 43 | - 过了一段时间,接收方的窗口不为 0 了,通知发送方,但是通知的数据包丢失了(接收方不会主动重传 ACK) 44 | - 发送方一直等待窗口打开,接收方一直等待新数据 45 | 做完上面的事情后,带宽可以被充分利用了,但是网络环境是复杂的 46 | ``` 47 | 48 | 做完上面的事情后,带宽可以被充分利用了,但是网络环境是复杂的,随时可能因为大量数据导致网络上的阻塞,这时候又需要引入阻塞控制等等。 49 | TCP 复杂的原因,就是要在工程上解决这些问题。 50 | 51 | ## 为什么需要三次握手? 52 | 首先,需要理解建立连接的目的,如下。 53 | 1. 确定对端在线 54 | 2. 保证包的顺序(因为数据传输是双向的,所以两端都需要确认对端的起始序列号) 55 | 56 | 三次握手,才能满足第 2 个条件,假如两次握手,至少有一端无法确认对端是否了解你的起始序列号,即:我是服务端,对端发送起始序列号后,我也给对端回复我的起始序列号,此时,回复的数据包丢失,但我无法确认对端是否收到,所以还需要对端再确认一次。 57 | 58 | 假如,服务端 A 收到客户端 B 发的 SYN ,并回复了 SYN+ACK 后,又收到了另一个客户端 C 的请求,此时 A 会和 C 建立后续的 ESTABLISHED 连接吗? 59 | 当然不会,因为一个新的客户端 IP + Port 都不一样,直接发 ACK 后,TCP 会直接回复 RST,内核通过四元组进行校验,调用 __inet_lookup()进行查找。 60 | 61 | ```c++ 62 | static inline struct sock *__inet_lookup(...省略函数参数) 63 | { 64 | u16 hnum = ntohs(dport); 65 | struct sock *sk; 66 | 67 | // 先检查 established 中是否有连接,如果没有就直接 send_reset 68 | sk = __inet_lookup_established(net, hashinfo, saddr, sport, 69 | daddr, hnum, dif, sdif); 70 | *refcounted = true; 71 | if (sk) 72 | return sk; 73 | *refcounted = false; 74 | // 再检查 linstener中 是否有连接,如果没有就直接 send_reset 75 | return __inet_lookup_listener(net, hashinfo, skb, doff, saddr, 76 | sport, daddr, hnum, dif, sdif); 77 | } 78 | ``` 79 | 80 | 通过代码中的注释,可以知道查找分为两步。 81 | 确认连接存在后,如果连接是 TCP_ESTABLISHED 状态,直接开始接收数据,否则开始处理 TCP 的各种状态,TCP 的不同状态如下。 82 | - 第一次握手, TCP_LISTEN 83 | - 第三次握手,TCP_SYN_RECV 84 | 服务端在 SYN RECVED 状态下,将在缓存中记录客户端 SYN 包中的内容,以便在收包的过程中查找。该内容占用的 SLAB 缓存。 85 | SLAB 机制是 Linux 内核中的一种内存管理机制,用于管理小块内存的分配,通过对内核中经常使用的对象进行缓存和释放,SLAB可以有效减少分配小块连续内存时产生的内部碎片 86 | TCP 包占用 SLAB 缓存是有上限的,上限值决定了 TCP 在正常状态下,可以维持多少个 TCP_SYN_RECVED 状态的连接,即半连接数,该值默认情况根据总内存大小自动生成,内存越大值越大。半连接以队列的形式存储。 87 | 88 | ## 半连接队列被耗尽怎么办? 89 | 回答这个问题需要先理解几个概念,如下。 90 | - syncookie 91 | - request_sock_queue 结构体中的 qlen 92 | - TFO - TCP Fast Open 93 | 先抛出几个结论,当 syncookie 开启的情况,半连接队列可认为无上限。 94 | request_sock_queue 结构体中的 qlen 会在 tcp_conn_request() 函数执行结束后增加,即 qlen 为服务器 listen 端口的半连接队列的当前长度。 95 | 96 | ```c++ 97 | if (!net->ipv4.sysctl_tcp_syncookies && 98 |                     (net->ipv4.sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) < 99 |                      (net->ipv4.sysctl_max_syn_backlog >> 2)) && 100 |                     !tcp_peer_is_proven(req, dst)) { 101 | ``` 102 | 103 | 当 sysctl_tcp_syncookies 未开启时,如果当前半连接池剩余长度,小于最大长度的四分之一后,就不再处理新建连接请求了,这也就是著名的 synflood 攻击的原理。 104 | syncookie 的作用就是为了防止 synflood。 105 | 106 | ## syncookie 如何防止 synflood? 107 | 已明确 synflood 是针对半连接队列的攻击,那我们可以想办法绕过半连接池,能不能让服务端不记录第一次握手发过来的 SYN 四元组信息,同时还能在第三次握手的时候验证呢?其实是可能的,既然三次握手的第二次是服务端回包,那可以把第一次握手得到的消息放回包里,让客户端在第三次握手的时候再把这个信息带回来,然后我们拿到第三次握手的四元组信息后,再做验证。为了保证包内容尽量小,我们把数据放到包之前,做一下 hash 运算(根据四元组信息和当前时间),运算得到的结果就叫 cookie。 -------------------------------------------------------------------------------- /BlogArticle/游戏推荐业务中基于Sentinel的动态限流实践/README.md: -------------------------------------------------------------------------------- 1 | 链接:https://mp.weixin.qq.com/s/lFuXVBUW_PkrBp4NoAFTig 2 | 3 | ## 个人打分 4 | ⭐️⭐️⭐️ 5 | 6 | ## 阅读时间 7 | 2024/10/17 - 2024/10/17 8 | 9 | ## 读书心得 10 | 二次封装 sentinel 实现动态限流。对游戏场景没什么用。介绍了滑动窗口限流计数器。 流量突刺的概念很有意思。sentinel 使用了责任链模式 -------------------------------------------------------------------------------- /BlogArticle/聊聊布隆过滤器-Go语言实践篇/README.md: -------------------------------------------------------------------------------- 1 | 链接:https://www.cnblogs.com/wzh2010/p/18030915 2 | 3 | ## 个人打分 4 | ⭐️⭐️⭐️ 5 | 6 | ## 阅读时间 7 | 2024/10/16 - 2024/10/16 8 | 9 | ## 读书心得 10 | 偏实践,流程图清晰明了。缺少如何在 Redis 中安装布隆过滤器、没有提供更多的 Go 封装示例 -------------------------------------------------------------------------------- /BlogArticle/英语进阶指南/README.md: -------------------------------------------------------------------------------- 1 | 链接:https://byoungd.gitbook.io/english-level-up-tips 2 | 3 | ## 个人打分 4 | ⭐️⭐️⭐️⭐️⭐️ 5 | 6 | ## 阅读时间 7 | 2024/11/18 - 2024/11/18 8 | 9 | ## 读书心得 10 | 飞书链接:https://xiaobilibili.feishu.cn/docx/PglSdF7OPoX7Wrx6LOqcdtpGnEg?from=from_copylink -------------------------------------------------------------------------------- /BlogArticle/转转门店基于MQ的Http重试实践/README.md: -------------------------------------------------------------------------------- 1 | 链接:https://mp.weixin.qq.com/s/gf2vOCk2a6RBwrw4le5x4g 2 | 3 | ## 个人打分 4 | ⭐️⭐️⭐️ 5 | 6 | ## 阅读时间 7 | 2024/11/07 - 2024/11/07 8 | 9 | ## 读书心得 10 | 11 | 引入 MQ 做 HTTP 重试的优缺点: 12 | 13 | - 优点:解耦、保证系统最终一致性 14 | - 缺点:太重了、需要考虑异步后的实时性 15 | 16 | 使用消息队列重试 HTTP 请求,其实要结合实际业务考虑,也就是目前的业务中,是否会在执行完逻辑后,向 MQ 发送消息,如果仅仅因为重试 HTTP 就引入 MQ,确实没必要。 17 | 18 | 如果自己实现 HTTP 重试,需要考虑如下点: 19 | - 请求是否为幂等; 20 | - 请求后响应的哪些错误不需要重试; 21 | - 同步最大重试次数+退避异步重试直到最终成功。 -------------------------------------------------------------------------------- /BlogArticle/通过实例理解Web应用用户密码存储方案/README.md: -------------------------------------------------------------------------------- 1 | 链接:https://tonybai.com/2023/10/25/understand-password-storage-of-web-app-by-example/ 2 | 3 | ## 个人打分 4 | ⭐️⭐️⭐️⭐️ 5 | 6 | ## 阅读时间 7 | 2024/11/26 - 2024/11/26 8 | 9 | ## 读书心得 10 | 给出了可运行的实例与每个阶段演进的原因。 11 | 12 | 密码存储:加盐+慢哈希。 13 | 14 | 加盐:在密码字符串前或后添加一个随机字符串,再对字符串进行哈希 15 | 16 | 慢哈希:是 hash 字符串的一种算法类型,主流的用户密码存储推荐方案是 Bcrypt。“慢” 针对的是生成时,CPU 和内存开销都更大。 17 | 18 | 慢哈希算法在给攻击者带来时间和资源成本等困难的同时,也给服务端正常的身份认证带来一定的性能开销,不过大多数开发者认为这种设计取舍是值得的。 -------------------------------------------------------------------------------- /Book/TCP-IP详解1-卷1-协议/README.md: -------------------------------------------------------------------------------- 1 | # 第1章 概述 2 | 3 | 有效沟通取决于使用共同的语言。当语言代表一组行为时,需要一种协议来转换。协议在字典中的定义,是国家事务或外交场合的正式程序或规则系统。 4 | 5 | - 一系列**相关**协议 = 协议族; 6 | - 各种协议之间的关系 = 体系结构; 7 | - 各种协议需要完成的任务 = 参考模型。 8 | 9 | TCP/IP 协议族的参考模型是什么 = TCP 协议和 IP 协议需要完成什么任务? -------------------------------------------------------------------------------- /Book/代码大全-第2版-纪念版/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/Book/代码大全-第2版-纪念版/README.md -------------------------------------------------------------------------------- /Book/现代操作系统-第4版/README.md: -------------------------------------------------------------------------------- 1 | # 第2章 进程与线程 2 | 3 | ## 2.1 进程 和 2.1.1 进程模型 4 | 进程是**抽象概念**,它是操作系统实现所有功能的基础,它与程序是有区别的。通过一个例子来描述它们区别,如下。 5 | 6 | 我是一个厨师,我正在厨房做菜,我有食谱,冰箱里面有我需要的食材。 7 | 8 | 在这个比喻中,食谱是程序,我是CPU,食材是输入的数据,而进程,是我前面所有动作行为的总和。 9 | 10 | 对于进程来说,任意时刻只会有一个进程活跃。 11 | 12 | 针对进程编程的时候,不应该对时序做任何想当然的假设。 13 | 14 | ### 小结 15 | 进程是某种类型的活动,它有程序、输入、输出及状态。另外如果一个程序运行了两遍,则算作两个进程。 -------------------------------------------------------------------------------- /Essays/Go接口相关的优化技巧/README.md: -------------------------------------------------------------------------------- 1 | 1. 避免小对象接口、使用批处理接口 2 | ```go 3 | // 避免小对象接口 4 | type Reader interface { 5 | Read([]byte) (int, error) // 传递切片而不是小对象 6 | } 7 | 8 | // 使用批处理接口 9 | type BatchProcessor interface { 10 | ProcessBatch(items []Item) error // 批量处理而不是一个个处理 11 | } 12 | ``` 13 | 14 | 2. 接口提供默认实现 15 | ```go 16 | type Logger interface { 17 | Log(message string) 18 | } 19 | 20 | // 提供默认实现 21 | type DefaultLogger struct{} 22 | 23 | func (l *DefaultLogger) Log(message string) { 24 | fmt.Println(message) 25 | } 26 | 27 | // 可以嵌入默认实现 28 | type MyService struct { 29 | *DefaultLogger // 嵌入默认实现 30 | } 31 | ``` 32 | 33 | 3. 使用泛型优化接口 34 | ```go 35 | // 传统方式 36 | type Repository interface { 37 | Find(id string) (interface{}, error) 38 | Save(entity interface{}) error 39 | } 40 | 41 | // 使用泛型优化 42 | type GenericRepository[T any] interface { 43 | Find(id string) (T, error) 44 | Save(entity T) error 45 | } 46 | ``` 47 | 48 | 4. 接口隔离原则 49 | ```go 50 | // 不好的做法:接口太大 51 | type FileHandler interface { 52 | Read() 53 | Write() 54 | Close() 55 | Seek() 56 | Flush() 57 | } 58 | 59 | // 好的做法:拆分成小接口 60 | type Reader interface { 61 | Read() error 62 | } 63 | 64 | type Writer interface { 65 | Write() error 66 | } 67 | 68 | type Closer interface { 69 | Close() error 70 | } 71 | 72 | // 组合接口 73 | type ReadWriter interface { 74 | Reader 75 | Writer 76 | } 77 | ``` -------------------------------------------------------------------------------- /Essays/ProtoJson可能会限制表设计/README.md: -------------------------------------------------------------------------------- 1 | Account 账户结构体中的 AccountId 是通过雪花 ID 生成的,它可以是字符串,也可以是整形。我倾向于整形,因为雪花算法生产的 ID 本身是有序的,对于数据库来说,查询和存储(字符串比整形占用更多的存储,虽然是微不足道的)都是更友好的。 2 | 3 | Proto 结构体存储到 MongoDB 的场景,需要将 Proto 先转换为 Json,再将 Json 装载为 Bson。 4 | 5 | 前面提到的将 Proto 先转换为 Json,通用方案是使用 google 官方的 `protojson` 包 6 | 7 | 但遗憾的是,protojson 包转换的过程中,会将 int64 类型转换为 string 类型,通过[该代码](https://github.com/protocolbuffers/protobuf-go/blob/b92717ecb630d4a4824b372bf98c729d87311a4d/encoding/protojson/encode.go#L275-L278 8 | )造成的,因为 JavaScript 无法处理 int64 类型的精度,但从 JSON 的角度来看,它不应该能处理任何整形吗? 9 | 10 | 在 [issue](https://github.com/golang/protobuf/issues/1414) 中我看到了同样的疑问,社区给出的答复:`protojson` 包遵循此处定义的标准 JSON 映射,该[规定](https://developers.google.com/protocol-buffers/docs/proto3#json)表示 64 位 int 编码为 JSON 字符串。 11 | 12 | 目前看来,社区是不会修复该问题的,毕竟已经从 2022 年放到现在了。我思考的解决方案如下。 13 | 14 | - 反射; 15 | 16 | - 公司项目是开发阶段,可以考虑将 AccountId 设置为 String 类型; 17 | 18 | - 考虑新增一个 Model 层,保存到 MongoDB 手动进行一次映射,针对该场景,其他情况继续使用 `protojson` 包。 -------------------------------------------------------------------------------- /Essays/Redis多机部署/README.md: -------------------------------------------------------------------------------- 1 | # 机器故障了怎么办? 2 | Redis 是内存数据库,我们知道可以通过持久化机制,保存快照或者保存记录日志的方式,将数据持久化到磁盘。但是,如果机器故障了或磁盘坏了,数据就不就全没了吗?这种情况应该怎么办呢?别担心,考虑**主从模式**。 3 | 4 | ## 主从模式 5 | 6 | 给主节点 Master 配置一个从节点 Slave,当 Master 挂了以后,Slave 可以顶上。通常如果是小规模应用,从节点只配置一个,提供基础备份功能。 7 | 8 | 假设我们现在有两台机器,A 和 B。在 B 机器上,通过如下命令,可以将 B 设置为 A 的从节点: 9 | 10 | ```bash 11 | # 连接 B 机器的 Redis 命令交互行 12 | $ redis-cli 13 | # 将 B 设置为 A 的从节点 14 | 127.0.0.1:B> slaveof 127.0.0.1 A 15 | # 验证主从配置 16 | 127.0.0.1:B> info replication 17 | # 输出如下信息代表设置成功 18 | role:slave 19 | master_host:127.0.0.1 20 | master_port:6379 21 | master_link_status:up 22 | ... 23 | ``` 24 | 25 | 接下来,让我们一起通过 Docker 进行实验: 26 | 27 | ```bash 28 | # 创建一个网络 29 | $ docker network create redis-net 30 | # 启动 Master 节点 31 | $ docker run -d --name redis-master \ 32 | --network redis-net \ 33 | -p 6379:6379 \ 34 | redis redis-serve 35 | # 启动 Slave 节点 36 | $ docker run -d --name redis-slave \ 37 | --network redis-net \ 38 | -p 6380:6379 \ 39 | redis redis-server --slaveof redis-master 6379 40 | 41 | # 连接主节点并验证主从配置 42 | $ docker exec -it redis-slave redis-cli -h redis-master 43 | # 验证主从配置 44 | 127.0.0.1:6379> info replication 45 | # 输出如下信息代表设置成功 46 | # Replication 47 | role:master 48 | connected_slaves:1 49 | slave0:ip=172.18.0.3,port=6379,state=online,offset=42,lag=0 50 | 51 | # 连接从节点同上,不赘述,输出如下信息代表设置成功 52 | # Replication 53 | role:slave 54 | master_host:redis-master 55 | master_port:6379 56 | master_link_status:up 57 | ``` 58 | 59 | 验证从节点是否同步了主节点的数据: 60 | 61 | ```bash 62 | 127.0.0.1:B> GET skey 63 | (nil) 64 | 127.0.0.1:A> SET skey abc 65 | OK 66 | 127.0.0.1:B> GET skey 67 | "abc" 68 | ``` 69 | 70 | 通过上述实验,我们可以看到,从节点已经同步了主节点的数据。那主节点之前就有的数据,是否也会同步到从节点呢? 71 | 72 | 答案是**会**。让我们再进行一次实验: 73 | 74 | ```bash 75 | # 将 B 从 A 的从节点设置为独立节点 76 | 127.0.0.1:B> SLAVEOF on one 77 | OK 78 | # 输出如下信息代表设置成功 79 | 127.0.0.1:B> info replication 80 | # Replication 81 | role:master 82 | connected_slaves:0 83 | master_failover_state:no-failover 84 | ... 85 | # 在主节点 A 新增键值对并查询(请自己新增数据,这里忽略了) 86 | 127.0.0.1:A> keys * 87 | 1) "skey" 88 | 2) "bkey" 89 | 3) "akey" 90 | # 在 B 节点查询键值对,只有之前同步的键值对 91 | 127.0.0.1:B> keys * 92 | 1) "skey" 93 | # 重新将 B 设置为 A 的从节点 94 | 127.0.0.1:B> slaveof redis-master 6379 95 | OK 96 | # 在 B 节点查询键值对,所有键值对都同步过来了 97 | 127.0.0.1:B> keys * 98 | 1) "skey" 99 | 2) "bkey" 100 | 3) "akey" 101 | ``` 102 | 103 | 有了从节点,在主节点发生故障的时候,我们就可以把项目中 Redis 的连接配置,从原来的主节点修改为从节点。但是,我们什么时候才知道主节点发生故障呢?最简单的方式就是写一些脚本进行监测,当主节点挂了以后,自动将项目中 Redis 的连接配置从主节点修改为从节点。不过,这种监控以及故障转移的能力,Redis 已经有了完整解决方案:**哨兵模式**。 104 | 105 | ## 哨兵模式 106 | 107 | 它由一个或多个哨兵实例组成,用于监控 Redis 主节点和从节点,并在主节点发生故障时自动进行故障转移。本质上,Redis 的哨兵模式就是一个 Redis 进程,只是启动参数不同和职责不同。哨兵模式的 Redis 进程不负责数据存储,它主要负责三件事: 108 | 109 | 1. 监控主从节点是否正常运行; 110 | 2. 通过发布订阅模式,将故障转移的结果通知给订阅者; 111 | 3. 当主节点发生故障时,自动进行故障转移,选择一个最合适的从节点升级为主节点,并通知应用方更新配置。 112 | 113 | 下图为哨兵和主从的工作模式: 114 | 115 | ![哨兵和主从的工作模式](image1.png) 116 | 117 | 接下来,我们继续用 Docker,搭建一个哨兵集群,用于监控我的主从同步 Redis 集群,然后,模拟主从同步中的主节点挂掉了,从节点被选举为新的主节点。 118 | 119 | 1. 启动 Redis 主从集群 120 | 121 | 前面已经介绍过,不再赘述。 122 | 123 | 2. 查看 Redis 主节点 IP 124 | 125 | ```bash 126 | $ docker inspect redis-master | grep IPAddress 127 | ``` 128 | 129 | 3. 创建三个哨兵的配置文件 130 | ```bash 131 | # 创建三个哨兵的配置文件,172.18.0.2 请修改为自己的 Redis 主节点 IP 132 | $ cat > sentinel1.conf << EOF 133 | sentinel monitor mymaster 172.18.0.2 6379 2 134 | sentinel down-after-milliseconds mymaster 5000 135 | sentinel failover-timeout mymaster 60000 136 | sentinel parallel-syncs mymaster 1 137 | EOF 138 | 139 | # 复制配置文件 140 | $ cp sentinel1.conf sentinel2.conf 141 | $ cp sentinel1.conf sentinel3.conf 142 | ``` 143 | 配置说明: 144 | - sentinel monitor mymaster 172.18.0.2 6379 2:表示监控的主节点 IP 为 172.18.0.2,端口为 6379,至少有 2 个哨兵认为主节点挂了,才能进行故障转移; 145 | - sentinel down-after-milliseconds mymaster 5000:哨兵发现主节点 5 秒没有响应,就认为主节点故障; 146 | - sentinel failover-timeout mymaster 60000:故障转移超时时间; 147 | - sentinel parallel-syncs mymaster 1:表示在进行故障转移时,最多有 1 个从节点参与同步。 148 | 4. 启动三个哨兵 149 | ```bash 150 | $ docker run -d --name sentinel1 \ 151 | --network redis-net \ 152 | -p 26379:26379 \ 153 | -v $(pwd)/sentinel1.conf:/etc/redis/sentinel.conf \ 154 | redis redis-sentinel /etc/redis/sentinel.conf 155 | 156 | $ docker run -d --name sentinel2 \ 157 | --network redis-net \ 158 | -p 26380:26379 \ 159 | -v $(pwd)/sentinel2.conf:/etc/redis/sentinel.conf \ 160 | redis redis-sentinel /etc/redis/sentinel.conf 161 | 162 | $docker run -d --name sentinel3 \ 163 | --network redis-net \ 164 | -p 26381:26379 \ 165 | -v $(pwd)/sentinel3.conf:/etc/redis/sentinel.conf \ 166 | redis redis-sentinel /etc/redis/sentinel.conf 167 | ``` 168 | 169 | 启动后,你会看到哨兵容器日志中,有如下信息: 170 | 171 | ```bash 172 | WARNING: Sentinel was not able to save the new configuration on disk!!!: Device or resource busy 173 | ``` 174 | 175 | 这个警告信息可以忽略,不影响使用,这个警告是因为在 Docker 容器中,Redis Sentinel 无法将更新后的配置写回配置文件。这是因为我们使用的是只读卷挂载。 176 | 177 | 到此为止,我们启动的容器如下图所示。 178 | 179 | ![启动的容器](image2.png) 180 | 181 | 5. 模拟主节点故障 182 | ```bash 183 | # 停止 Redis 主节点 184 | $ docker stop redis-master 185 | 186 | # 连接原从节点验证是否已升级为主节点 187 | $ docker exec -it redis-slave redis-cli 188 | 127.0.0.1:6379> info replication 189 | # Replication 190 | role:master 191 | connected_slaves:0 192 | ... 193 | ``` 194 | 195 | 6. 恢复原主节点 196 | ```bash 197 | # 启动原主节点 198 | $ docker start redis-master 199 | 200 | # 查看原主节点状态(此时变成了从节点) 201 | $ docker exec -it redis-master redis-cli 202 | 127.0.0.1:6379> info replication 203 | # Replication 204 | role:slave 205 | master_host:172.18.0.3 206 | master_port:6379 207 | ... 208 | ``` 209 | 210 | ### Leader 哨兵的选举策略 211 | 发生故障后,哨兵集群会动态产生一个 **Leader 哨兵**,由它执行主从同步的故障转移,即最适合的从节点升级为新主节点,并通知其他哨兵和客户端配置更新。请注意,Leader 身份是**临时的**,**动态的**,完成故障转移后,所有哨兵又恢复平等地位,下次故障时重新选举,这种设计是为了避免多个哨兵同时执行故障转移和故障转移的一致性。**Leader 哨兵** 的选举如下图所示。 212 | 213 | ![Leader 哨兵的选举](image3.png) 214 | 215 | 当一个哨兵节点确定 Redis 集群的主节点下线后,会请求其他哨兵将自己选举为 Leader,被请求的哨兵如果没有同意过其他哨兵节点的选举请求,就会同意请求,也就是选票+1。 216 | 217 | 如果某个哨兵节点获得了超过半数哨兵节点的选票,且大于 quorum 配置值[1],就会成为 Leader 哨兵,否则重新进行选举。 218 | 219 | ### Leader 哨兵选择新主节点策略 220 | 当哨兵集群选举出 Leader 哨兵后,它将从主从同步的 Redis 集群中,选择一个节点作为新的主节点,选择策略优先级如下: 221 | 1. 过滤故障节点,故障节点包括网络状态不好的节点; 222 | 2. 选择 replica-priority 优先级最高的从节点。replica-priority(Redis 旧版本叫作 slave-priority)是 Redis 从节点的一个启动配置参数,默认值 100,取值范围 0-100; 223 | 3. 选择偏移量最大的从节点。偏移量记录写了从节点写了多少主节点的数据,偏移量越大,同步的数据越多,主从偏移量相同,则数据完全同步; 224 | 4. 选择 runId 最小的从节点作为主节点。runId 是 Redis 每次启动随机生成的 Redis 标识。 225 | 226 | ![Leader 哨兵重新选择主节点](image4.png) 227 | 228 | ## 参考资料 229 | 230 | [1] [Redis 集群配置:https://redis.io/docs/latest/topics/cluster-config/](https://www.w3cschool.cn/redis_all_about/redis_all_about-9tgp271d.html) 231 | -------------------------------------------------------------------------------- /Essays/Redis多机部署/image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/Essays/Redis多机部署/image1.png -------------------------------------------------------------------------------- /Essays/Redis多机部署/image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/Essays/Redis多机部署/image2.png -------------------------------------------------------------------------------- /Essays/Redis多机部署/image3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/Essays/Redis多机部署/image3.png -------------------------------------------------------------------------------- /Essays/Redis多机部署/image4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/Essays/Redis多机部署/image4.png -------------------------------------------------------------------------------- /Essays/关于Actor模型的思考/README.md: -------------------------------------------------------------------------------- 1 | # Actor 模型是什么? 2 | 3 | # Actor 模型解决了什么问题? 4 | 5 | ## 保证消息的有序性 6 | 假设玩家收到两个客户端请求: 7 | - 购买武器,处理时长 100 ms; 8 | - 将购买的武器装备到角色身上,处理时长 10 ms。 9 | 在 Actor 模型中,会严格顺序处理,先处理购买武器,再处理装备武器。而使用网关投递消息的情况下,网关只负责投递消息,并不保证处理顺序,这会出现消息乱序和数据竞争,接着刚才的例子,我们分析来看为什么网关会引起这些问题,如下。 10 | 11 | **消息乱序** 12 | 13 | - 实际发送顺序:购买武器 -> 装备武器; 14 | - 可能的处理顺序:装备武器 -> 购买武器。 15 | - 结果:装备了不存在的武器。从玩家的视角,这很奇怪,我明明购买了武器,为什么会提示我武器不存在呢?然后过一会儿,武器又出现在背包中,又可以装备了。 16 | 在实际游戏业务中,客户端有 UI 交互限制,这两个操作存在间隔是必然的,达到了秒级,但墨菲定律告诉我们,可能出问题的地方终将出问题,提前做好防护,总是明智的,就像我不会假设数据库响应永远很快,每个开发人员的水平都是优秀的。 17 | 18 | **数据竞争** 19 | 20 | 背包有物品数量的属性。 21 | - 玩家装备武器时,物品数量减少(武器到玩家身上了) 22 | - 玩家购买武器时,物品数量增加 23 | 这种情况,多个操作都在修改物品数量,就可能出现计数错误,通常,我们的解决方案是加锁,但“锁”自身也有性能损耗,性能测试代码与结果如下: 24 | 25 | ```go 26 | func BenchmarkMutexLoss(b *testing.B) { 27 | // 准备测试数据 28 | counter := 0 29 | 30 | // 重置计时器 31 | b.ResetTimer() 32 | 33 | // 测试无锁的情况 34 | b.Run("NoLock", func(b *testing.B) { 35 | for i := 0; i < b.N; i++ { 36 | counter++ 37 | } 38 | }) 39 | 40 | // 重置计数器 41 | counter = 0 42 | 43 | // 测试有锁的情况 44 | mutex := &sync.Mutex{} 45 | b.Run("WithLock", func(b *testing.B) { 46 | for i := 0; i < b.N; i++ { 47 | mutex.Lock() 48 | counter++ 49 | mutex.Unlock() 50 | } 51 | }) 52 | 53 | // 执行 go test -bench . -benchmem 结果如下。 54 | // BenchmarkMutexLoss/NoLock-12 804751552 1.369 ns/op 55 | // BenchmarkMutexLoss/WithLock-12 93302852 13.23 ns/op 56 | } 57 | ``` 58 | 59 | 有锁版本(13.23 ns) / 无锁版本(1.369 ns) ≈ 9.66,这意味着加锁操作大约比无锁操作慢了 10 倍。这就引出了 Actor 模型要解决的第二个问题,无锁编程。 60 | 61 | ## 避免数据竞争 62 | 我们接着用上面背包的例子,解释为什么 Actor 能避免数据竞争。 63 | 64 | - 传统加锁处理方式 65 | ```go 66 | type BackpackSystem struct { 67 | mu sync.Mutex 68 | backpack *Backpack 69 | } 70 | 71 | func (s *BackpackSystem) HandleBuyWeapon(weaponId int32) { 72 | s.mu.Lock() 73 | defer s.mu.Unlock() 74 | 75 | s.backpack.Used++ 76 | s.backpack.Slots = append(s.backpack.Slots, newWeapon) 77 | } 78 | ``` 79 | 80 | - Actor 方式 81 | ```go 82 | type BackpackActor struct { 83 | mailbox chan Message // 消息队列 84 | backpack *Backpack // 背包数据 85 | } 86 | 87 | func (a *BackpackActor) dispatch() { 88 | // 单线程处理消息 89 | for msg := range a.mailbox { 90 | switch msg.Type { 91 | case BuyWeapon: 92 | // 不需要加锁,因为消息是顺序处理的 93 | a.backpack.Used++ 94 | a.backpack.Slots = append(a.backpack.Slots, newWeapon) 95 | 96 | case EquipWeapon: 97 | // 同样不需要加锁 98 | a.backpack.Used-- 99 | // 移除背包物品... 100 | } 101 | } 102 | } 103 | 104 | // 外部调用只能通过发消息 105 | func (a *BackpackActor) BuyWeapon(weaponId int32) { 106 | a.mailbox <- Message{ 107 | Type: BuyWeapon, 108 | Data: weaponId, 109 | } 110 | } 111 | ``` 112 | 113 | 我们可以看到,修改数据,并不是直接访问数据,而是通过向 mailbox 发送消息,Actor 通过消息传递替代共享内存!这不正是 Go 的哲学之一:**通过消息共享内存**吗? 114 | Actor 推崇的是**数据完全被封装在 Actor 内**,每个 Actor 的数据具备隔离性,请留意,外部调用只能通过发消息,**Actor 内部可以直接调用方法**,这里的内部可以理解为 Actor 中的逻辑处理过程,下面是伪代码解释这个点。 115 | 116 | ```go 117 | // 玩家自己修改背包 118 | func (a *BackpackActor) EquipWeapon(weaponId int32) { 119 | // 本地方法调用,直接修改 120 | a.removeWeaponFromBackpack(weaponId) 121 | a.equipWeapon(weaponId) 122 | } 123 | 124 | // 外部请求修改背包 125 | func (a *BackpackActor) HandleMessage(msg Message) { 126 | a.mailbox <- msg // 通过消息队列处理 127 | } 128 | ``` 129 | 130 | ## 小结 131 | - 降低开发人员心智负担 132 | 133 | # Actor 模型如何实现? 134 | -------------------------------------------------------------------------------- /Essays/初级Go工程师/容器/Map/README.md: -------------------------------------------------------------------------------- 1 | # Map 2 | 3 | 字典(哈希表)是 Go 语言的内置类型,它是无序键值对集合。可以根据 Key,在 O(1) 的时间复杂度获取到 Value。字典的 Key 可以为任意的可比较数据类型,例如数字、字符串和指针等。 4 | 5 | ## 底层结构 6 | 7 | ## 操作性能 8 | 9 | 字典对象本身就是指针包装,传参时无须再次取地址。 10 | ```go 11 | func TestMapPointer(t *testing.T) { 12 | test := func(m map[string]int) { 13 | fmt.Printf("传参后 map 的内存地址:%p\n", m) 14 | } 15 | 16 | m := make(map[string]int) 17 | fmt.Printf("传参前 map 的内存地址:%p\n", m) 18 | test(m) 19 | } 20 | ``` 21 | 22 | 输出: 23 | 24 | ```shell 25 | 传参前 map 的内存地址:0xc000114510 26 | 传参后 map 的内存地址:0xc000114510 27 | ``` 28 | 29 | 创建时预先分配内存,可以减少扩容时的内存分配和重新哈希操作,提升性能。 30 | ```go 31 | func test() map[int]int { 32 | m := make(map[int]int) 33 | for i := 0; i < 1000; i++ { 34 | m[i] = i 35 | } 36 | 37 | return m 38 | } 39 | 40 | // 预先准备足够的空间 41 | func testCap() map[int]int { 42 | m := make(map[int]int, 1000) 43 | for i := 0; i < 1000; i++ { 44 | m[i] = i 45 | } 46 | 47 | return m 48 | } 49 | 50 | func BenchmarkTest(b *testing.B) { 51 | for i := 0; i < b.N; i++ { 52 | test() 53 | } 54 | } 55 | 56 | func BenchmarkTestCap(b *testing.B) { 57 | for i := 0; i < b.N; i++ { 58 | testCap() 59 | } 60 | } 61 | ``` 62 | 63 | 输出: 64 | 65 | ```shell 66 | # ns/op: 每次操作的纳秒数 67 | # B/op: 每次操作,分配的内存字节数 68 | # allocs/op: 每次操作,分配内存的次数 69 | BenchmarkTest-12 18790 62315 ns/op 86551 B/op 64 allocs/op 70 | BenchmarkTestCap-12 45966 26243 ns/op 41097 B/op 6 allocs/op 71 | ``` 72 | 73 | Map 存储海量小对象,应直接存储值,而不是指针,这样有利于减少垃圾回收的压力。 74 | 75 | > 小对象没有一个明确的内存大小,根据 Go 社区的经验,一般认为小于 128 字节的对象被认为是小对象,我们可以使用 `unsafe.Sizeof` 来查看对象大小。 76 | 77 | > 如果对象需要在多个地方共享,必须使用指针。注意,对象大小不是决定使用值还是指针的唯一要素,还需要考虑:使用频率;是否需要修改;是否需要共享。 78 | 79 | 使用指针的问题: 80 | - 指针会导致内存碎片化; 81 | - 每个指针都是一个需要被 GC 扫描的对象,如果有 100 万条记录,GC 就需要扫描 100 万个指针。 82 | 83 | 直接存储值的优势: 84 | - 内存布局更紧凑,更有利于 CPU 缓存; 85 | - 数据直接存储在 map 中,减少了 GC 需要扫描的对象数量。 86 | 87 | 另外,字典不会收缩内存,适当替换成新对象是必要的。例如删除的值数量,远大于保留的值数量。 88 | 89 | ## 扩容策略 90 | 91 | ## 安全 92 | 93 | 在迭代期间删除或新增键值是安全的。 94 | 95 | ```go 96 | // 1. 创建了一个包含 0-9 的 map 97 | // 2. 在迭代过程中,当 k=5 时添加了新的键值对 m[100] = 1000 98 | // 3. 每次迭代都会删除当前的键 99 | func TestSafeIterate(t *testing.T) { 100 | m := make(map[int]int) 101 | for i := 0; i < 10; i++ { 102 | m[i] = i + 10 103 | } 104 | 105 | fmt.Println("开始迭代前:", m) 106 | visited := make([]int, 0) 107 | 108 | for k := range m { 109 | visited = append(visited, k) 110 | if k == 5 { 111 | m[100] = 1000 112 | fmt.Println("添加新键值对 100:1000") 113 | } 114 | delete(m, k) 115 | fmt.Printf("当前迭代到 k=%d, 删除后的 %v\n", k, m) 116 | } 117 | 118 | // Go 的设计决定:保持 Map 迭代的性能和简单性 119 | //因此我们无法保证在迭代过程中,新增的键值对会被当前的迭代遍历到 120 | fmt.Println("访问过的键:", visited) 121 | } 122 | ``` 123 | 124 | 输出: 125 | 126 | ```shell 127 | 开始迭代前: map[0:10 1:11 2:12 3:13 4:14 5:15 6:16 7:17 8:18 9:19] 128 | 添加新键值对 100:1000 129 | 当前迭代到 k=5, 删除后的 map[0:10 1:11 2:12 3:13 4:14 6:16 7:17 8:18 9:19 100:1000] 130 | 当前迭代到 k=7, 删除后的 map[0:10 1:11 2:12 3:13 4:14 6:16 8:18 9:19 100:1000] 131 | 当前迭代到 k=0, 删除后的 map[1:11 2:12 3:13 4:14 6:16 8:18 9:19 100:1000] 132 | 当前迭代到 k=3, 删除后的 map[1:11 2:12 4:14 6:16 8:18 9:19 100:1000] 133 | 当前迭代到 k=4, 删除后的 map[1:11 2:12 6:16 8:18 9:19 100:1000] 134 | 当前迭代到 k=8, 删除后的 map[1:11 2:12 6:16 9:19 100:1000] 135 | 当前迭代到 k=9, 删除后的 map[1:11 2:12 6:16 100:1000] 136 | 当前迭代到 k=100, 删除后的 map[1:11 2:12 6:16] 137 | 当前迭代到 k=1, 删除后的 map[2:12 6:16] 138 | 当前迭代到 k=2, 删除后的 map[6:16] 139 | 当前迭代到 k=6, 删除后的 map[] 140 | 访问过的键: [5 7 0 3 4 8 9 100 1 2 6] 141 | ``` 142 | 143 | 运行时会对字典并发操作做出检测。如果某个任务正在对字典进行写操作,应该避免其他任务对该字典执行并发操作(读、写、删除)。 144 | 145 | ```go 146 | // 目前我使用的 Go 1.22 版本,map 的并发写操作并不会 panic 147 | // Go 1.19 之前的版本中,并发读写 map 会 panic 148 | // 但这并不意味着它是安全的,Go 1.22 版本,通过 go test -race 仍可以检测到数据竞争 149 | func TestConcurrentWrite(t *testing.T) { 150 | m := make(map[string]int) 151 | 152 | go func() { 153 | for { 154 | // 写操作,操作同一个键 "a" 155 | m["a"] += 1 156 | fmt.Println("写操作:", m["a"]) 157 | } 158 | }() 159 | 160 | go func() { 161 | for { 162 | // 读操作,也操作键 "a" 163 | v := m["a"] 164 | fmt.Println("读操作:", v) 165 | } 166 | }() 167 | 168 | time.Sleep(time.Second) // 给并发操作一些时间 169 | } 170 | ``` 171 | 172 | 输出: 173 | 174 | ```shell 175 | # Go 1.22 版本,无异常 176 | # 通过 go test -race 检测到数据竞争 177 | --- FAIL: TestConcurrentWrite (1.00s) 178 | testing.go:1398: race detected during execution of test 179 | 180 | # Go 1.19 版本,直接 panic 181 | panic: runtime error: concurrent map writes 182 | ``` 183 | -------------------------------------------------------------------------------- /Essays/初级Go工程师/泛型/README.md: -------------------------------------------------------------------------------- 1 | # Go 语言泛型 2 | 3 | ## 概述 4 | 5 | Go 语言从 Go 1.18 版本开始支持泛型。这是 Go 开源以来,在语法层面最大的一次变动。 6 | 7 | 泛型,是将算法与数据类型解耦,实现算法更广泛的复用性。 8 | 9 | ```go 10 | package generics 11 | 12 | import ( 13 | "fmt" 14 | "testing" 15 | ) 16 | 17 | func TestGenerics(t *testing.T) { 18 | // 没有泛型的情况下,我们需要针对不同数据类型,重复实现相同的算法逻辑 19 | fmt.Printf("Add(1, 2) = %d\n", Add(1, 2)) 20 | // fmt.Printf("Add(1.1, 2.1) = %f\n", Add(1.1, 2.1)) // 编译错误:类型不匹配 21 | 22 | fmt.Printf("GenericsAdd(1, 2) = %d\n", GenericsAdd(1, 2)) 23 | fmt.Printf("GenericsAdd(1.1, 2.1) = %f\n", GenericsAdd(1.1, 2.1)) 24 | } 25 | 26 | func Add(a, b int32) int32 { 27 | return a + b 28 | } 29 | 30 | func GenericsAdd[T int | float64](a, b T) T { 31 | return a + b 32 | } 33 | ``` 34 | 35 | 没有泛型之前,通常使用空接口类型 interface{},缺点: 36 | - 性能有损失; 37 | - 无法进行类型安全检查。 38 | 39 | ## 基本语法 40 | 41 | ### 类型参数 42 | 43 | 类型参数是在函数声明、方法声明的**入参的类型**,在泛型函数或泛型类型实例化时,类型参数会被一个类型实参替换。 44 | 45 | 以函数为例,对普通函数的参数与泛型函数的类型参数作一下对比。 46 | 47 | ```go 48 | // 普通函数的参数列表 49 | func Foo(a, b int, c string) { 50 | // a, b, c 是参数名,int 和 string 是类型 51 | } 52 | 53 | // 泛型函数的类型参数 54 | func GenericFoo[P int, Q string](a, b P, c Q) { 55 | // a, b, c 是参数名,P 和 Q 是类型 56 | // int, string 代表对参数名类型的约束, 我们可以理解为对参数类型可选值的一种限定 57 | } 58 | ``` 59 | 60 | GenericFoo 泛型函数的声明,比普通函数多了一个组成部分:参数类型列表。**它位于函数名和函数参数列表之间,通过一个方括号括起。** 61 | 62 | 泛型函数不支持变长类型参数,例如:`func GenericFoo[P ...int](a, b P)`。 63 | 64 | P、Q 在参数列表中,像一个未知类型的占位符,等到泛型函数具化时才能确定参数类型。 65 | 66 | ### 约束 67 | 68 | 约束,规定一个类型实参必须满足的条件要求。 69 | 70 | 在 Go 泛型中,**我们使用 interface 类型来定义约束。** 71 | 72 | > Go 1.18 以后,interface 既可以声明接口的方法集合,也可以用作泛型函数入参的类型列表。 73 | 74 | ```go 75 | // C1 定义约束 76 | // ~int | ~int32,带 ~ 的约束,代表接受指定的类型;接受以这些类型为底层类型的所有类型 77 | // ~ 更灵活,因为它支持自定义类型。如果不带 ~ 的约束,只接受确切的类型,不接受基于这些类型定义的新类型 78 | type C1 interface { 79 | ~int | ~int32 80 | M1() 81 | } 82 | 83 | func foo[P C1](t P) { 84 | 85 | } 86 | 87 | type T struct{} 88 | 89 | func (t T) M1() {} 90 | 91 | type T1 int 92 | 93 | func (t T1) M1() { 94 | 95 | } 96 | 97 | func TestGenericConstraint(t *testing.T) { 98 | var t1 T1 99 | foo(t1) 100 | 101 | // var t T 102 | // foo(t) // 编译错误:T 没有实现 C1 接口,t 的底层类型是 struct,不是 int 或 int32 103 | } 104 | ``` 105 | 106 | 建议将做约束的接口类型与做传统接口的接口类型,分开定义。 107 | 108 | ## 类型具化 109 | 110 | 通过一个泛型版本 Sort 函数的调用例子,看看调用泛型函数的过程都发生了什么: 111 | 112 | ```go 113 | // Sort 函数定义了一个泛型约束,要求 Elem 类型必须实现 Less 方法 114 | func Sort[Elem interface{ Less(y Elem) bool }](list []Elem) {} 115 | 116 | type book struct{} 117 | 118 | func (x book) Less(y book) bool { 119 | return true 120 | } 121 | 122 | func TestGenericInstantiation(t *testing.T) { 123 | var bookshelf []book 124 | Sort[book](bookshelf) // 泛型函数调用 125 | } 126 | ``` 127 | 128 | 上面的泛型函数调用 Sort[book],分为两个阶段: 129 | 130 | - 阶段一:具化 131 | ```go 132 | func TestGenericInstantiation(t *testing.T) { 133 | var bookshelf []book 134 | Sort(bookshelf) // 在这里,编译器会进行具化 135 | 136 | // 具化后相当于编译器生成了这样的函数: 137 | // func Sort_book(list []book) {} 138 | ``` 139 | 140 | 说白了,具化,会让编译器会为每个不同的具体类型生成专门的函数。 141 | 142 | - 阶段二:调用 143 | 和普通的函数调用没有区别,调用的是编译器具化后生成的函数 Sort_book。 144 | 145 | 有一个编译器的细节需要注意,编译器会进行实参类型参数的自动推导,我们例子中的泛型函数调用可以修改为: 146 | 147 | ``` 148 | func TestGenericInstantiation(t *testing.T) { 149 | var bookshelf []book 150 | Sort(bookshelf) // 去掉了[book] 151 | } 152 | ``` 153 | 154 | ## 泛型类型 155 | 除了函数可以携带类型参数变身为“泛型函数”外,类型也可以拥有类型参数而化身为“泛型类型”: 156 | 157 | ```go 158 | // Vector 是一个泛型类型,它接受一个类型参数 T,T 的约束为 any 159 | // any 在 Go 1.18 中,是 interface{} 的别名 160 | // 用 any 作为类型参数的约束,代表没有任何约束 161 | type Vector[T any] []T 162 | ``` 163 | 164 | 使用泛型类型,也要遵循先具化,再使用: 165 | ```go 166 | type Vector[T any] []T 167 | 168 | func (v Vector[T]) Print() { 169 | fmt.Println(v) 170 | } 171 | 172 | func TestGenericType(t *testing.T) { 173 | // 用类型实参对泛型类型进行了具化 174 | // 具化后的底层类型为 []int 175 | var iv = Vector[int]{1, 2, 3} 176 | 177 | iv.Print() // [1, 2, 3] 178 | } 179 | ``` 180 | 181 | ## 泛型的性能 182 | 183 | - Go 标准库 sort 包(非泛型版)的 Ints 函数; 184 | - Go 团队维护 golang.org/x/exp/slices 中的泛型版 Sort 函数。 185 | 186 | ```shell 187 | goos: darwin 188 | goarch: amd64 189 | pkg: guowei-gong/bryan/generics 190 | cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz 191 | BenchmarkSlicesSort-12 163 7611486 ns/op 0 B/op 0 allocs/op 192 | BenchmarkIntSort-12 174 6384800 ns/op 0 B/op 0 allocs/op 193 | PASS 194 | ok guowei-gong/bryan/generics 7.962s 195 | ``` 196 | 197 | 我本地使用的 Go 版本是 1.22,得出的结论是泛型是有一定性能损失的,不过内存效率上,两种都实现了零内存分配。 198 | 199 | 同样的代码,在 Go 1.18 上,得出的结论是泛型比无泛型的性能要高出一倍。 200 | 201 | 从编译速度的角度来说,Go 1.22 的编译速度比 Go 1.18 更快。 202 | 203 | ## 泛型的适用场景 204 | 205 | - 编写通用数据结构 206 | 207 | Go 中没有预定义的数据类型,例如树、链表等。 208 | ```go 209 | // 泛型链表 210 | type List[T any] struct { 211 | head *Node[T] 212 | } 213 | 214 | // 泛型树 215 | type Tree[T any] struct { 216 | value T 217 | left, right *Tree[T] 218 | } 219 | ``` 220 | 221 | - 函数支持多类型参数 222 | 223 | 当编写的函数的操作元素的类型为 slice、map、channel 等特定类型的时候。如果一个函数接受这些类型的形参,并且**函数代码没有对参数的元素类型作出任何假设**。在这种场合下,泛型方案可以替代反射方案,获得更高的性能。 224 | -------------------------------------------------------------------------------- /Essays/初级Go工程师/深拷贝与浅拷贝/README.md: -------------------------------------------------------------------------------- 1 | # Go 语言的深拷贝与浅拷贝 2 | 3 | - 深拷贝通常比浅拷贝耗时更多; 4 | - 日常开发工作,深拷贝的使用频率相对较低,可能有 80% 的时间不需要使用深拷贝。 5 | 6 | ## 概念 7 | 8 | ![alt text](image.png) 9 | 10 | ### 浅拷贝 11 | 12 | 创建一个新对象,并复制原对象的字段值,但对于引用类型(如指针、切片、map等),仅复制引用,不复制引用所包含的对象。 13 | 14 | 通过简单的赋值操作就能实现浅拷贝。 15 | 16 | ### 深拷贝 17 | 18 | 创建一个新对象,**递归**地复制原对象的所有字段值,对于引用类型,创建新的对象并复制其内容,而不是简单地复制引用。 19 | 20 | 通常,深拷贝需要额外编写代码实现,简单的赋值操作对于复杂类型而言,无法实现深拷贝。 21 | 22 | ## 深拷贝的适用场景 23 | 24 | ### 防止意外修改共享数据 25 | 26 | 不可变对象需求,例如函数式编程和安全性要求较高的场景。 27 | 28 | ```go 29 | package deep_copy 30 | 31 | import ( 32 | "fmt" 33 | "testing" 34 | ) 35 | 36 | type Config struct { 37 | Port int 38 | Data map[string]string 39 | } 40 | 41 | func TestDeepCopy(t *testing.T) { 42 | original := &Config{ 43 | Port: 8080, 44 | Data: map[string]string{"key": "value"}, 45 | } 46 | 47 | shallowCopy := original // shallowCopy 是浅拷贝的产物,共享 original 的 Data 引用 48 | 49 | // 深拷贝 50 | deepCopy := &Config{ 51 | Port: original.Port, 52 | Data: map[string]string{}, 53 | } 54 | for k, v := range original.Data { 55 | deepCopy.Data[k] = v 56 | } 57 | 58 | // 修改原始对象 59 | original.Port = 8081 60 | original.Data["key"] = "newValue" 61 | 62 | deepCopy.Port = 8082 63 | deepCopy.Data["key"] = "deepCopyValue" 64 | 65 | // 验证浅拷贝和深拷贝的结果 66 | fmt.Println("original:", original) // &{8081 map[key:newValue]} 67 | fmt.Println("shallowCopy:", shallowCopy) // &{8081 map[key:newValue]},shallowCopy 的修改影响 original 68 | fmt.Println("deepCopy:", deepCopy) // &{8082 map[key:deepCopyValue]},deepCopy 的修改不影响 original 69 | } 70 | ``` 71 | 72 | ### 并发编程中的数据隔离 73 | 74 | 如果每个 goroutine 都需要独立的数据副本,那么深拷贝是确保数据隔离的最佳方法。 75 | ```go 76 | func worker(data []int, ch chan []int) { 77 | // 深拷贝切片,避免影响其他 goroutine 78 | newData := append([]int(nil), data...) 79 | for i := range newData { 80 | newData[i] *= 2 // 修改数据 81 | } 82 | // 写入数据 83 | ch <- newData 84 | } 85 | 86 | func TestDeepCopy2(t *testing.T) { 87 | data := []int{1, 2, 3} 88 | ch := make(chan []int) 89 | 90 | go worker(data, ch) 91 | go worker(data, ch) 92 | 93 | // 读取数据 94 | result1 := <-ch 95 | result2 := <-ch 96 | 97 | fmt.Println("data:", data) // [1, 2, 3] 98 | fmt.Println("result1:", result1) // [2, 4, 6] 99 | fmt.Println("result2:", result2) // [2, 4, 6] 100 | } 101 | ``` 102 | 103 | ### 回滚机制或撤销操作 104 | 105 | 在涉及事务处理或修改数据等场景,用户操作出现错误,需要恢复到原始状态。 106 | 107 | ```go 108 | func TestDeepCopy3(t *testing.T) { 109 | // 初始化原始状态 110 | state := &State{ 111 | Value: "initial", 112 | Data: []int{1, 2, 3}, 113 | Metadata: &Metadata{ 114 | Version: 1, 115 | Author: "Alice", 116 | }, 117 | } 118 | 119 | // 保存当前状态的深拷贝 120 | backup := deepCopy(state) 121 | 122 | // 修改状态 123 | state.Value = "modified" 124 | state.Data[0] = 100 125 | state.Metadata.Version = 2 126 | 127 | // 输出修改后的状态 128 | fmt.Println("Current state:", state.Value) // 输出 "modified" 129 | fmt.Println("Current Data:", state.Data) // 输出 "[100 2 3]" 130 | fmt.Println("Current Metadata.Version:", state.Metadata.Version) // 输出 "2" 131 | 132 | // 恢复之前的状态 133 | state = backup 134 | 135 | // 输出恢复后的状态 136 | fmt.Println("Restored state:", state.Value) // 输出 "initial" 137 | fmt.Println("Restored Data:", state.Data) // 输出 "[1 2 3]" 138 | fmt.Println("Restored Metadata.Version:", state.Metadata.Version) // 输出 "1" 139 | } 140 | 141 | // 深拷贝函数,通过 JSON序列化与反序列化实现 142 | func deepCopy(original *State) *State { 143 | copy := &State{} 144 | bytes, _ := json.Marshal(original) 145 | _ = json.Unmarshal(bytes, copy) 146 | return copy 147 | } 148 | ``` 149 | 150 | ### 小结 151 | 深拷贝虽然开销较大,但它确保了数据的独立性、隔离性以及安全性。深拷贝适用场景无法全部穷举,只能列举部分。 152 | 153 | ## Go 语言中实现深拷贝的方法 154 | 155 | ### 自己手搓 156 | 157 | 像上面示例中那样,根据具体的类型手动实现深拷贝。 158 | 159 | #### 优点 160 | - 避免反射,性能较好; 161 | - 开发者可以完全控制拷贝的过程。 162 | 163 | 164 | #### 缺点 165 | - 需要为每种类型都单独实现深拷贝; 166 | - 对带有复杂嵌套的类型,实现会冗长和复杂。 167 | 168 | ### 反射 169 | 170 | 通过 Go 的 reflect,实现通用的深拷贝。 171 | 172 | #### 优点 173 | - 通用性强。 174 | 175 | #### 缺点 176 | - 性能降低,有额外开销; 177 | - 被拷贝类型中带有非导出字段,会抛出 panic。 178 | 179 | ```go 180 | type Address struct { 181 | Street string 182 | City string 183 | } 184 | 185 | type Person struct { 186 | Name string 187 | Age int 188 | Address *Address 189 | } 190 | 191 | func TestDeepCopy4(t *testing.T) { 192 | // 初始化原始对象 193 | original := &Person{ 194 | Name: "Alice", 195 | Age: 30, 196 | Address: &Address{ 197 | Street: "123", 198 | City: "Golang City", 199 | }, 200 | } 201 | 202 | // 使用 reflect 实现的通用深拷贝 203 | copy := DeepCopy(original).(*Person) 204 | 205 | // 修改拷贝对象的值 206 | copy.Address.City = "New City" 207 | 208 | // 输出结果 209 | fmt.Println("Original Addr:", original.Address) // 输出 &{123 Golang City} 210 | fmt.Println("Copy Addr:", copy.Address) // 输出 &{123 New City} 211 | } 212 | 213 | // 深拷贝函数,使用 reflect 递归处理各种类型 214 | func DeepCopy(src interface{}) interface{} { 215 | if src == nil { 216 | return nil 217 | } 218 | 219 | // 运行时,通过 reflect 获取值和类型 220 | value := reflect.ValueOf(src) 221 | typ := reflect.TypeOf(src) 222 | 223 | switch value.Kind() { 224 | case reflect.Ptr: 225 | // 对于指针,递归处理指针指向的值 226 | copyValue := reflect.New(value.Elem().Type()) 227 | copyValue.Elem().Set(reflect.ValueOf(DeepCopy(value.Elem().Interface()))) 228 | return copyValue.Interface() 229 | 230 | case reflect.Struct: 231 | // 对于结构体,递归处理每个字段 232 | copyValue := reflect.New(typ).Elem() 233 | for i := 0; i < value.NumField(); i++ { 234 | fieldValue := DeepCopy(value.Field(i).Interface()) 235 | copyValue.Field(i).Set(reflect.ValueOf(fieldValue)) 236 | } 237 | return copyValue.Interface() 238 | 239 | case reflect.Slice: 240 | // 对于切片,递归处理每个元素 241 | copyValue := reflect.MakeSlice(typ, value.Len(), value.Cap()) 242 | for i := 0; i < value.Len(); i++ { 243 | copyValue.Index(i).Set(reflect.ValueOf(DeepCopy(value.Index(i).Interface()))) 244 | } 245 | return copyValue.Interface() 246 | 247 | case reflect.Map: 248 | // 对于映射,递归处理每个键值对 249 | copyValue := reflect.MakeMap(typ) 250 | for _, key := range value.MapKeys() { 251 | copyValue.SetMapIndex(key, reflect.ValueOf(DeepCopy(value.MapIndex(key).Interface()))) 252 | } 253 | return copyValue.Interface() 254 | 255 | default: 256 | // 其他类型(基本类型,数组等)直接返回原始值 257 | return src 258 | } 259 | } 260 | 261 | ``` 262 | 263 | ### 第三方库 264 | 265 | - github.com/jinzhu/copier 266 | 267 | ## Go 语言中深拷贝的局限性 268 | 269 | ### 无法访问非导出字段 270 | 271 | 如果原类型中带有非导出字段,那么有些时候即便使用 jinzhu/copier 这样的第三方通用拷贝库也很难实现真正的深拷贝。 272 | 273 | 如果原类型在你的控制下,最好的方法是为原类型手动添加一个 DeepCopy 方法供外部使用。(回归手搓) 274 | 275 | ### 循环引用问题 276 | 277 | 原类型中存在循环引用时,简单的递归深拷贝可能会导致无限循环。 278 | 279 | 针对这样的带有循环引用的类型,通常会手搓 DeepCopy 方法,通过使用类似哈希表的方式,记录已经复制过的对象。 280 | 281 | ### 某些类型不支持拷贝 282 | 283 | 某些内置类型或标准库中的类型,比如 sync.Mutex、time.Timer 等,复制这些类型可能会导致未知行为。 284 | 285 | 对于这样的包含不支持拷贝的类型,在不改变源类型组成的情况下,无法实现深拷贝。 286 | 287 | ## 小结 288 | 除了上面三种情况外,有些时候性能也是使用深拷贝时需要考量的点,尤其是使用反射的时候,可以考虑手搓代替反射、使用对象池和预分配的方式,优化内存分配,减少深拷贝次数。 289 | 290 | ## 深拷贝与克隆 291 | 它们都是复制对象的概念,但它们在概念和实现细节上,存在一些差异。 292 | 293 | 克隆是指复制一个对象。其行为依赖于具体语言的实现方式。对于某些语言,克隆可能指的是浅拷贝(Shallow Copy),即只复制对象的基础数据字段,引用类型字段仍然指向原始对象。也有些语言将克隆定义为深拷贝,取决于上下文。比如在 Java 中,Object 类提供了 clone() 方法,默认是浅拷贝,用户可以通过实现 Cloneable 接口来自定义克隆的行为,比如实现为深拷贝的逻辑。 294 | 295 | 而深拷贝是一种递归的复制过程,不仅复制对象本身,还会复制该对象所有引用的其他对象。 296 | 297 | 目标对象在结构上与原对象一致的情况下,**可以将深拷贝理解为一种特定类型的克隆。** 298 | 299 | 300 | ## 参考 301 | - 《Go语言中的深拷贝:概念、实现与局限》,作者:TonyBai,链接:https://tonybai.com/?s=%E6%B7%B1%E6%8B%B7%E8%B4%9D -------------------------------------------------------------------------------- /Essays/初级Go工程师/深拷贝与浅拷贝/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/Essays/初级Go工程师/深拷贝与浅拷贝/image.png -------------------------------------------------------------------------------- /Essays/初级Go工程师/类型转换/README.md: -------------------------------------------------------------------------------- 1 | # Go 语言类型转换 2 | 3 | ## 1. 类型别名转换规则 4 | 5 | ```go 6 | // 这些类型本质上是完全等价的,只是名字不同而已 7 | // 下面是 Go 语言预定义的类型别名 8 | byte = uint8 9 | rune = int32 10 | []byte = []uint8 11 | ``` 12 | 13 | 你也可以用 type 自定义类型别名。当新类型与原类型完全等价时,可以进行隐式转换,如下: 14 | 15 | ```go 16 | type Text = string 17 | var s string = "hello" 18 | var t Text = s // 可以直接赋值 19 | ``` 20 | 21 | 注意,`type Text string` 与 `type Text = string` 是不同的,前者是定义了一个新类型,后者是定义了一个类型别名。 22 | 23 | 如果定义的是新类型,即使新类型与原类型,底层类型相同,也不能进行隐式转换。如下: 24 | 25 | ```go 26 | type Text string 27 | var s string = "hello" 28 | var t Text = s // 错误:不能进行隐式转换 29 | var t2 Text = Text(s) // 正确:可以进行显示转换 30 | ``` 31 | 32 | ## 2. 底层类型相关的类型转换规则 33 | 34 | ### 2.1 基本类型转换例子: 35 | ```go 36 | type Age int 37 | type Year int 38 | 39 | // TestUnderlying 测试底层类型相关的类型转换规则 40 | // 有相同的底层类型时,允许显示转换 41 | func TestUnderlying(t *testing.T) { 42 | var age Age = 27 43 | var year Year 44 | 45 | //year = age // 错误:虽然底层类型相同,但不能直接赋值(隐式转换) 46 | year = Year(age) // 正确:允许显示转换,因为底层类型相同 47 | 48 | t.Logf("year: %v", year) 49 | } 50 | ``` 51 | 52 | 底层类型是引用类型的时候,不能直接显式转换,因为引用类型包含了复杂的内部结构,可能包含元信息、指针、其他引用等,直接转换可能会导致数据丢失或损坏。引用类型包括:slice、map、channel。 53 | 54 | ## 3. slice 相关的转换例子: 55 | 56 | ```go 57 | func TestSlice(t *testing.T) { 58 | var a []int32 59 | var b []int64 60 | // b = []int64(a) // 错误:不能直接转换 61 | 62 | // 正确的转换方式是遍历 63 | b = make([]int64, len(a)) 64 | for i, v := range a { 65 | b[i] = int64(v) 66 | } 67 | } 68 | 69 | // TestSlice2 测试基于相同底层类型的 slice 的转换 70 | func TestSlice2(t *testing.T) { 71 | // 定义两个基于 []int32 的类型 72 | type Nums1 []int32 73 | type Nums2 []int32 74 | 75 | var a Nums1 = []int32{1, 2, 3} 76 | var b Nums2 77 | // b = Nums2(a) // 错误:即使底层类型相同,slice 也不能直接转换 78 | 79 | // 正确的转换方式是遍历 80 | b = make(Nums2, len(a)) 81 | for i, v := range a { 82 | b[i] = v // 这里不需要类型转换,因为底层类型相同 83 | } 84 | } 85 | ``` 86 | 87 | ## 4. map 相关的转换例子: 88 | 89 | ```go 90 | func TestMap(t *testing.T) { 91 | var userScores map[UserID]Score 92 | var rawData map[string]int 93 | 94 | // 初始化 95 | userScores = make(map[UserID]Score) 96 | rawData = map[string]int{ 97 | "user1": 100, 98 | "user2": 200, 99 | } 100 | 101 | //userScores = rawData // 错误:不能直接转换 102 | 103 | // 正确的转换方式,手动转换数据 104 | for k, v := range rawData { 105 | userScores[UserID(k)] = Score(v) 106 | } 107 | } 108 | ``` 109 | 110 | ## 5. channel 相关的转换例子: 111 | 112 | ```go 113 | func TestChannel1(t *testing.T) { 114 | type C chan string // 双向通道类型 115 | type C1 chan<- string 116 | type C2 <-chan string 117 | 118 | var ca C 119 | var cb chan string 120 | 121 | cb = ca // ok,可以隐式转换,因为底层类型相同 122 | ca = cb // ok 123 | 124 | // cb 为无名类型,且底层类型相同,可以被隐式转换 125 | var _ chan<- string = cb // ok 126 | var _ <-chan string = cb // ok 127 | var _ C1 = cb // ok 128 | var _ C2 = cb // ok 129 | 130 | // C 类型的值无法直接转换为类型 C1 或 C2 131 | // var _ = C1(ca) // compile error 132 | // var _ = C2(ca) // compile error 133 | 134 | // 但是类型C的值可以间接转换为类型C1或C2。 135 | var _ = C1((chan<- string)(ca)) // ok 136 | var _ = C2((<-chan string)(ca)) // ok 137 | var _ C1 = (chan<- string)(ca) // ok 138 | var _ C2 = (<-chan string)(ca) // ok 139 | } 140 | ``` 141 | 142 | ## 6. 指针相关的转换例子: 143 | 144 | ```go 145 | func TestPtr(t *testing.T) { 146 | type MyInt int 147 | type IntPtr *int 148 | type MyIntPtr *int 149 | 150 | var pi = new(int) // pi 类型为 *int 151 | var ip IntPtr = pi // 没问题,因为底层类型都相同。另外,pi 的类型为无名类型(注:无名类型,可以理解为没有使用 type 关键字定义的字面量/匿名结构体/函数类型) 152 | 153 | // var _ *MyInt = pi // 不能隐式转换 154 | var _ = (*MyInt)(pi) // 显式转换可以 155 | 156 | // Go 1.22.6 版本,类型 *int 的值可以直接转换为类型 MyIntPtr,间接转换反而不行了,使用的旧版本,可以间接地转换过去,直接转换不行 157 | var _ MyIntPtr = pi // 隐式转换可以 158 | var _ = MyIntPtr(pi) // 显式转换也可以 159 | 160 | // 类型 IntPtr 的值,不能被隐转换为类型 MyIntPtr 161 | // var _ MyIntPtr = ip // 不可以隐式转换 162 | var _ = MyIntPtr(ip) // 可以显式转换 163 | } 164 | ``` 165 | 166 | ## 7. 和接口实现相关的类型转换规则 167 | 给定一个值 x 和一个接口类型 I,如果 x 的类型为 Tx 并且类型 Tx 实现了接口类型 I,则 x 可以被隐式转换为类型 I。 168 | 169 | 此转换的结果为一个类型为 I 的接口值。此接口值包裹了: 170 | 171 | - x 的一个副本(如果 Tx 是一个非接口值); 172 | - x 的动态值的一个副本(如果 Tx 是一个接口值)。 173 | 174 | 这两种情况都确保了接口值的独立性,避免通过接口,修改原始值的可能性。 175 | 176 | ```go 177 | type Speaker interface { 178 | Speak() string 179 | } 180 | 181 | type Dog struct { 182 | Name string 183 | } 184 | 185 | func (d Dog) Speak() string { 186 | return d.Name + " says woof!" 187 | } 188 | 189 | // 非接口值转换为接口 190 | func TestInterface1(t *testing.T) { 191 | dog := Dog{Name: "旺财"} 192 | 193 | // dog 的类型是 Dog,实现了 Speaker 接口 194 | // 所以可以被隐式转换为 Speaker 接口 195 | var speaker Speaker = dog 196 | 197 | // 修改 dog 不会影响 speaker 198 | dog.Name = "小黑" 199 | 200 | fmt.Println(speaker.Speak()) // 输出: "旺财 says woof!" 201 | fmt.Println(dog.Speak()) // 输出: "小黑 says woof!" 202 | } 203 | 204 | type Animal interface { 205 | Speak() string 206 | } 207 | 208 | type Pet interface { 209 | Animal 210 | GetName() string 211 | } 212 | 213 | type Cat struct { 214 | Name string 215 | } 216 | 217 | func (c Cat) Speak() string { 218 | return c.Name + " says meow!" 219 | } 220 | 221 | func (c Cat) GetName() string { 222 | return c.Name 223 | } 224 | 225 | // 接口值转换为另一个接口 226 | func TestInterface2(t *testing.T) { 227 | cat := Cat{Name: "咪咪"} 228 | 229 | // cat 转换为 pet 接口 230 | var pet Pet = cat 231 | 232 | // pet 接口值可以转换为 Animal 接口 233 | // 因为 Pet 接口包含了 Animal 接口的所有方法 234 | var animal Animal = pet 235 | 236 | fmt.Println(animal.Speak()) // 输出: "咪咪 says meow!" 237 | } 238 | ``` 239 | 240 | ## 8. 类型不确定值 241 | 242 | ```go 243 | func TestUnspecified(t *testing.T) { 244 | // 值被隐式转换为目标类型了,如果转换不合法(例如 123.1 转换为 int),编译器会报错 245 | var a int = 123.0 246 | var b float32 = 123 247 | } 248 | ``` 249 | 250 | -------------------------------------------------------------------------------- /Essays/浅谈游戏ECS架构/README.md: -------------------------------------------------------------------------------- 1 | ## ECS 架构与 OOP 架构有什么区别? 2 | 3 | - 内存布局不同 4 | 5 | ```go 6 | // OOP 架构 7 | type GameObject struct { 8 | Position Vector3 9 | Health int 10 | Attack int 11 | Speed float32 12 | // 更多属性... 13 | } 14 | 15 | // 内存中的布局(示意) 16 | gameObjects := []*GameObject{ 17 | // object1: [ptr]->[pos|health|attack|speed|inventory...] 18 | // object2: [ptr]->[pos|health|attack|speed|inventory...] 19 | // object3: [ptr]->[pos|health|attack|speed|inventory...] 20 | } 21 | 22 | // ECS 架构 23 | type PositionComponent []Vector3 24 | type HealthComponent []int 25 | type AttackComponent []int 26 | type SpeedComponent []float32 27 | 28 | // 内存中的布局(示意) 29 | positions := []Vector3{pos1, pos2, pos3, pos4...} // 连续内存 30 | healths := []int{health1, health2, health3, health4...} // 连续内存 31 | speeds := []float32{speed1, speed2, speed3, speed4...} // 连续内存 32 | ``` 33 | 34 | - ECS 架构的内存布局是连续的,而 OOP 架构的内存布局是分散的。 35 | 36 | gameObjects 我们很好理解,每个实体就是一个玩家,那 ECS 架构中如何区分玩家呢? 37 | 38 | 在 ECS 架构中,通过 E - Entity 实体,来区分不同的实体,组件数组通过这个实体中的 ID 来索引定位数据。 39 | 40 | - 数据装载方式不同 -------------------------------------------------------------------------------- /Essays/游戏冷启动阶段后端配置热更/README.md: -------------------------------------------------------------------------------- 1 | ## 冷启动阶段,热更新后端游戏配置方案 2 | 3 | 选择将生成配置功能与 GM 后台分为两个容器部署,原因如下。 4 | - GM 容器专注后台服务 5 | - 从 Gitlab 权限视角来说,GM 不应该有访问代码仓库的敏感权限 6 | - 容器中添加 dotnet-sdk(执行 luban 必要环境),会显著增加镜像大小(dotnet-sdk:8 在 1GB 以上) 7 | 8 | 在 Docker Compose 环境中,Game 容器和 Gen Config 容器挂载 Game Config Data 目录,Game 容器只负责读取,Gen Config 只负责写入。 9 | 10 | ![设计图](image.png) -------------------------------------------------------------------------------- /Essays/游戏冷启动阶段后端配置热更/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/Essays/游戏冷启动阶段后端配置热更/image.png -------------------------------------------------------------------------------- /Essays/游戏架构中为啥远程服务调用选型gRPC/README.md: -------------------------------------------------------------------------------- 1 | ## 为啥远程服务调用选型 gRPC 2 | 3 | 远程服务调用功能,在我接触的游戏架构中,均选择了 gRPC,这不禁让我思考,为什么要有远程服务调用、为什么选择 gRPC? 4 | 5 | 先来看第一个问题,远程服务调用在游戏中解决了什么问题? 6 | 7 | 目前我一个场景就需要用到,在 GM Node(游戏运营管理节点) 操作玩家数据,当玩家在线时,需要即时通知玩家(在线玩家在其他节点)。 8 | 9 | 跨节点的通信,均基于套接字 Socket,假如未跨节点,仅跨进程,Socket 也是优化过的,不会经过网络协议栈和打包拆包等操作。 10 | 11 | 至此,我们得出一个结论,远程服务调用是为了解决游戏架构中的跨节点通信。 12 | 13 | 第二个问题,为什么选择 gRPC ? 14 | 15 | 先说结论,为了性能。 16 | 17 | 现代跨界点通信,需要解决三个问题,如下。 18 | - 如何表示数据? 19 | 表示数据指的就是,序列化和反序列化。gRPC 给出的答案是:Protocl Buffers 20 | - 如何传递数据? 21 | gRPC 传输层基于 TCP,应用层基于 HTTP/2。为什么会选择 HTTP/2?前面提到,选择 gRPC 是为了性能,而决定性能的主要因素是序列化效率和信息密度,传输协议的层次越高,信息密度越低,不直接给予 TCP(传输层),是因为 HTTP/2 的核心优势:多路复用+头部压缩,并且还不用自己处理连接管理和流控等,即 HTTP/2 带来的便利性和功能性,弥补了高层次性能损失。 22 | - 如何确认方法 23 | 通过 HTTP/2 头部标识中的完整路径格式,完整路径格式=包名+服务名+方法名 24 | 25 | 现代 RPC 的发展是分裂的,朝着面向对象、性能、简化… … 不同方向发展,近年,RPC 框架朝着更高层次抽象去发展了,不仅负责远程服务调用,还负责管理远程服务,例如阿里的 Dubbo,而这些都是根据的业务场景去选择的 :-) -------------------------------------------------------------------------------- /Essays/游戏登录系统设计/README.md: -------------------------------------------------------------------------------- 1 | # 游戏登录系统设计 2 | 3 | ## 确定设计范围 4 | 5 | ### 游戏预计有多少用户? 6 | 参考深空之眼,总注册在 300 万到 400 万之间。 7 | 8 | ### 游戏有多少种登录方式? 9 | 仅通过手机号登录和注册。 10 | 11 | ### 同一个手机号能否注册多次? 12 | 不可以,最多一次。 13 | 14 | 15 | ### 如何生成账号的唯一标识? 16 | 通过雪花算法生成唯一标识,相比 UUID,雪花算法的优势如下: 17 | - 全球唯一性更强 18 | 游戏会上台服、日服、韩服和国服 19 | - 不需要考虑时钟回拨 20 | 时钟回拨:服务器的系统时间突然回到了过去的某个时间点 21 | - 生成速度比 UUID 更快 22 | - 纯数字计算; 23 | - 只需要 8 字节(16位),而 UUID 需要 16 字节(128位)或 32字节(字符串形式)。 24 | - 数据库友好 25 | - 基于时间戳生成,天然有序; 26 | - 数字索引比字符串索引效率高。 27 | 28 | ### 公司其他游戏是否可以使用同一个账号? 29 | 仅考虑一款游戏。 30 | 31 | ### 其他 32 | - 会在分布式环境下工作; 33 | - 登录系统是一个单独的服务。 34 | 35 | ## 高层次的设计方案 36 | ### 玩家视角的登录流程 37 | 38 | #### 玩家在首次登录 39 | - 点击游戏应用 40 | - 客户端获取各种手机权限 41 | - 输入手机号 (运营商有一键登录) 42 | - 弹窗用户协议和隐私政策 43 | - 获取验证码,如果手机号未注册过,直接注册 44 | - 加载游戏资源 45 | - 展示公告 46 | - 进入游戏 47 | - 创建游戏角色 48 | - 昵称 49 | - 职业 50 | 51 | #### N 天内,玩家非首次登录 52 | - 点击游戏应用 53 | - 加载游戏资源 54 | - 进入游戏 55 | 56 | ### 开发视角的登录流程 57 | - Token 需要具有时效性 58 | - 同一台设备,N 天内,玩家不用每次登录都需要输入密码 59 | - 和上一次设备不一样,必须重新获取验证码登录 60 | - 需要接入第三方服务 61 | - 短信、运营商一键登录 62 | - 登录流程中可以查看公共数据 63 | - 公告 64 | - 登录后,需要保存玩家登录设备和上一次登录时间 65 | - 校验是否需要颁发新的 Token 66 | 67 | ## 深入设计 68 | 69 | ### 安全 70 | - Token 不支持续期; 71 | - Token 中的有效载荷,携带玩家设备唯一 ID,判断玩家的设备与上一次使用的设备是否一致,不一致则要求重新获取验证码。 72 | 73 | ### 切换账号 74 | 在【进入游戏】界面,点击切换账号,可以查看到近期在该设备中登录过的手机号。 75 | 76 | ### 忘记密码 77 | 78 | ### 连接即身份 79 | 假设 Token 有效期为 30 天,建立连接后,31 天未下限,一直保持连接,不会强制重新验证 Token。这里其实可以考虑防沉迷和禁止多设备登录兜底。避免盗号场景下,盗号人一直不下线。 80 | 81 | ## 架构图 82 | 83 | ## 时序图 84 | ![alt text](image.png) 85 | -------------------------------------------------------------------------------- /Essays/游戏登录系统设计/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/Essays/游戏登录系统设计/image.png -------------------------------------------------------------------------------- /Essays/部署游戏后端,win下使用docker遇见的坑/README.md: -------------------------------------------------------------------------------- 1 | 今天在 Win 下使用 docker 踩了不少坑,把耗费时间最久的拿出来记录一下。 2 | 3 | 我在 docker 容器中有一个 git 仓库,我希望通过 ssh 的方式拉取项目,一开始,该容器的 .rsa 文件是从宿主机复制的,宿主机可以拉取仓库代码,但是容器不行。 4 | 5 | 于是,我移除了原来的 rsa 文件,在容器中,新生成了一个 rsa 密钥,并配置到 git 仓库的 ssh 中。此时,容器中可以拉取仓库最新代码。 6 | 7 | 我把这些 .rsa 文件保存到宿主机,并让容器镜像构建时,使用刚才在容器中生成的 .rsa 文件,构建镜像后,启动容器,发现又不能拉取仓库代码了,我可以确定文件是同一个,并且权限是正确的, git pull 还是无法拉取。 8 | 9 | 我通过 cat -A,发现 Windows 上运行的 Docker,执行 dockerfile 的 COPY 关键词时,会偷偷加一个 $ 换行符,直接打开看,还看不到。 10 | 11 | 这是编码问题,在类 Unix 的系统上执行,是不会有这个问题的。Mac 和 Win 差别还是太大了,今天做了一下午的兼容处理,如下。 12 | 13 | - Win 复制到镜像中的脚本不可使用; 14 | - Win 的 COPY 会改变源文件的编码格式,肉眼不可见; 15 | - Win 的 volums 不支持权限配置,比如 ssh:ro,在 Win 的镜像中去看,文件居然是 777 的权限 16 | 17 | 用 Win 主要因为开发游戏过程中,需要跑 Unity,Mac 跑 Unity 装虚拟机的话,M3 Max 芯片都烫。 18 | 19 | 如何解决这个问题呢,我目前的解决方案是使用 dos2unix 转换一下。如果你的容器需要在类 Unix 环境下工作,而宿主机是 Win,建议文本内容都转换一下,比如,脚本和密钥等。 20 | 21 | ![alt text](image.png) -------------------------------------------------------------------------------- /Essays/部署游戏后端,win下使用docker遇见的坑/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/Essays/部署游戏后端,win下使用docker遇见的坑/image.png -------------------------------------------------------------------------------- /Pictures/Code Complete, 2nd Edition.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/Pictures/Code Complete, 2nd Edition.jpg -------------------------------------------------------------------------------- /Pictures/Computer Systems- A Programmer's Perspective.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/Pictures/Computer Systems- A Programmer's Perspective.jpg -------------------------------------------------------------------------------- /Pictures/Modern Operating Systems (4th Edition).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/Pictures/Modern Operating Systems (4th Edition).jpg -------------------------------------------------------------------------------- /Pictures/Operating Systems- Three Easy Pieces.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/Pictures/Operating Systems- Three Easy Pieces.jpg -------------------------------------------------------------------------------- /Pictures/TCP-IP Illustrated- The Protocols, Volume 1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/Pictures/TCP-IP Illustrated- The Protocols, Volume 1.jpg -------------------------------------------------------------------------------- /Pictures/Tencent Game Development Essentials II.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guowei-gong/GameBackendProgrammerStudyNotes/10540905fea08464597b3f42d4cebc9e75c89c7e/Pictures/Tencent Game Development Essentials II.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GameBackendProgrammerStudyNotes 2 | 3 | ## TCP/IP详解 卷1:协议 4 | 5 | ![](./Pictures/TCP-IP%20Illustrated-%20The%20Protocols,%20Volume%201.jpg) 6 | 7 | * 第1章 概述 8 | 9 | ## 深入理解计算机系统 10 | 11 | ![](./Pictures/Computer%20Systems-%20A%20Programmer's%20Perspective.jpg) 12 | 13 | ## 现代操作系统(原书第4版) 14 | 15 | 16 | 17 | * 第2章 进程与线程 18 | 19 | ## 操作系统导论 20 | 21 | ![](./Pictures/Operating%20Systems-%20Three%20Easy%20Pieces.jpg) 22 | 23 | ## 代码大全-第2版-纪念版 24 | 25 | ![](./Pictures/Code%20Complete,%202nd%20Edition.jpg) 26 | 27 | ## 腾讯游戏开发精粹II 28 | 29 | 30 | 31 | ## 后端游戏开发知识图谱 32 | ![](https://github.com/user-attachments/assets/0d5ab703-76f9-495a-8445-e7681ee4f599) 33 | --------------------------------------------------------------------------------