Golang开源:leaf - 游戏服务器框架
txvf9334
8年前
<p style="text-align:start">Leaf 是一个由 Go 语言(golang)编写的开发效率和执行效率并重的开源游戏服务器框架。Leaf 适用于各类游戏服务器的开发,包括 H5(HTML5)游戏服务器。</p> <p style="text-align:start">Leaf 的关注点:</p> <ul> <li>良好的使用体验。Leaf 总是尽可能的提供简洁和易用的接口,尽可能的提升开发的效率</li> <li>稳定性。Leaf 总是尽可能的恢复运行过程中的错误,避免崩溃</li> <li>多核支持。Leaf 通过模块机制和 <a href="/misc/goto?guid=4959642639323954285" style="box-sizing: border-box; background-color: transparent; color: rgb(64, 120, 192); text-decoration: none;">leaf/go</a> 尽可能的利用多核资源,同时又尽量避免各种副作用</li> <li>模块机制。</li> </ul> <h2 style="text-align:start">Leaf 的模块机制</h2> <p style="text-align:start">一个 Leaf 开发的游戏服务器由多个模块组成(例如 <a href="/misc/goto?guid=4959642639405480625" style="box-sizing: border-box; background-color: transparent; color: rgb(64, 120, 192); text-decoration: none;">LeafServer</a>),模块有以下特点:</p> <ul> <li>每个模块运行在一个单独的 goroutine 中</li> <li>模块间通过一套轻量的 RPC 机制通讯(<a href="/misc/goto?guid=4959642639490230030" style="box-sizing: border-box; background-color: transparent; color: rgb(64, 120, 192); text-decoration: none;">leaf/chanrpc</a>)</li> </ul> <p style="text-align:start">Leaf 不建议在游戏服务器中设计过多的模块。</p> <p style="text-align:start">游戏服务器在启动时进行模块的注册,例如:</p> <pre> <code class="language-go">leaf.<span style="color:rgb(0, 134, 179)">Run</span>( game.<span style="color:rgb(51, 51, 51)">Module</span>, gate.<span style="color:rgb(51, 51, 51)">Module</span>, login.<span style="color:rgb(51, 51, 51)">Module</span>, )</code></pre> <p style="text-align:start">这里按顺序注册了 game、gate、login 三个模块。每个模块都需要实现接口:</p> <pre> <code class="language-go"><span style="color:rgb(167, 29, 93)">type</span> <span style="color:rgb(237, 106, 67)">Module</span> <span style="color:rgb(167, 29, 93)">interface</span> { <span style="color:rgb(0, 134, 179)">OnInit</span>() <span style="color:rgb(0, 134, 179)">OnDestroy</span>() <span style="color:rgb(0, 134, 179)">Run</span>(closeSig <span style="color:rgb(167, 29, 93)">chan</span> <span style="color:rgb(167, 29, 93)">bool</span>) }</code></pre> <p style="text-align:start">Leaf 首先会在同一个 goroutine 中按模块注册顺序执行模块的 OnInit 方法,等到所有模块 OnInit 方法执行完成后则为每一个模块启动一个 goroutine 并执行模块的 Run 方法。最后,游戏服务器关闭时(Ctrl + C 关闭游戏服务器)将按模块注册相反顺序在同一个 goroutine 中执行模块的 OnDestroy 方法。</p> <h2 style="text-align:start">Leaf 源码概览</h2> <ul> <li>leaf/chanrpc 提供了一套基于 channel 的 RPC 机制,用于游戏服务器模块间通讯</li> <li>leaf/db 数据库相关,目前支持 <a href="/misc/goto?guid=4958830654050811860" style="box-sizing: border-box; background-color: transparent; color: rgb(64, 120, 192); text-decoration: none;">MongoDB</a></li> <li>leaf/gate 网关模块,负责游戏客户端的接入</li> <li>leaf/go 用于创建能够被 Leaf 管理的 goroutine</li> <li>leaf/log 日志相关</li> <li>leaf/network 网络相关,使用 TCP 和 WebSocket 协议,可自定义消息格式,默认 Leaf 提供了基于 <a href="/misc/goto?guid=4959642639597028411" style="box-sizing: border-box; background-color: transparent; color: rgb(64, 120, 192); text-decoration: none;">protobuf</a> 和 JSON 的消息格式</li> <li>leaf/recordfile 用于管理游戏数据</li> <li>leaf/timer 定时器相关</li> <li>leaf/util 辅助库</li> </ul> <h2 style="text-align:start">使用 Leaf 开发游戏服务器</h2> <p style="text-align:start"><a href="/misc/goto?guid=4959642639405480625" style="box-sizing: border-box; background-color: transparent; color: rgb(64, 120, 192); text-decoration: none;">LeafServer</a> 是一个基于 Leaf 开发的游戏服务器,我们以 LeafServer 作为起点。</p> <p style="text-align:start">获取 LeafServer:</p> <pre style="text-align:start"> <code class="language-go">git clone https://github.com/name5566/leafserver </code></pre> <p style="text-align:start">设置 leafserver 目录到 GOPATH 环境变量后获取 Leaf:</p> <pre style="text-align:start"> <code class="language-go">go get github.com/name5566/leaf </code></pre> <p style="text-align:start">编译 LeafServer:</p> <pre style="text-align:start"> <code class="language-go">go install server </code></pre> <p style="text-align:start">如果一切顺利,运行 server 你可以获得以下输出:</p> <pre style="text-align:start"> <code class="language-go">2015/08/26 22:11:27 [release] Leaf 1.1.2 starting up </code></pre> <p style="text-align:start">敲击 Ctrl + C 关闭游戏服务器,服务器正常关闭输出:</p> <pre style="text-align:start"> <code class="language-go">2015/08/26 22:12:30 [release] Leaf closing down (signal: interrupt) </code></pre> <h3 style="text-align:start">Hello Leaf</h3> <p style="text-align:start">现在,在 LeafServer 的基础上,我们来看看游戏服务器如何接收和处理网络消息。</p> <p style="text-align:start">首先定义一个 JSON 格式的消息(protobuf 类似)。打开 LeafServer msg/msg.go 文件可以看到如下代码:</p> <pre> <code class="language-go"><span style="color:rgb(167, 29, 93)">package</span> msg <span style="color:rgb(167, 29, 93)">import</span> ( <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>github.com/name5566/leaf/network<span style="color:rgb(24, 54, 145)">"</span></span> ) <span style="color:rgb(167, 29, 93)">var</span> <span style="color:rgb(51, 51, 51)">Processor</span> network.<span style="color:rgb(51, 51, 51)">Processor</span> <span style="color:rgb(167, 29, 93)">func</span> <span style="color:rgb(121, 93, 163)">init</span>() { }</code></pre> <p style="text-align:start">Processor 为消息的处理器(可由用户自定义),这里我们使用 Leaf 默认提供的 JSON 消息处理器并尝试添加一个名字为 Hello 的消息:</p> <pre> <code class="language-go"><span style="color:rgb(167, 29, 93)">package</span> msg <span style="color:rgb(167, 29, 93)">import</span> ( <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>github.com/name5566/leaf/network/json<span style="color:rgb(24, 54, 145)">"</span></span> ) <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 使用默认的 JSON 消息处理器(默认还提供了 protobuf 消息处理器)</span> <span style="color:rgb(167, 29, 93)">var</span> <span style="color:rgb(51, 51, 51)">Processor</span> = json.<span style="color:rgb(0, 134, 179)">NewProcessor</span>() <span style="color:rgb(167, 29, 93)">func</span> <span style="color:rgb(121, 93, 163)">init</span>() { <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 这里我们注册了一个 JSON 消息 Hello</span> Processor.<span style="color:rgb(0, 134, 179)">Register</span>(&Hello{}) } <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 一个结构体定义了一个 JSON 消息的格式</span> <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 消息名为 Hello</span> <span style="color:rgb(167, 29, 93)">type</span> <span style="color:rgb(237, 106, 67)">Hello</span> <span style="color:rgb(167, 29, 93)">struct</span> { <span style="color:rgb(237, 106, 67)">Name</span> <span style="color:rgb(167, 29, 93)">string</span> }</code></pre> <p style="text-align:start">客户端发送到游戏服务器的消息需要通过 gate 模块路由,简而言之,gate 模块决定了某个消息具体交给内部的哪个模块来处理。这里,我们将 Hello 消息路由到 game 模块中。打开 LeafServer gate/router.go,敲入如下代码:</p> <pre> <code class="language-go"><span style="color:rgb(167, 29, 93)">package</span> gate <span style="color:rgb(167, 29, 93)">import</span> ( <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>server/game<span style="color:rgb(24, 54, 145)">"</span></span> <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>server/msg<span style="color:rgb(24, 54, 145)">"</span></span> ) <span style="color:rgb(167, 29, 93)">func</span> <span style="color:rgb(121, 93, 163)">init</span>() { <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 这里指定消息 Hello 路由到 game 模块</span> <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 模块间使用 ChanRPC 通讯,消息路由也不例外</span> msg.<span style="color:rgb(51, 51, 51)">Processor</span>.<span style="color:rgb(0, 134, 179)">SetRouter</span>(&msg.<span style="color:rgb(51, 51, 51)">Hello</span>{}, game.<span style="color:rgb(51, 51, 51)">ChanRPC</span>) }</code></pre> <p style="text-align:start">一切就绪,我们现在可以在 game 模块中处理 Hello 消息了。打开 LeafServer game/internal/handler.go,敲入如下代码:</p> <pre> <code class="language-go"><span style="color:rgb(167, 29, 93)">package</span> internal <span style="color:rgb(167, 29, 93)">import</span> ( <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>github.com/name5566/leaf/log<span style="color:rgb(24, 54, 145)">"</span></span> <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>github.com/name5566/leaf/gate<span style="color:rgb(24, 54, 145)">"</span></span> <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>reflect<span style="color:rgb(24, 54, 145)">"</span></span> <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>server/msg<span style="color:rgb(24, 54, 145)">"</span></span> ) <span style="color:rgb(167, 29, 93)">func</span> <span style="color:rgb(121, 93, 163)">init</span>() { <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 向当前模块(game 模块)注册 Hello 消息的消息处理函数 handleHello</span> <span style="color:rgb(0, 134, 179)">handler</span>(&msg.<span style="color:rgb(51, 51, 51)">Hello</span>{}, handleHello) } <span style="color:rgb(167, 29, 93)">func</span> <span style="color:rgb(121, 93, 163)">handler</span>(<span style="color:rgb(237, 106, 67)">m</span> <span style="color:rgb(237, 106, 67)">interface</span>{}, <span style="color:rgb(237, 106, 67)">h</span> <span style="color:rgb(237, 106, 67)">interface</span>{}) { skeleton.<span style="color:rgb(0, 134, 179)">RegisterChanRPC</span>(reflect.<span style="color:rgb(0, 134, 179)">TypeOf</span>(m), h) } <span style="color:rgb(167, 29, 93)">func</span> <span style="color:rgb(121, 93, 163)">handleHello</span>(<span style="color:rgb(237, 106, 67)">args</span> []<span style="color:rgb(237, 106, 67)">interface</span>{}) { <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 收到的 Hello 消息</span> <span style="color:rgb(51, 51, 51)">m</span> <span style="color:rgb(167, 29, 93)">:=</span> args[<span style="color:rgb(0, 134, 179)">0</span>].(*msg.<span style="color:rgb(51, 51, 51)">Hello</span>) <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 消息的发送者</span> <span style="color:rgb(51, 51, 51)">a</span> <span style="color:rgb(167, 29, 93)">:=</span> args[<span style="color:rgb(0, 134, 179)">1</span>].(gate.<span style="color:rgb(51, 51, 51)">Agent</span>) <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 输出收到的消息的内容</span> log.<span style="color:rgb(0, 134, 179)">Debug</span>(<span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>hello <span style="color:rgb(0, 134, 179)">%v</span><span style="color:rgb(24, 54, 145)">"</span></span>, m.<span style="color:rgb(51, 51, 51)">Name</span>) <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 给发送者回应一个 Hello 消息</span> a.<span style="color:rgb(0, 134, 179)">WriteMsg</span>(&msg.<span style="color:rgb(51, 51, 51)">Hello</span>{ Name: <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>client<span style="color:rgb(24, 54, 145)">"</span></span>, }) }</code></pre> <p style="text-align:start">到这里,一个简单的范例就完成了。为了更加清楚的了解消息的格式,我们从 0 编写一个最简单的测试客户端。</p> <p style="text-align:start">Leaf 中,当选择使用 TCP 协议时,在网络中传输的消息都会使用以下格式:</p> <pre style="text-align:start"> <code class="language-go">-------------- | len | data | -------------- </code></pre> <p style="text-align:start">其中:</p> <ol> <li>len 表示了 data 部分的长度(字节数)。len 本身也有长度,默认为 2 字节(可配置),len 本身的长度决定了单个消息的最大大小</li> <li>data 部分使用 JSON 或者 protobuf 编码(也可自定义其他编码方式)</li> </ol> <p style="text-align:start">测试客户端同样使用 Go 语言编写:</p> <pre> <code class="language-go"><span style="color:rgb(167, 29, 93)">package</span> main <span style="color:rgb(167, 29, 93)">import</span> ( <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>encoding/binary<span style="color:rgb(24, 54, 145)">"</span></span> <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>net<span style="color:rgb(24, 54, 145)">"</span></span> ) <span style="color:rgb(167, 29, 93)">func</span> <span style="color:rgb(121, 93, 163)">main</span>() { <span style="color:rgb(51, 51, 51)">conn</span>, <span style="color:rgb(51, 51, 51)">err</span> <span style="color:rgb(167, 29, 93)">:=</span> net.<span style="color:rgb(0, 134, 179)">Dial</span>(<span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>tcp<span style="color:rgb(24, 54, 145)">"</span></span>, <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>127.0.0.1:3563<span style="color:rgb(24, 54, 145)">"</span></span>) <span style="color:rgb(167, 29, 93)">if</span> err != <span style="color:rgb(0, 134, 179)">nil</span> { <span style="color:rgb(0, 134, 179)">panic</span>(err) } <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> Hello 消息(JSON 格式)</span> <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 对应游戏服务器 Hello 消息结构体</span> <span style="color:rgb(51, 51, 51)">data</span> <span style="color:rgb(167, 29, 93)">:=</span> []<span style="color:rgb(167, 29, 93)">byte</span>(<span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">`</span>{</span> <span style="color:rgb(24, 54, 145)"> "Hello": {</span> <span style="color:rgb(24, 54, 145)"> "Name": "leaf"</span> <span style="color:rgb(24, 54, 145)"> }</span> <span style="color:rgb(24, 54, 145)"> }<span style="color:rgb(24, 54, 145)">`</span></span>) <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> len + data</span> <span style="color:rgb(51, 51, 51)">m</span> <span style="color:rgb(167, 29, 93)">:=</span> <span style="color:rgb(0, 134, 179)">make</span>([]<span style="color:rgb(167, 29, 93)">byte</span>, <span style="color:rgb(0, 134, 179)">2</span>+<span style="color:rgb(0, 134, 179)">len</span>(data)) <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 默认使用大端序</span> binary.<span style="color:rgb(51, 51, 51)">BigEndian</span>.<span style="color:rgb(0, 134, 179)">PutUint16</span>(m, <span style="color:rgb(0, 134, 179)">uint16</span>(<span style="color:rgb(0, 134, 179)">len</span>(data))) <span style="color:rgb(0, 134, 179)">copy</span>(m[<span style="color:rgb(0, 134, 179)">2</span>:], data) <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 发送消息</span> conn.<span style="color:rgb(0, 134, 179)">Write</span>(m) }</code></pre> <p style="text-align:start">执行此测试客户端,游戏服务器输出:</p> <pre style="text-align:start"> <code class="language-go">2015/09/25 07:41:03 [debug ] hello leaf 2015/09/25 07:41:03 [debug ] read message: read tcp 127.0.0.1:3563->127.0.0.1:54599: wsarecv: An existing connection was forcibly closed by the remote host. </code></pre> <p style="text-align:start">测试客户端发送完消息以后就退出了,此时和游戏服务器的连接断开,相应的,游戏服务器输出连接断开的提示日志(第二条日志,日志的具体内容和 Go 语言版本有关)。</p> <p style="text-align:start">除了使用 TCP 协议外,还可以选择使用 WebSocket 协议(例如开发 H5 游戏)。Leaf 可以单独使用 TCP 协议或 WebSocket 协议,也可以同时使用两者,换而言之,服务器可以同时接受 TCP 连接和 WebSocket 连接,对开发者而言消息来自 TCP 还是 WebSocket 是完全透明的。现在,我们来编写一个对应上例的使用 WebSocket 协议的客户端:</p> <pre> <code class="language-go"><<span style="color:rgb(99, 163, 92)">script</span> <span style="color:rgb(121, 93, 163)">type</span>=<span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>text/javascript<span style="color:rgb(24, 54, 145)">"</span></span>> <span style="color:rgb(167, 29, 93)">var</span> ws <span style="color:rgb(167, 29, 93)">=</span> <span style="color:rgb(167, 29, 93)">new</span> <span style="color:rgb(121, 93, 163)">WebSocket</span>(<span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">'</span>ws://127.0.0.1:3653<span style="color:rgb(24, 54, 145)">'</span></span>) <span style="color:rgb(51, 51, 51)">ws</span>.<span style="color:rgb(121, 93, 163)">onopen</span> <span style="color:rgb(167, 29, 93)">=</span> <span style="color:rgb(167, 29, 93)">function</span>() { <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 发送 Hello 消息</span> <span style="color:rgb(51, 51, 51)">ws</span>.<span style="color:rgb(0, 134, 179)">send</span>(<span style="color:rgb(0, 134, 179)">JSON</span>.<span style="color:rgb(121, 93, 163)">stringify</span>({Hello<span style="color:rgb(167, 29, 93)">:</span> { Name<span style="color:rgb(167, 29, 93)">:</span> <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">'</span>leaf<span style="color:rgb(24, 54, 145)">'</span></span> }})) } </<span style="color:rgb(99, 163, 92)">script</span>></code></pre> <p style="text-align:start">保存上述代码到某 HTML 文件中并使用(任意支持 WebSocket 协议的)浏览器打开。在打开此 HTML 文件前,首先需要配置一下 LeafServer 的 bin/conf/server.json 文件,增加 WebSocket 监听地址(WSAddr):</p> <pre> <code class="language-go">{ <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>LogLevel<span style="color:rgb(24, 54, 145)">"</span></span>: <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>debug<span style="color:rgb(24, 54, 145)">"</span></span>, <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>LogPath<span style="color:rgb(24, 54, 145)">"</span></span>: <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span><span style="color:rgb(24, 54, 145)">"</span></span>, <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>TCPAddr<span style="color:rgb(24, 54, 145)">"</span></span>: <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>127.0.0.1:3563<span style="color:rgb(24, 54, 145)">"</span></span>, <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>WSAddr<span style="color:rgb(24, 54, 145)">"</span></span>: <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>127.0.0.1:3653<span style="color:rgb(24, 54, 145)">"</span></span>, <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>MaxConnNum<span style="color:rgb(24, 54, 145)">"</span></span>: <span style="color:rgb(0, 134, 179)">20000</span> }</code></pre> <p style="text-align:start">重启游戏服务器后,方可接受 WebSocket 消息:</p> <pre style="text-align:start"> <code class="language-go">2015/09/25 07:50:03 [debug ] hello leaf </code></pre> <p style="text-align:start">在 Leaf 中使用 WebSocket 需要注意的一点是:Leaf 总是发送二进制消息而非文本消息。</p> <h3 style="text-align:start">Leaf 模块详解</h3> <p style="text-align:start">LeafServer 中包含了 3 个模块,它们分别是:</p> <ul> <li>gate 模块,负责游戏客户端的接入</li> <li>login 模块,负责登录流程</li> <li>game 模块,负责游戏主逻辑</li> </ul> <p style="text-align:start">一般来说(而非强制规定),从代码结构上,一个 Leaf 模块:</p> <ol> <li>放置于一个目录中(例如 game 模块放置于 game 目录中)</li> <li>模块的具体实现放置于 internal 包中(例如 game 模块的具体实现放置于 game/internal 包中)</li> </ol> <p style="text-align:start">每个模块下一般有一个 external.go 的文件,顾名思义表示模块对外暴露的接口,这里以 game 模块的 external.go 文件为例:</p> <pre> <code class="language-go"><span style="color:rgb(167, 29, 93)">package</span> game <span style="color:rgb(167, 29, 93)">import</span> ( <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>server/game/internal<span style="color:rgb(24, 54, 145)">"</span></span> ) <span style="color:rgb(167, 29, 93)">var</span> ( <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 实例化 game 模块</span> <span style="color:rgb(237, 106, 67)">Module</span> = <span style="color:rgb(0, 134, 179)">new</span>(internal.<span style="color:rgb(51, 51, 51)">Module</span>) <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 暴露 ChanRPC</span> <span style="color:rgb(237, 106, 67)">ChanRPC</span> = internal.<span style="color:rgb(51, 51, 51)">ChanRPC</span> )</code></pre> <p style="text-align:start">首先,模块会被实例化,这样才能注册到 Leaf 框架中(详见 LeafServer main.go),另外,模块暴露的 ChanRPC 被用于模块间通讯。</p> <p style="text-align:start">进入 game 模块的内部(LeafServer game/internal/module.go):</p> <pre> <code class="language-go"><span style="color:rgb(167, 29, 93)">package</span> internal <span style="color:rgb(167, 29, 93)">import</span> ( <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>github.com/name5566/leaf/module<span style="color:rgb(24, 54, 145)">"</span></span> <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>server/base<span style="color:rgb(24, 54, 145)">"</span></span> ) <span style="color:rgb(167, 29, 93)">var</span> ( skeleton = base.<span style="color:rgb(0, 134, 179)">NewSkeleton</span>() <span style="color:rgb(237, 106, 67)">ChanRPC</span> = skeleton.<span style="color:rgb(51, 51, 51)">ChanRPCServer</span> ) <span style="color:rgb(167, 29, 93)">type</span> <span style="color:rgb(237, 106, 67)">Module</span> <span style="color:rgb(167, 29, 93)">struct</span> { *module.<span style="color:rgb(51, 51, 51)">Skeleton</span> } <span style="color:rgb(167, 29, 93)">func</span> <span style="color:rgb(121, 93, 163)">(<span style="color:rgb(237, 106, 67)">m</span> *<span style="color:rgb(237, 106, 67)">Module</span>) <span style="color:rgb(121, 93, 163)">OnInit</span></span>() { m.<span style="color:rgb(51, 51, 51)">Skeleton</span> = skeleton } <span style="color:rgb(167, 29, 93)">func</span> <span style="color:rgb(121, 93, 163)">(<span style="color:rgb(237, 106, 67)">m</span> *<span style="color:rgb(237, 106, 67)">Module</span>) <span style="color:rgb(121, 93, 163)">OnDestroy</span></span>() { }</code></pre> <p style="text-align:start">模块中最关键的就是 skeleton(骨架),skeleton 实现了 Module 接口的 Run 方法并提供了:</p> <ul> <li>ChanRPC</li> <li>goroutine</li> <li>定时器</li> </ul> <h3 style="text-align:start">Leaf ChanRPC</h3> <p style="text-align:start">由于 Leaf 中,每个模块跑在独立的 goroutine 上,为了模块间方便的相互调用就有了基于 channel 的 RPC 机制。一个 ChanRPC 需要在游戏服务器初始化的时候进行注册(注册过程不是 goroutine 安全的),例如 LeafServer 中 game 模块注册了 NewAgent 和 CloseAgent 两个 ChanRPC:</p> <pre> <code class="language-go"><span style="color:rgb(167, 29, 93)">package</span> internal <span style="color:rgb(167, 29, 93)">import</span> ( <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>github.com/name5566/leaf/gate<span style="color:rgb(24, 54, 145)">"</span></span> ) <span style="color:rgb(167, 29, 93)">func</span> <span style="color:rgb(121, 93, 163)">init</span>() { skeleton.<span style="color:rgb(0, 134, 179)">RegisterChanRPC</span>(<span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>NewAgent<span style="color:rgb(24, 54, 145)">"</span></span>, rpcNewAgent) skeleton.<span style="color:rgb(0, 134, 179)">RegisterChanRPC</span>(<span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>CloseAgent<span style="color:rgb(24, 54, 145)">"</span></span>, rpcCloseAgent) } <span style="color:rgb(167, 29, 93)">func</span> <span style="color:rgb(121, 93, 163)">rpcNewAgent</span>(<span style="color:rgb(237, 106, 67)">args</span> []<span style="color:rgb(237, 106, 67)">interface</span>{}) { } <span style="color:rgb(167, 29, 93)">func</span> <span style="color:rgb(121, 93, 163)">rpcCloseAgent</span>(<span style="color:rgb(237, 106, 67)">args</span> []<span style="color:rgb(237, 106, 67)">interface</span>{}) { }</code></pre> <p style="text-align:start">使用 skeleton 来注册 ChanRPC。RegisterChanRPC 的第一个参数是 ChanRPC 的名字,第二个参数是 ChanRPC 的实现。这里的 NewAgent 和 CloseAgent 会被 LeafServer 的 gate 模块在连接建立和连接中断时调用。ChanRPC 的调用方有 3 种调用模式:</p> <ol> <li>同步模式,调用并等待 ChanRPC 返回</li> <li>异步模式,调用并提供回调函数,回调函数会在 ChanRPC 返回后被调用</li> <li>Go 模式,调用并立即返回,忽略任何返回值和错误</li> </ol> <p style="text-align:start">gate 模块这样调用 game 模块的 NewAgent ChanRPC(这仅仅是一个示例,实际的代码细节复杂的多):</p> <pre> <code class="language-go">game.<span style="color:rgb(51, 51, 51)">ChanRPC</span>.<span style="color:rgb(0, 134, 179)">Go</span>(<span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>NewAgent<span style="color:rgb(24, 54, 145)">"</span></span>, a)</code></pre> <p style="text-align:start">这里调用 NewAgent 并传递参数 a,我们在 rpcNewAgent 的参数 args[0] 中可以取到 a(args[1] 表示第二个参数,以此类推)。</p> <p style="text-align:start">更加详细的用法可以参考 <a href="/misc/goto?guid=4959647773209038467" style="box-sizing: border-box; background-color: transparent; color: rgb(64, 120, 192); text-decoration: none;">leaf/chanrpc</a>。需要注意的是,无论封装多么精巧,跨 goroutine 的调用总不能像直接的函数调用那样简单直接,因此除非必要我们不要构建太多的模块,模块间不要太频繁的交互。模块在 Leaf 中被设计出来最主要是用于划分功能而非利用多核,Leaf 认为在模块内按需使用 goroutine 才是多核利用率问题的解决之道。</p> <h3 style="text-align:start">Leaf Go</h3> <p style="text-align:start">善用 goroutine 能够充分利用多核资源,Leaf 提供的 Go 机制解决了原生 goroutine 存在的一些问题:</p> <ul> <li>能够恢复 goroutine 运行过程中的错误</li> <li>游戏服务器会等待所有 goroutine 执行结束后才关闭</li> <li>非常方便的获取 goroutine 执行的结果数据</li> <li>在一些特殊场合保证 goroutine 按创建顺序执行</li> </ul> <p style="text-align:start">我们来看一个例子(可以在 LeafServer 的模块的 OnInit 方法中测试):</p> <pre> <code class="language-go">log.<span style="color:rgb(0, 134, 179)">Debug</span>(<span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>1<span style="color:rgb(24, 54, 145)">"</span></span>) <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 定义变量 res 接收结果</span> <span style="color:rgb(167, 29, 93)">var</span> <span style="color:rgb(51, 51, 51)">res</span> <span style="color:rgb(167, 29, 93)">string</span> skeleton.<span style="color:rgb(0, 134, 179)">Go</span>(<span style="color:rgb(167, 29, 93)">func</span>() { <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 这里使用 Sleep 来模拟一个很慢的操作</span> time.<span style="color:rgb(0, 134, 179)">Sleep</span>(<span style="color:rgb(0, 134, 179)">1</span> * time.<span style="color:rgb(51, 51, 51)">Second</span>) <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 假定得到结果</span> res = <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>3<span style="color:rgb(24, 54, 145)">"</span></span> }, <span style="color:rgb(0, 134, 179)">func</span>() { log.<span style="color:rgb(0, 134, 179)">Debug</span>(res) }) log.<span style="color:rgb(0, 134, 179)">Debug</span>(<span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>2<span style="color:rgb(24, 54, 145)">"</span></span>)</code></pre> <p style="text-align:start">上面代码执行结果如下:</p> <pre> <code class="language-go"><span style="color:rgb(0, 134, 179)">2015</span>/<span style="color:rgb(0, 134, 179)">08</span>/<span style="color:rgb(0, 134, 179)">27</span> <span style="color:rgb(0, 134, 179)">20</span>:<span style="color:rgb(0, 134, 179)">37</span>:<span style="color:rgb(0, 134, 179)">17</span> [debug ] <span style="color:rgb(0, 134, 179)">1</span> <span style="color:rgb(0, 134, 179)">2015</span>/<span style="color:rgb(0, 134, 179)">08</span>/<span style="color:rgb(0, 134, 179)">27</span> <span style="color:rgb(0, 134, 179)">20</span>:<span style="color:rgb(0, 134, 179)">37</span>:<span style="color:rgb(0, 134, 179)">17</span> [debug ] <span style="color:rgb(0, 134, 179)">2</span> <span style="color:rgb(0, 134, 179)">2015</span>/<span style="color:rgb(0, 134, 179)">08</span>/<span style="color:rgb(0, 134, 179)">27</span> <span style="color:rgb(0, 134, 179)">20</span>:<span style="color:rgb(0, 134, 179)">37</span>:<span style="color:rgb(0, 134, 179)">18</span> [debug ] <span style="color:rgb(0, 134, 179)">3</span></code></pre> <p style="text-align:start">这里的 Go 方法接收 2 个函数作为参数,第一个函数会被放置在一个新创建的 goroutine 中执行,在其执行完成之后,第二个函数会在当前 goroutine 中被执行。由此,我们可以看到变量 res 同一时刻总是只被一个 goroutine 访问,这就避免了同步机制的使用。Go 的设计使得 CPU 得到充分利用,避免操作阻塞当前 goroutine,同时又无需为共享资源同步而忧心。</p> <p style="text-align:start">更加详细的用法可以参考 <a href="/misc/goto?guid=4959647773292263907" style="box-sizing: border-box; background-color: transparent; color: rgb(64, 120, 192); text-decoration: none;">leaf/go</a>。</p> <h3 style="text-align:start">Leaf timer</h3> <p style="text-align:start">Go 语言标准库提供了定时器的支持:</p> <pre> <code class="language-go"><span style="color:rgb(167, 29, 93)">func</span> <span style="color:rgb(237, 106, 67)">AfterFunc</span>(d <span style="color:rgb(237, 106, 67)">Duration</span>, f <span style="color:rgb(167, 29, 93)">func</span>()) *Timer</code></pre> <p style="text-align:start">AfterFunc 会等待 d 时长后调用 f 函数,这里的 f 函数将在另外一个 goroutine 中执行。Leaf 提供了一个相同的 AfterFunc 函数,相比之下,f 函数在 AfterFunc 的调用 goroutine 中执行,这样就避免了同步机制的使用:</p> <pre> <code class="language-go">skeleton.<span style="color:rgb(0, 134, 179)">AfterFunc</span>(<span style="color:rgb(0, 134, 179)">5</span> * time.<span style="color:rgb(51, 51, 51)">Second</span>, <span style="color:rgb(0, 134, 179)">func</span>() { <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> ...</span> })</code></pre> <p style="text-align:start">另外,Leaf timer 还支持 <a href="/misc/goto?guid=4959647773379496057" style="box-sizing: border-box; background-color: transparent; color: rgb(64, 120, 192); text-decoration: none;">cron 表达式</a>,用于实现诸如“每天 9 点执行”、“每周末 6 点执行”的逻辑。</p> <p style="text-align:start">更加详细的用法可以参考 <a href="/misc/goto?guid=4959647773450514045" style="box-sizing: border-box; background-color: transparent; color: rgb(64, 120, 192); text-decoration: none;">leaf/timer</a>。</p> <h3 style="text-align:start">Leaf log</h3> <p style="text-align:start">Leaf 的 log 系统支持多种日志级别:</p> <ol> <li>Debug 日志,非关键日志</li> <li>Release 日志,关键日志</li> <li>Error 日志,错误日志</li> <li>Fatal 日志,致命错误日志</li> </ol> <p style="text-align:start">Debug < Release < Error < Fatal(日志级别高低)</p> <p style="text-align:start">在 LeafServer 中,bin/conf/server.json 可以配置日志级别,低于配置的日志级别的日志将不会输出。Fatal 日志比较特殊,每次输出 Fatal 日志之后游戏服务器进程就会结束,通常来说,只在游戏服务器初始化失败时使用 Fatal 日志。</p> <p style="text-align:start">更加详细的用法可以参考 <a href="/misc/goto?guid=4959647773534084335" style="box-sizing: border-box; background-color: transparent; color: rgb(64, 120, 192); text-decoration: none;">leaf/log</a>。</p> <h3 style="text-align:start">Leaf recordfile</h3> <p style="text-align:start">Leaf 的 recordfile 是基于 CSV 格式(范例见<a href="/misc/goto?guid=4959647773620083045" style="box-sizing: border-box; background-color: transparent; color: rgb(64, 120, 192); text-decoration: none;">这里</a>)。recordfile 用于管理游戏配置数据。在 LeafServer 中使用 recordfile 非常简单:</p> <ol> <li>将 CSV 文件放置于 bin/gamedata 目录中</li> <li>在 gamedata 模块中调用函数 readRf 读取 CSV 文件</li> </ol> <p style="text-align:start">范例:</p> <pre> <code class="language-go"><span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 确保 bin/gamedata 目录中存在 Test.txt 文件</span> <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 文件名必须和此结构体名称相同(大小写敏感)</span> <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 结构体的一个实例映射 recordfile 中的一行</span> <span style="color:rgb(167, 29, 93)">type</span> <span style="color:rgb(237, 106, 67)">Test</span> <span style="color:rgb(167, 29, 93)">struct</span> { <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 将第一列按 int 类型解析</span> <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> "index" 表明在此列上建立唯一索引</span> <span style="color:rgb(237, 106, 67)">Id</span> <span style="color:rgb(167, 29, 93)">int</span> <span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span>index<span style="color:rgb(24, 54, 145)">"</span></span> <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 将第二列解析为长度为 4 的整型数组</span> <span style="color:rgb(237, 106, 67)">Arr</span> [<span style="color:rgb(0, 134, 179)">4</span>]<span style="color:rgb(167, 29, 93)">int</span> <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 将第三列解析为字符串</span> <span style="color:rgb(237, 106, 67)">Str</span> <span style="color:rgb(167, 29, 93)">string</span> } <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 读取 recordfile Test.txt 到内存中</span> <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> RfTest 即为 Test.txt 的内存镜像</span> <span style="color:rgb(167, 29, 93)">var</span> <span style="color:rgb(51, 51, 51)">RfTest</span> = <span style="color:rgb(0, 134, 179)">readRf</span>(Test{}) <span style="color:rgb(167, 29, 93)">func</span> <span style="color:rgb(121, 93, 163)">init</span>() { <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 按索引查找</span> <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 获取 Test.txt 中 Id 为 1 的那一行</span> <span style="color:rgb(51, 51, 51)">r</span> <span style="color:rgb(167, 29, 93)">:=</span> RfTest.<span style="color:rgb(0, 134, 179)">Index</span>(<span style="color:rgb(0, 134, 179)">1</span>) <span style="color:rgb(167, 29, 93)">if</span> r != <span style="color:rgb(0, 134, 179)">nil</span> { <span style="color:rgb(51, 51, 51)">row</span> <span style="color:rgb(167, 29, 93)">:=</span> r.(*Test) <span style="color:rgb(150, 152, 150)"><span style="color:rgb(150, 152, 150)">//</span> 输出此行的所有列的数据</span> log.<span style="color:rgb(0, 134, 179)">Debug</span>(<span style="color:rgb(24, 54, 145)"><span style="color:rgb(24, 54, 145)">"</span><span style="color:rgb(0, 134, 179)">%v</span> <span style="color:rgb(0, 134, 179)">%v</span> <span style="color:rgb(0, 134, 179)">%v</span><span style="color:rgb(24, 54, 145)">"</span></span>, row.<span style="color:rgb(51, 51, 51)">Id</span>, row.<span style="color:rgb(51, 51, 51)">Arr</span>, row.<span style="color:rgb(51, 51, 51)">Str</span>) } }</code></pre> <p style="text-align:start">更加详细的用法可以参考 <a href="/misc/goto?guid=4959647773707067636" style="box-sizing: border-box; background-color: transparent; color: rgb(64, 120, 192); text-decoration: none;">leaf/recordfile</a>。</p> <h2 style="text-align:start">了解更多</h2> <p style="text-align:start">阅读 Wiki 获取更多的帮助:<a href="/misc/goto?guid=4959737752388868699" style="box-sizing: border-box; background-color: transparent; color: rgb(64, 120, 192); text-decoration: none;">https://github.com/name5566/leaf/wiki</a></p>