Leaf 是一个由 Go 语言(golang)编写的开发效率和执行效率并重的开源游戏服务器框架。Leaf 适用于各类游戏服务器的开发,包括 H5(HTML5)游戏服务器。

Leaf 的关注点:

  • 良好的使用体验。Leaf 总是尽可能的提供简洁和易用的接口,尽可能的提升开发的效率
  • 稳定性。Leaf 总是尽可能的恢复运行过程中的错误,避免崩溃
  • 多核支持。Leaf 通过模块机制和 leaf/go 尽可能的利用多核资源,同时又尽量避免各种副作用
  • 模块机制。

Leaf 的模块机制

一个 Leaf 开发的游戏服务器由多个模块组成(例如 LeafServer),模块有以下特点:

  • 每个模块运行在一个单独的 goroutine 中
  • 模块间通过一套轻量的 RPC 机制通讯(leaf/chanrpc)

Leaf 不建议在游戏服务器中设计过多的模块。

游戏服务器在启动时进行模块的注册,例如:

leaf.Run( game.Module, gate.Module, login.Module, )
1
2
3
4
5

leaf.Run(
    game.Module,
    gate.Module,
    login.Module,
)

这里按顺序注册了 game、gate、login 三个模块。每个模块都需要实现接口:

type Module interface { OnInit() OnDestroy() Run(closeSig chan bool) }
1
2
3
4
5

typeModuleinterface{
    OnInit()
    OnDestroy()
    Run(closeSig chan bool)
}

Leaf 首先会在同一个 goroutine 中按模块注册顺序执行模块的 OnInit 方法,等到所有模块 OnInit 方法执行完成后则为每一个模块启动一个 goroutine 并执行模块的 Run 方法。最后,游戏服务器关闭时(Ctrl + C 关闭游戏服务器)将按模块注册相反顺序在同一个 goroutine 中执行模块的 OnDestroy 方法。

Leaf 源码概览

  • leaf/chanrpc 提供了一套基于 channel 的 RPC 机制,用于游戏服务器模块间通讯
  • leaf/db 数据库相关,目前支持 MongoDB
  • leaf/gate 网关模块,负责游戏客户端的接入
  • leaf/go 用于创建能够被 Leaf 管理的 goroutine
  • leaf/log 日志相关
  • leaf/network 网络相关,使用 TCP 和 WebSocket 协议,可自定义消息格式,默认 Leaf 提供了基于 protobuf 和 JSON 的消息格式
  • leaf/recordfile 用于管理游戏数据
  • leaf/timer 定时器相关
  • leaf/util 辅助库

使用 Leaf 开发游戏服务器

LeafServer 是一个基于 Leaf 开发的游戏服务器,我们以 LeafServer 作为起点。

获取 LeafServer:

git clone https://github.com/name5566/leafserver
1
git clonehttps://github.com/name5566/leafserver

设置 leafserver 目录到 GOPATH 环境变量后获取 Leaf:

go get github.com/name5566/leaf
1
go get github.com/name5566/leaf

编译 LeafServer:

go install server
1
go install server

如果一切顺利,运行 server 你可以获得以下输出:

2015/08/26 22:11:27 [release] Leaf 1.1.2 starting up
1
2015/08/2622:11:27[release]Leaf1.1.2starting up

敲击 Ctrl + C 关闭游戏服务器,服务器正常关闭输出

2015/08/26 22:12:30 [release] Leaf closing down (signal: interrupt)
1
2015/08/2622:12:30[release]Leaf closing down(signal:interrupt)

Hello Leaf

现在,在 LeafServer 的基础上,我们来看看游戏服务器如何接收和处理网络消息。

首先定义一个 JSON 格式的消息(protobuf 类似)。打开 LeafServer msg/msg.go 文件可以看到如下代码:

package msg import ( "github.com/name5566/leaf/network" ) var Processor network.Processor func init() { }
1
2
3
4
5
6
7
8
9
10
11

packagemsg
import(
    "github.com/name5566/leaf/network"
)
varProcessor network.Processor
func init(){
}

Processor 为消息的处理器(可由用户自定义),这里我们使用 Leaf 默认提供的 JSON 消息处理器并尝试添加一个名字为 Hello 的消息:

package msg import ( "github.com/name5566/leaf/network/json" ) // 使用默认的 JSON 消息处理器(默认还提供了 protobuf 消息处理器) var Processor = json.NewProcessor() func init() { // 这里我们注册了一个 JSON 消息 Hello Processor.Register(&Hello{}) } // 一个结构体定义了一个 JSON 消息的格式 // 消息名为 Hello type Hello struct { Name string }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

packagemsg
import(
    "github.com/name5566/leaf/network/json"
)
// 使用默认的 JSON 消息处理器(默认还提供了 protobuf 消息处理器)
varProcessor=json.NewProcessor()
func init(){
    // 这里我们注册了一个 JSON 消息 Hello
    Processor.Register(&Hello{})
}
// 一个结构体定义了一个 JSON 消息的格式
// 消息名为 Hello
typeHellostruct{
    Name string
}

客户端发送到游戏服务器的消息需要通过 gate 模块路由,简而言之,gate 模块决定了某个消息具体交给内部的哪个模块来处理。这里,我们将 Hello 消息路由到 game 模块中。打开 LeafServer gate/router.go,敲入如下代码:

package gate import ( "server/game" "server/msg" ) func init() { // 这里指定消息 Hello 路由到 game 模块 // 模块间使用 ChanRPC 通讯,消息路由也不例外 msg.Processor.SetRouter(&msg.Hello{}, game.ChanRPC) }
1
2
3
4
5
6
7
8
9
10
11
12

packagegate
import(
    "server/game"
    "server/msg"
)
func init(){
    // 这里指定消息 Hello 路由到 game 模块
    // 模块间使用 ChanRPC 通讯,消息路由也不例外
    msg.Processor.SetRouter(&msg.Hello{},game.ChanRPC)
}

一切就绪,我们现在可以在 game 模块中处理 Hello 消息了。打开 LeafServer game/internal/handler.go,敲入如下代码:

package internal import ( "github.com/name5566/leaf/log" "github.com/name5566/leaf/gate" "reflect" "server/msg" ) func init() { // 向当前模块(game 模块)注册 Hello 消息的消息处理函数 handleHello handler(&msg.Hello{}, handleHello) } func handler(m interface{}, h interface{}) { skeleton.RegisterChanRPC(reflect.TypeOf(m), h) } func handleHello(args []interface{}) { // 收到的 Hello 消息 m := args[0].(*msg.Hello) // 消息的发送者 a := args[1].(gate.Agent) // 输出收到的消息的内容 log.Debug("hello %v", m.Name) // 给发送者回应一个 Hello 消息 a.WriteMsg(&msg.Hello{ Name: "client", }) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

packageinternal
import(
    "github.com/name5566/leaf/log"
    "github.com/name5566/leaf/gate"
    "reflect"
    "server/msg"
)
func init(){
    // 向当前模块(game 模块)注册 Hello 消息的消息处理函数 handleHello
    handler(&msg.Hello{},handleHello)
}
funchandler(minterface{},hinterface{}){
    skeleton.RegisterChanRPC(reflect.TypeOf(m),h)
}
funchandleHello(args[]interface{}){
    // 收到的 Hello 消息
    m:=args[0].(*msg.Hello)
    // 消息的发送者
    a:=args[1].(gate.Agent)
    // 输出收到的消息的内容
    log.Debug("hello %v",m.Name)
    // 给发送者回应一个 Hello 消息
    a.WriteMsg(&msg.Hello{
        Name:"client",
    })
}

到这里,一个简单的范例就完成了。为了更加清楚的了解消息的格式,我们从 0 编写一个最简单的测试客户端。

Leaf 中,当选择使用 TCP 协议时,在网络中传输的消息都会使用以下格式:

-------------- | len | data | --------------
1
2
3

--------------
|len|data|
--------------

其中:

  1. len 表示了 data 部分的长度(字节数)。len 本身也有长度,默认为 2 字节(可配置),len 本身的长度决定了单个消息的最大大小
  2. data 部分使用 JSON 或者 protobuf 编码(也可自定义其他编码方式)

测试客户端同样使用 Go 语言编写:

package main import ( "encoding/binary" "net" ) func main() { conn, err := net.Dial("tcp", "127.0.0.1:3563") if err != nil { panic(err) } // Hello 消息(JSON 格式) // 对应游戏服务器 Hello 消息结构体 data := []byte(`{ "Hello": { "Name": "leaf" } }`) // len + data m := make([]byte, 2+len(data)) // 默认使用大端序 binary.BigEndian.PutUint16(m, uint16(len(data))) copy(m[2:], data) // 发送消息 conn.Write(m) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

packagemain
import(
    "encoding/binary"
    "net"
)
func main(){
    conn,err:=net.Dial("tcp","127.0.0.1:3563")
    iferr!=nil{
        panic(err)
    }
    // Hello 消息(JSON 格式)
    // 对应游戏服务器 Hello 消息结构体
    data:=[]byte(`{
        "Hello":{
            "Name":"leaf"
        }
    }`)
    // len + data
    m:=make([]byte,2+len(data))
    // 默认使用大端序
    binary.BigEndian.PutUint16(m,uint16(len(data)))
    copy(m[2:],data)
    // 发送消息
    conn.Write(m)
}

执行此测试客户端,游戏服务器输出:

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.
1
2
3

2015/09/2507:41:03[debug  ]hello leaf
2015/09/2507:41:03[debug  ]read message:read tcp127.0.0.1:3563->127.0.0.1:54599:
wsarecv:An existing connection was forcibly closed by the remote host.

测试客户端发送完消息以后就退出了,此时和游戏服务器的连接断开,相应的,游戏服务器输出连接断开的提示日志(第二条日志,日志的具体内容和 Go 语言版本有关)。

除了使用 TCP 协议外,还可以选择使用 WebSocket 协议(例如开发 H5 游戏)。Leaf 可以单独使用 TCP 协议或 WebSocket 协议,也可以同时使用两者,换而言之,服务器可以同时接受 TCP 连接和 WebSocket 连接,对开发者而言消息来自 TCP 还是 WebSocket 是完全透明的。现在,我们来编写一个对应上例的使用 WebSocket 协议的客户端:

<script type="text/javascript"> var ws = new WebSocket('ws://127.0.0.1:3653') ws.onopen = function() { // 发送 Hello 消息 ws.send(JSON.stringify({Hello: { Name: 'leaf' }})) } </script>
1
2
3
4
5
6
7
8
9
10

<script type="text/javascript">
varws=newWebSocket('ws://127.0.0.1:3653')
ws.onopen=function(){
    // 发送 Hello 消息
    ws.send(JSON.stringify({Hello:{
        Name:'leaf'
    }}))
}
</script>

保存上述代码到某 HTML 文件中并使用(任意支持 WebSocket 协议的)浏览器打开。在打开此 HTML 文件前,首先需要配置一下 LeafServer 的 bin/conf/server.json 文件,增加 WebSocket 监听地址(WSAddr):

{ "LogLevel": "debug", "LogPath": "", "TCPAddr": "127.0.0.1:3563", "WSAddr": "127.0.0.1:3653", "MaxConnNum": 20000 }
1
2
3
4
5
6
7

{
    "LogLevel":"debug",
    "LogPath":"",
    "TCPAddr":"127.0.0.1:3563",
    "WSAddr":"127.0.0.1:3653",
    "MaxConnNum":20000
}

重启游戏服务器后,方可接受 WebSocket 消息:

2015/09/25 07:50:03 [debug ] hello leaf
1
2015/09/2507:50:03[debug  ]hello leaf

在 Leaf 中使用 WebSocket 需要注意的一点是:Leaf 总是发送二进制消息而非文本消息。

Leaf 模块详解

LeafServer 中包含了 3 个模块,它们分别是:

  • gate 模块,负责游戏客户端的接入
  • login 模块,负责登录流程
  • game 模块,负责游戏主逻辑

一般来说(而非强制规定),从代码结构上,一个 Leaf 模块:

  1. 放置于一个目录中(例如 game 模块放置于 game 目录中)
  2. 模块的具体实现放置于 internal 包中(例如 game 模块的具体实现放置于 game/internal 包中)

每个模块下一般有一个 external.go 的文件,顾名思义表示模块对外暴露的接口,这里以 game 模块的 external.go 文件为例:

package game import ( "server/game/internal" ) var ( // 实例化 game 模块 Module = new(internal.Module) // 暴露 ChanRPC ChanRPC = internal.ChanRPC )
1
2
3
4
5
6
7
8
9
10
11
12

packagegame
import(
    "server/game/internal"
)
var(
    // 实例化 game 模块
    Module  =new(internal.Module)
    // 暴露 ChanRPC
    ChanRPC=internal.ChanRPC
)

首先,模块会被实例化,这样才能注册到 Leaf 框架中(详见 LeafServer main.go),另外,模块暴露的 ChanRPC 被用于模块间通讯。

进入 game 模块的内部(LeafServer game/internal/module.go):

package internal import ( "github.com/name5566/leaf/module" "server/base" ) var ( skeleton = base.NewSkeleton() ChanRPC = skeleton.ChanRPCServer ) type Module struct { *module.Skeleton } func (m *Module) OnInit() { m.Skeleton = skeleton } func (m *Module) OnDestroy() { }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

packageinternal
import(
    "github.com/name5566/leaf/module"
    "server/base"
)
var(
    skeleton=base.NewSkeleton()
    ChanRPC  =skeleton.ChanRPCServer
)
typeModulestruct{
    *module.Skeleton
}
func(m *Module)OnInit(){
    m.Skeleton=skeleton
}
func(m *Module)OnDestroy(){
}

模块中最关键的就是 skeleton(骨架),skeleton 实现了 Module 接口的 Run 方法并提供了:

  • ChanRPC
  • goroutine
  • 定时器

Leaf ChanRPC

由于 Leaf 中,每个模块跑在独立的 goroutine 上,为了模块间方便的相互调用就有了基于 channel 的 RPC 机制。一个 ChanRPC 需要在游戏服务器初始化的时候进行注册(注册过程不是 goroutine 安全的),例如 LeafServer 中 game 模块注册了 NewAgent 和 CloseAgent 两个 ChanRPC:

package internal import ( "github.com/name5566/leaf/gate" ) func init() { skeleton.RegisterChanRPC("NewAgent", rpcNewAgent) skeleton.RegisterChanRPC("CloseAgent", rpcCloseAgent) } func rpcNewAgent(args []interface{}) { } func rpcCloseAgent(args []interface{}) { }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

packageinternal
import(
    "github.com/name5566/leaf/gate"
)
func init(){
    skeleton.RegisterChanRPC("NewAgent",rpcNewAgent)
    skeleton.RegisterChanRPC("CloseAgent",rpcCloseAgent)
}
funcrpcNewAgent(args[]interface{}){
}
funcrpcCloseAgent(args[]interface{}){
}

使用 skeleton 来注册 ChanRPC。RegisterChanRPC 的第一个参数是 ChanRPC 的名字,第二个参数是 ChanRPC 的实现。这里的 NewAgent 和 CloseAgent 会被 LeafServer 的 gate 模块在连接建立和连接中断时调用。ChanRPC 的调用方有 3 种调用模式:

  1. 同步模式,调用并等待 ChanRPC 返回
  2. 异步模式,调用并提供回调函数,回调函数会在 ChanRPC 返回后被调用
  3. Go 模式,调用并立即返回,忽略任何返回值和错误

gate 模块这样调用 game 模块的 NewAgent ChanRPC(这仅仅是一个示例,实际的代码细节复杂的多):

game.ChanRPC.Go("NewAgent", a)
1
game.ChanRPC.Go("NewAgent",a)

这里调用 NewAgent 并传递参数 a,我们在 rpcNewAgent 的参数 args[0] 中可以取到 a(args[1] 表示第二个参数,以此类推)。

更加详细的用法可以参考 leaf/chanrpc。需要注意的是,无论封装多么精巧,跨 goroutine 的调用总不能像直接的函数调用那样简单直接,因此除非必要我们不要构建太多的模块,模块间不要太频繁的交互。模块在 Leaf 中被设计出来最主要是用于划分功能而非利用多核,Leaf 认为在模块内按需使用 goroutine 才是多核利用率问题的解决之道。

Leaf Go

善用 goroutine 能够充分利用多核资源,Leaf 提供的 Go 机制解决了原生 goroutine 存在的一些问题:

  • 能够恢复 goroutine 运行过程中的错误
  • 游戏服务器会等待所有 goroutine 执行结束后才关闭
  • 非常方便的获取 goroutine 执行的结果数据
  • 在一些特殊场合保证 goroutine 按创建顺序执行

我们来看一个例子(可以在 LeafServer 的模块的 OnInit 方法中测试):

log.Debug("1") // 定义变量 res 接收结果 var res string skeleton.Go(func() { // 这里使用 Sleep 来模拟一个很慢的操作 time.Sleep(1 * time.Second) // 假定得到结果 res = "3" }, func() { log.Debug(res) }) log.Debug("2")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

log.Debug("1")
// 定义变量 res 接收结果
varres string
skeleton.Go(func(){
    // 这里使用 Sleep 来模拟一个很慢的操作
    time.Sleep(1*time.Second)
    // 假定得到结果
    res="3"
},func(){
    log.Debug(res)
})
log.Debug("2")

上面代码执行结果如下:

2015/08/27 20:37:17 [debug ] 1 2015/08/27 20:37:17 [debug ] 2 2015/08/27 20:37:18 [debug ] 3
1
2
3

2015/08/2720:37:17[debug  ]1
2015/08/2720:37:17[debug  ]2
2015/08/2720:37:18[debug  ]3

这里的 Go 方法接收 2 个函数作为参数,第一个函数会被放置在一个新创建的 goroutine 中执行,在其执行完成之后,第二个函数会在当前 goroutine 中被执行。由此,我们可以看到变量 res 同一时刻总是只被一个 goroutine 访问,这就避免了同步机制的使用。Go 的设计使得 CPU 得到充分利用,避免操作阻塞当前 goroutine,同时又无需为共享资源同步而忧心。

更加详细的用法可以参考 leaf/go。

Leaf timer

Go 语言标准库提供了定时器的支持:

func AfterFunc(d Duration, f func()) *Timer
1
func AfterFunc(dDuration,ffunc())*Timer

AfterFunc 会等待 d 时长后调用 f 函数,这里的 f 函数将在另外一个 goroutine 中执行。Leaf 提供了一个相同的 AfterFunc 函数,相比之下,f 函数在 AfterFunc 的调用 goroutine 中执行,这样就避免了同步机制的使用:

skeleton.AfterFunc(5 * time.Second, func() { // ... })
1
2
3

skeleton.AfterFunc(5*time.Second,func(){
    // ...
})

另外,Leaf timer 还支持 cron 表达式,用于实现诸如“每天 9 点执行”、“每周末 6 点执行”的逻辑。

更加详细的用法可以参考 leaf/timer。

Leaf log

Leaf 的 log 系统支持多种日志级别:

  1. Debug 日志,非关键日志
  2. Release 日志,关键日志
  3. Error 日志,错误日志
  4. Fatal 日志,致命错误日志

Debug < Release < Error < Fatal(日志级别高低)

在 LeafServer 中,bin/conf/server.json 可以配置日志级别,低于配置的日志级别的日志将不会输出。Fatal 日志比较特殊,每次输出 Fatal 日志之后游戏服务器进程就会结束,通常来说,只在游戏服务器初始化失败时使用 Fatal 日志。

更加详细的用法可以参考 leaf/log。

Leaf recordfile

Leaf 的 recordfile 是基于 CSV 格式(范例见这里)。recordfile 用于管理游戏配置数据。在 LeafServer 中使用 recordfile 非常简单:

  1. 将 CSV 文件放置于 bin/gamedata 目录中
  2. 在 gamedata 模块中调用函数 readRf 读取 CSV 文件

范例:

// 确保 bin/gamedata 目录中存在 Test.txt 文件 // 文件名必须和此结构体名称相同(大小写敏感) // 结构体的一个实例映射 recordfile 中的一行 type Test struct { // 将第一列按 int 类型解析 // "index" 表明在此列上建立唯一索引 Id int "index" // 将第二列解析为长度为 4 的整型数组 Arr [4]int // 将第三列解析为字符串 Str string } // 读取 recordfile Test.txt 到内存中 // RfTest 即为 Test.txt 的内存镜像 var RfTest = readRf(Test{}) func init() { // 按索引查找 // 获取 Test.txt 中 Id 为 1 的那一行 r := RfTest.Index(1) if r != nil { row := r.(*Test) // 输出此行的所有列的数据 log.Debug("%v %v %v", row.Id, row.Arr, row.Str) } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

// 确保 bin/gamedata 目录中存在 Test.txt 文件
// 文件名必须和此结构体名称相同(大小写敏感)
// 结构体的一个实例映射 recordfile 中的一行
typeTeststruct{
    // 将第一列按 int 类型解析
    // "index" 表明在此列上建立唯一索引
    Id  int"index"
    // 将第二列解析为长度为 4 的整型数组
    Arr[4]int
    // 将第三列解析为字符串
    Str string
}
// 读取 recordfile Test.txt 到内存中
// RfTest 即为 Test.txt 的内存镜像
varRfTest=readRf(Test{})
func init(){
    // 按索引查找
    // 获取 Test.txt 中 Id 为 1 的那一行
    r:=RfTest.Index(1)
    ifr!=nil{
        row:=r.(*Test)
        // 输出此行的所有列的数据
        log.Debug("%v %v %v",row.Id,row.Arr,row.Str)
    }
}

更加详细的用法可以参考 leaf/recordfile。

Leaf:GO语言游戏框架介绍及入门相关推荐

  1. ❤️六万字《SpringMVC框架介绍—从入门到高级》(建议收藏)❤️

    <SpringMVC框架介绍>(建议收藏) ❤️希望博友给个三连+关注!!! SSM: mybatis + Spring + SpringMVC MVC三层架构 SpringMVC + V ...

  2. Android游戏框架AndEngine使用入门

    项目站点:http://www.andengine.org 项目地址:http://code.google.com/p/andengine 示例地址:http://code.google.com/p/ ...

  3. 【ORM框架】go语言ORM框架 - gorm快速入门

    文章目录 前言 一.正确看待orm 1.什么是orm 2.常用的orm 3.orm的优缺点 4.如何正确看待orm和sql之间的关系 二.gorm入门 前言 Gorm 是 Golang 的一个 orm ...

  4. go语言MVC框架beego快速入门

    beego快速入门 beego 是一个快速开发 Go 应用的 HTTP 框架,他可以用来快速开发 API.Web 及后端服务等各种应用,是一个 RESTful 的框架,主要设计灵感来源于 tornad ...

  5. 【android-tips】SurfaceView的制作android游戏框架介绍

    (转载请注明出处:http://blog.csdn.net/buptgshengod) 1.介绍 我们知道android游戏主要包含两方面.一方面是控制类,这个通过一些循环以及监听机制来实现.另一方面 ...

  6. GitHub开源游戏框架和引擎介绍

    GitHub开源游戏框架介绍 ET ET的介绍 GitHub地址 NoahGameFrame NoahGameFrame的介绍 GitHub地址 CSDN相关教程 ET ET的介绍 ET是一个开源的游 ...

  7. 游戏程序设计 # ch13 开发工具 # 开发语言、开发环境、腾讯开发组件、网络通信和业务框架介绍

    第十三章 开发工具 1 开发语言 2 开发环境 3 腾讯开发组件介绍 4 网络通信+业务框架介绍 第一节 开发语言 腾讯服务器主要使用的语言是C/C++ 第二节 开发环境 (指:客户端) 第三节 腾讯 ...

  8. swift语言注册非免费苹果账号iOS游戏框架Sprite Kit基础教程

    swift语言注册非免费苹果账号iOS游戏框架Sprite Kit基础教程 1.2.3  注册非免费苹果账号swift语言注册非免费苹果账号iOS游戏框架Sprite Kit基础教程 免费的苹果账号在 ...

  9. Unity3damp;amp;C#分布式游戏服务器ET框架介绍-组件式设计

    前几天写了<开源分享 Unity3d客户端与C#分布式服务端游戏框架>,受到很多人关注,QQ群几天就加了80多个人.开源这个框架的主要目的也是分享自己设计ET的一些想法,所以我准备写一系列 ...

最新文章

  1. 杭州一高中推刷脸吃饭:8秒搞定;乌海市与上海荷福签署合作协议 进入人工智能矿业安全领域...
  2. ifconfig route 手动设置网卡route路由 示例
  3. django得到Model的全部字段名(field)
  4. 苹果Mac强大的采样器音源软件:Native Instruments Kontakt
  5. 利用图片延迟加载来优化页面性能(jQuery)
  6. 倍福--软件界面介绍
  7. 潮流计算和最优潮流计算
  8. Linux Redh9 下LumaQQ 2005的安装
  9. Neural Networks and Deep Learing笔记:一个简单的识别手写数字的神经网络
  10. Excel2013向被粘贴区域的非空单元格内粘贴数据
  11. 2021全网最全接口测试及常用接口测试工具
  12. eclipse java web乱码,eclipse中文乱码解决
  13. 插入mysql数据库时间相差14个小时
  14. HPE还将继续支持下一代Superdome GPU芯片
  15. 创客集结号:3D打印需要依托多个学科领域的尖端技术
  16. BCC-funccount
  17. TEXTOUT颜色和透明背景
  18. 关于编程,我要矫情两句
  19. [组装电脑DIY]#nvidia GeForce GTX1070ti 公版#开箱图赏
  20. 实用Apache的mod_proxy可以实现负载均衡。

热门文章

  1. python秒转换成小时分钟秒_1小时3分59秒!双11天猫交易额超1000亿元
  2. 浏览是不是计算机网络功能,什么是因特网——计算机网络是怎么为你服务的?...
  3. cad怎么画坐标系箭头_CAD绘图中箭头太大(小)怎么调整?
  4. 双流棠湖中学怎么样_全川前十高中,棠湖中学后来居上,七中、四中、成外是“大佬”...
  5. linux mysql 1045 错误_Linux 下,mysql数据库报无法登陆错误:ERROR 1045 (28000): Access denied for use...
  6. fedora如何隐藏顶部状态栏_如何使用PDF Arranger来对PDF文件进行排版和修改
  7. Java综合知识总结_数据库篇
  8. 超声波测距测速升压可调中周倒车雷达变频器传感器1:10
  9. python表达式3and not5的值_太原理工大学python考试题总结
  10. oracle 只读同名词,Oracle创建只读用户,授予视图/同义词/会话权限