本文参考
Gob的数据
手机网络游戏应用协议设计(一)
关于protobuf基础知识,可以参考Protobuf 以及在Laya中的使用
关于varint,参考数值压缩存储方法Varint和图解Protobuf编码
为了让某个数据结构能够在网络上传输或能够保存至文件,它必须被编码然后再解码。当然,已经有许多可用的编码方式了:JSON,XML,Google 的 protocol buffers,等等。而现在,又多了一种,由 Go 的 gob 包提供的方式。
为什么定义新的编码?这要做许多繁重的工作。为什么不使用某个现成的格式?呃,无论如何,我们这样做了!Go 已经有刚才提到的所有编码方式的包(protocol buffer 包在另外一个代码库中,但它是下载得最多的包之一)。并且在许多情况下,包括同其他语言编写的工具和系统通讯,这些都是正确的选择。
但是在特定的 Go 环境中,例如在两个 Go 编写的服务之间通讯,这需要某些东西使得其更加容易使用,并且可能更加有效率。Gobs 协同 Go 语言的工作方式,对于那些外部定义的、同语言无关的编码方式来说无法做到。同时,从现有的系统中也吸取了很多教训。
一、目标
gob 包有在设计时有许多目标。
首先,也是最显然的,它被设计成为非常容易使用的。一方面,由于 Go 有反射(reflection),就没有必要弄一个单独的接口定义语言或“协议编译器”。数据结构本身提供了编码和解码所需要的全部信息。 另一方面,这种方法也意味着 gob 永远无法良好的同其他语言协同工作,但这没问题:gob 是厚颜无耻的以 Go 为中心(译注:呃,以XXX为中心,坚决贯彻XXX的领导……)。
效率也是非常重要的。基于文本形式的,如 XML 和 JSON ,应用于高效通讯网络会太慢了。二进制编码是必须的(译注:二进制神马的,是必须的!回音:必须的……)!
Gob 流必须可以自解释。每个 gob 流,从开始读取,整个流将由包含足够的信息,以便在终端对其内容毫不知情的前提下对整个流加以解析。这一特性意味,即便你忘记了保存在文件中的 gob 流表示什么,也总是可以对其解码。
同样,这里有一些从 Google protocol buffers 获得的经验。
二、Protocol buffer 的硬伤
Protocol buffers 对 gob 的设计产生了主要的影响,但是有三个特性被谨慎的避开了。(暂且不说 protocol buffer 不是自解释的:如果你不知道 protocol buffer 编码时的数据的定义,你就无法解析它。)
首先,protocol buffer 仅工作于 Go 的 struct 数据类型。不能在最顶级编码一个整数或者数组,只可以将其至于 struct 中作为一个字段。 至少在 Go 中,这个限制似乎没有什么意义。如果你希望传输的仅仅是一个数组或者整数,为什么你要先将其放到 struct 中?
其次,可能 protocol buffer 的定义指定字段 T.x 和 T.y 需要解析,无论是在编码还是解码类型 T 的值。虽然,这样的必须字段看起来是个好主意,但是由于编解码器中必须含有用于编码和解码的特定的数据结构,用于报告必须字段是否丢失,实现的开销是大的。这同样也产生了问题。过一段时间后,某人可能希望修改数据定义,移除了必须的字段,但这导致现有接收数据的客户端崩溃。最好是在编码时就根本没有这些字段。(Protocol buffer 也有可选字段。但是,如果我们没有必须字段,所有的字段就是可选的。等一下还会针对可选字段进行一些讨论。)
第三个 protocol buffer 的硬伤是默认值。当 protocol buffer 在某个“默认”字段上设置了默认值,而解码后的结构就像那个字段被设置了某个值一样。这个想法在有 getter 和 setter 控制字段的访问的时候非常棒,但是当容器是一个原始结构的时候就很难控制其保持清晰了。必须的字段也存在同样的麻烦:在哪定义默认值,它们的类型是什么(是UTF-8文本?无符号字节串?在浮点型中有几位?)尽管有许多看起来很简单,protocol buffer 的设计和实现还是有许多伴随的问题。我们决定让这些都远离 gob,并且回到我们的 Go 旅程中,一个很有效率的默认规则:除非你设置了一些内容,否则就是那个类型的“零值”,而这个不需要被传输。
所以 gob 最终看起来是个更加通用、简单的 protocol buffer。它又是如何工作的呢?
三、值
编码后的 gob 数据不是 int8 或者 uint16 的串。作为代替,其看起来更象是 Go 的常量,不论是有符号的还是无符号的整数值是虚拟的、无大小定义的数字。当你编码一个 int8 的时候,其值被转换为一个无大小定义的变长整数。当你对 int64 编码时,其值也是转换为一个无大小定义的变长整数。(有符号和无符号是相同处理的,无大小定义也适用于无符号值。)如果都是值 7,在线传输的位是一致的。当接收者解码其值,它将其放入接收者变量中,可能是任意的一个整数类型。因此,编码方发送了一个来自 int8 的 7,而接收方可能将其保存在 int64 中。这没有问题:这个值永远匹配于一个整数。(如果不匹配,会产生错误。)在变量的大小上解偶,为编码提供了一些灵活性:我们可以随着软件演化扩展整数类型,但是仍然可以解码旧的数据。
这种灵活性对于指针同样有效。在传输前,所有指针都进行整理。int8、int8、*int8、****int8等等的值,被传输为可能被存储于任何大小的 int,或者 *int,或者 ******int等等的整数值。这同样是一种灵活性。
同样的原因,在解码一个 struct,当其字段由编码方发送,存储于目标方的时候,也体现出这种灵活性。给出这样一个值:type T struct { X, Y, Z int } // 只有导出字段(exported fields)被编码和解码。 var t = T{X: 7, Y: 0, Z: 8}
编码后仅发送 7 和 8。由于为零,Y 不会被发送;没有必要发送一个零值。
接收方可能用下面的结构解码:type U struct { X, Y *int8 } // 注意:int8 的指针 var u U
而获得的 u 的值只有 X (值为 7 的 int8 变量的地址);Z 字段被忽略了——你应将其放到哪里呢?当解码一个 struct 的时候,字段会匹配其名字和类型,只有双方都有的字段会生效。这个简单的办法巧妙处理了“可选字段”问题:类型 T 添加了字段,过期的接收者仍然能处理它们知道的那部分。因此 gob 在可选字段上提供了重要的特性——无须任何额外的机制或标识。
从整数串可以构造其他类型:字节数组、字符串、数组、内存片段、Map,甚至浮点数组。IEEE 754 浮点位定义描述了浮点值存储为整数,在你知道其类型的时候,这会工作得很好,我们总是知道类型的吧。另外,这里的整数使用字节翻转的顺序发送,因为一般的浮点数字,就像是小整数数组,在低位上有许多个零是不用传递的。
gob 还有一个非常棒的特性是 Go 使得通过 GobEncoder 和 GobDecoder 接口使得自定义类型的编码成为可能,从某个意义上说类似于 JSON 包的 Marshaler 和 Unmarshaler,以及 fmt 包的 String 化接口。这个技巧使一些特殊功能成为可能,强制使用常量,或者传输数据的时候隐藏信息。
四、类型的传输
在第一次传输给定类型的时候,gob 包中包含了这个类型的描述。实际上,是这样的,编码器编码的是gob标准格式,而内部的 struct 则带有类型描述并给其标识一个唯一编号。(基本类型、类型描述结构的层级,在软件启动时已经定义好了。)在类型被描述后,它可以通过编号来引用。
因此,当我们发送类型 T 时,gob 编码器发送 T 的描述,并对其编号,例如 127。包括第一个数据包在内的所有的数据,都使用这个编号,所以 T 值的数据流看起来是这样:("define type id" 127, definition of type T)(127, T value)(127, T value), ...
类型编号使得描述递归类型,以及发送这些类型的数据成为可能。因此,gob 可以对树状类型做编码:type Node struct { Value int Left, Right *Node }
带有了类型信息,gob 流就完全自说明了。除了那些初始类型,它们已经在开始的时候就定义好了。
五、编译机
在第一次传输给定类型的时候,gob 包会构造一个针对这个类型的小翻译机。在这个类型上使用了反射来构造这个翻译机,但是一旦翻译机构建完成,它就不再依赖反射。这个翻译机使用了 unsafe 和其他一些巧妙的机制来高速的将数据转化为编码后的字节流。也可以使用反射来避免 unsafe,但是会明显变慢。(受到 gob 实现的影响,Go 的 protocol buffer 使用了类似的机制提高速度。)而后的同样类型的值使用已经编译好的翻译机,这样就可以总是有一致的编码。
解码类似,但是略微复杂。当你解码一个数据,gob 包用一个字节片保存编码后的类型的值用于来解码,再加上得到解码的 Go 的值。gob 包构造一个翻译机用于这个过程:gob 类型在线传输用于 Go 类型的解码。一旦解码翻译机构造,一个没有反射的使用 unsafe 方法的引擎能提供最快的速度。
六、例子
例1:数据结构与bytes.Buffer之间的转换(编码成字节切片)
1package main
2
3import (
4 "bytes"
5 "fmt"
6 "encoding/gob"
7 "io"
8)
9
10//准备编码的数据
11type P struct {
12 X, Y, Z int
13 Name string
14}
15
16//接收解码结果的结构
17type Q struct {
18 X, Y *int32
19 Name string
20}
21
22func main() {
23 //初始化一个数据
24 data := P{3, 4, 5, "CloudGeek"}
25 //编码后得到buf字节切片
26 buf := encode(data)
27 //用于接收解码数据
28 var q *Q
29 //解码操作
30 q = decode(buf)
31 //"CloudGeek": {3,4}
32 fmt.Printf("%q: {%d,%d}\n", q.Name, *q.X, *q.Y)
33
34}
35
36func encode(data interface{}) *bytes.Buffer {
37 //Buffer类型实现了io.Writer接口
38 var buf bytes.Buffer
39 //得到编码器
40 enc := gob.NewEncoder(&buf)
41 //调用编码器的Encode方法来编码数据data
42 enc.Encode(data)
43 //编码后的结果放在buf中
44 return &buf
45}
46
47func decode(data interface{}) *Q {
48 d := data.(io.Reader)
49 //获取一个解码器,参数需要实现io.Reader接口
50 dec := gob.NewDecoder(d)
51 var q Q
52 //调用解码器的Decode方法将数据解码,用Q类型的q来接收
53 dec.Decode(&q)
54 return &q
55}
例2:数据结构到文件的序列化和反序列化
1package main
2
3import (
4 "encoding/gob"
5 "os"
6 "fmt"
7)
8
9//试验用的数据类型
10type Address struct {
11 City string
12 Country string
13}
14
15//序列化后数据存放的路径
16var filePath string
17
18func main() {
19 filePath = "./address.gob"
20 encode()
21 pa := decode()
22 fmt.Println(*pa) //{Chengdu China}
23}
24
25//将数据序列号后写到文件中
26func encode() {
27 pa := &Address{"Chengdu", "China"}
28 //打开文件,不存在的时候新建
29 file, _ := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY, 0666)
30 defer file.Close()
31
32 //encode后写到这个文件中
33 enc := gob.NewEncoder(file)
34 enc.Encode(pa)
35}
36
37//从文件中读取数据并反序列化
38func decode() *Address {
39 file, _ := os.Open(filePath)
40 defer file.Close()
41
42 var pa Address
43 //decode操作
44 dec := gob.NewDecoder(file)
45 dec.Decode(&pa)
46 return &pa
47}
七、golang中的rpc
参考
如何给老婆解释什么是RPC
golang - gob与rpc
1.rpc服务端
1package main
2
3import (
4 "net"
5 "net/rpc"
6 "net/http"
7)
8
9type Args struct {
10 A, B int
11}
12
13//定义一个算术类型,其实就是int
14type Arith int
15
16//实现乘法的方法绑定到Arith类型,先不管为什么是这样的形式
17func (t *Arith) Multiply(args *Args, reply *int) error {
18 *reply = args.A * args.B
19 return nil
20}
21
22func main() {
23 //得到一个Arith类型的指针实例
24 arith := new(Arith)
25 //注册到rpc服务
26 rpc.Register(arith)
27 //挂到http服务上
28 rpc.HandleHTTP()
29 //开始监听
30 l, _ := net.Listen("tcp", ":1234")
31 http.Serve(l, nil)
32}
2.rpc客户端
1package main
2
3import (
4 "net/rpc"
5 "fmt"
6)
7
8type Args struct {
9 A, B int
10}
11
12func main() {
13 //连接服务器端,创建一个client
14 client, _ := rpc.DialHTTP("tcp", "127.0.0.1:1234")
15 args := &Args{7, 8}
16 var reply int
17 //通过Call方法调用Arith类型的Multiply方法,注意形参
18 client.Call("Arith.Multiply", args, &reply)
19 //得到调用结果,输出Arith: 7*8=56
20 fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
21}
下面我们再来看一些rpc相关的细节,首先能够被rpc调用的方法应该看起来像这样:func (t *T) MethodName(argType T1, replyType *T2) error
,大概解释一下:
- 函数必须是可导出的(首字母大写)
- 必须有两个导出类型的参数,第一个参数用来接收参数,第二个参数是返回给客户端的结果参数,第二个参数必须是指针类型的
- 函数还要有一个返回值error
- T1、T2能够被encoding/gob编码
看到这里你应该对于rpc的作用有了一定的认识,go中rpc包的用法简单来看就是准备一个类型,绑定一堆符合规范的方法,然后注册给rpc服务,监听客户端连接,客户端通过rpc包提供的Call方法可以调用到server注册好的方法。
有疑问加站长微信联系(非本文作者)